← Back to Agent Passport Overview

Agent Secrets Vault

Secure per-agent secrets management with AES-256-GCM encryption and audit logging

Production Ready

Overview

The Agent Secrets Vault provides secure, encrypted storage for API keys, credentials, and tokens that your agents need to access external services. Think of it like Vercel's environment variables, but per-agent with cryptographic access control.

Zero-Trust Security Model

Your human owner configures secrets in the dashboard. Agents can only read their own secrets (verified by Ed25519 signature), never create, update, or delete them. This prevents compromised agents from modifying credentials.

Quickstart (2 minutes)

Get from “no secrets” to “my agent can read a secret” with the fewest steps.

  1. In the dashboard, open your agent → SecretsNew secret.
  2. Add a name (e.g. OPENAI_API_KEY) and value.
  3. From your agent runtime, call GET /v1/vault/secrets/:name with your agent signature headers.
  4. Use the returned value immediately (don’t print it, don’t store it in logs).

Common gotchas: most failures come from a DID mismatch or a signature timestamp/nonce issue. If you’re stuck, log the HTTP status + error body (not the secret).

Want a copy/paste example that generates a real X-Agent-Signature? See Docs → Examples → Node Signing.

Key Features

Envelope Encryption

Secrets encrypted with AES-256-GCM using unique data encryption keys (DEK), which are themselves encrypted with a master key encryption key (KEK) derived via HKDF

Per-Agent Isolation

Each agent only sees their own secrets. Access verified via Ed25519 signature authentication on every request

Complete Audit Trail

Every secret access logged with timestamp, IP address, user agent, and success/failure status

Environment Support

Organize secrets by environment (production, development, staging) and provider (openai, stripe, github)

Expiration Support

Set expiration dates on secrets. Expired secrets return 410 Gone and are automatically flagged

Read-Only for Agents

Agents can only retrieve secrets. All create/update/delete operations require dashboard JWT authentication

How It Works

1

Human Configures Secrets

Developer logs into NervePay dashboard, navigates to their agent, and creates secrets in the Secrets tab. Each secret has a name, value, optional description, provider, and environment.

2

Server Encrypts Secret

Backend generates a random 256-bit data encryption key (DEK), encrypts the secret value with AES-256-GCM, then encrypts the DEK with the master key encryption key (KEK) derived from SUPABASE_SERVICE_ROLE_KEY via HKDF.

3

Agent Requests Secret

Agent signs request with Ed25519 private key and calls GET /v1/vault/secrets/SECRET_NAME. Server verifies signature and checks that the agent DID matches the secret owner.

4

Server Decrypts & Returns

Backend decrypts the DEK using KEK, then decrypts the secret value using the DEK. Returns plaintext value to agent. Access is logged to secret_access_log table.

5

Agent Uses Secret

Agent uses the decrypted secret (e.g., API key) to call external services like OpenAI, Stripe, GitHub, etc. Agent should immediately track usage with /v1/agent-identity/track-service.

Copy/paste snippets

Minimal examples for signing requests and reading Vault secrets. Don’t log secret values.

TypeScript (Node)

ts
// Minimal Passport signing + Vault get (Node)
import crypto from "node:crypto";
const BASE58 = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
function decodeBase58(str: string): Buffer {
const bytes: number[] = [];
for (const char of str) {
const index = BASE58.indexOf(char);
if (index === -1) throw new Error('Invalid base58 char: ' + char);
let carry = index;
for (let i = 0; i < bytes.length; i++) {
carry += bytes[i] * 58;
bytes[i] = carry & 0xff;
carry >>= 8;
}
while (carry > 0) {
bytes.push(carry & 0xff);
carry >>= 8;
}
}
for (const char of str) {
if (char !== "1") break;
bytes.push(0);
}
return Buffer.from(bytes.reverse());
}
function signEd25519(privateKeyBase58: string, payload: unknown): string {
const keyStr = privateKeyBase58.replace(/^ed25519:/, "");
const privateKeyBytes = decodeBase58(keyStr);
const seed = privateKeyBytes.subarray(0, 32);
const pkcs8Header = Buffer.from("302e020100300506032b657004220420", "hex");
const keyObject = crypto.createPrivateKey({
key: Buffer.concat([pkcs8Header, seed]),
format: "der",
type: "pkcs8",
});
const msg = JSON.stringify(payload);
const sig = crypto.sign(null, Buffer.from(msg), keyObject);
return sig.toString("base64");
}
function authHeaders({ did, privateKey, method, path, bodyJson }: any) {
const nonce = crypto.randomUUID();
const timestamp = new Date().toISOString();
const bodyHash = bodyJson
? crypto.createHash("sha256").update(JSON.stringify(bodyJson)).digest("hex")
: null;
const payload = {
method: method.toUpperCase(),
path,
query: null,
body: bodyHash,
nonce,
timestamp,
agent_did: did,
};
return {
"Agent-DID": did,
"X-Agent-Signature": signEd25519(privateKey, payload),
"X-Agent-Nonce": nonce,
"X-Signature-Timestamp": timestamp,
};
}
async function vaultGetSecret({ apiUrl, did, privateKey, name }: any) {
const path = "/v1/vault/secrets/" + encodeURIComponent(name);
const headers = authHeaders({ did, privateKey, method: "GET", path });
const res = await fetch(apiUrl + path, { method: "GET", headers });
if (!res.ok) throw new Error(await res.text());
return res.json();
}

Python

py
# pip install pynacl requests
import base64
import hashlib
import json
import time
import uuid
import requests
from nacl.signing import SigningKey
BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
def b58decode(s: str) -> bytes:
n = 0
for ch in s:
n *= 58
n += BASE58_ALPHABET.index(ch)
h = n.to_bytes((n.bit_length() + 7) // 8, "big")
pad = 0
for ch in s:
if ch == "1":
pad += 1
else:
break
return b"" * pad + h
def nervepay_headers(did: str, private_key_b58: str, method: str, path: str, body_json: dict | None):
nonce = str(uuid.uuid4())
timestamp = time.strftime("%Y-%m-%dT%H:%M:%S.000Z", time.gmtime())
body_hash = None
if body_json is not None:
body_hash = hashlib.sha256(json.dumps(body_json).encode("utf-8")).hexdigest()
payload = {
"method": method.upper(),
"path": path,
"query": None,
"body": body_hash,
"nonce": nonce,
"timestamp": timestamp,
"agent_did": did,
}
key_str = private_key_b58.replace("ed25519:", "")
seed = b58decode(key_str)[:32]
signing_key = SigningKey(seed)
msg = json.dumps(payload).encode("utf-8")
sig = signing_key.sign(msg).signature
return {
"Agent-DID": did,
"X-Agent-Signature": base64.b64encode(sig).decode("utf-8"),
"X-Agent-Nonce": nonce,
"X-Signature-Timestamp": timestamp,
}
def vault_get(api_url: str, did: str, private_key: str, name: str):
path = f"/v1/vault/secrets/{name}"
headers = nervepay_headers(did, private_key, "GET", path, None)
r = requests.get(api_url + path, headers=headers, timeout=30)
r.raise_for_status()
return r.json()

API Reference

Get Secret by Name

Retrieve a specific secret's decrypted value. Most common operation for agents.

GET/v1/vault/secrets/:name

Authentication: Agent Ed25519 Signature (required)

bash
curl "https://api.nervepay.xyz/v1/vault/secrets/OPENAI_API_KEY" \
-H "Agent-DID: did:nervepay:agent:abc123xyz" \
-H "X-Agent-Signature: ${SIGNATURE}" \
-H "X-Agent-Nonce: $(uuidgen)" \
-H "X-Signature-Timestamp: $(date -u +%Y-%m-%dT%H:%M:%SZ)"

Response (200 OK):

json
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "OPENAI_API_KEY",
"value": "sk-abc123...",
"description": "OpenAI API key for production",
"provider": "openai",
"environment": "production",
"created_at": "2026-02-05T12:00:00Z",
"updated_at": "2026-02-05T12:00:00Z",
"expires_at": null
}

List All Secrets

Get all secrets for your agent. Values are NOT included for security (use Get Secret by Name to retrieve values).

GET/v1/vault/secrets

Authentication: Agent Ed25519 Signature (required)

Query params: environment, provider

bash
# List all secrets
curl "https://api.nervepay.xyz/v1/vault/secrets" \
-H "Agent-DID: did:nervepay:agent:abc123xyz" \
-H "X-Agent-Signature: ${SIGNATURE}" \
-H "X-Agent-Nonce: $(uuidgen)" \
-H "X-Signature-Timestamp: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
# Filter by environment
curl "https://api.nervepay.xyz/v1/vault/secrets?environment=production" ...
# Filter by provider
curl "https://api.nervepay.xyz/v1/vault/secrets?provider=openai" ...

Response (200 OK):

json
{
"secrets": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "OPENAI_API_KEY",
"description": "OpenAI API key",
"provider": "openai",
"environment": "production",
"created_at": "2026-02-05T12:00:00Z",
"updated_at": "2026-02-05T12:00:00Z",
"expires_at": null
}
]
}

Common Secret Names

Use these standardized naming conventions for consistency across agents:

Secret NameProviderPurpose
OPENAI_API_KEYopenaiOpenAI API authentication
ANTHROPIC_API_KEYanthropicClaude API authentication
STRIPE_SECRET_KEYstripeStripe payments
GITHUB_TOKENgithubGitHub API access
DATABASE_URLdatabaseDatabase connection string
AWS_ACCESS_KEY_IDawsAWS credentials (access key)
AWS_SECRET_ACCESS_KEYawsAWS credentials (secret key)
WEBHOOK_SECRETgenericWebhook signature verification
SMTP_PASSWORDsmtpEmail service password
API_BASE_URLgenericExternal API base URL

Example Workflow

Here's a complete example of an agent retrieving an OpenAI API key and using it:

bash
#!/bin/bash
# 1. Retrieve secret from vault
response=$(curl -s "https://api.nervepay.xyz/v1/vault/secrets/OPENAI_API_KEY" \
-H "Agent-DID: ${NERVEPAY_DID}" \
-H "X-Agent-Signature: ${SIGNATURE}" \
-H "X-Agent-Nonce: $(uuidgen)" \
-H "X-Signature-Timestamp: $(date -u +%Y-%m-%dT%H:%M:%SZ)")
# 2. Extract the secret value
OPENAI_KEY=$(echo "$response" | jq -r '.value')
# 3. Use it to call OpenAI
openai_response=$(curl -s https://api.openai.com/v1/chat/completions \
-H "Authorization: Bearer $OPENAI_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-4",
"messages": [{"role": "user", "content": "Hello!"}]
}')
# 4. Track usage (mandatory!)
curl -X POST "https://api.nervepay.xyz/v1/agent-identity/track-service" \
-H "Agent-DID: ${NERVEPAY_DID}" \
-H "X-Agent-Signature: ${TRACK_SIGNATURE}" \
-d '{
"service_name": "openai",
"endpoint": "/v1/chat/completions",
"success": true,
"response_time_ms": 1250
}'

Security Best Practices

Never log secret values

Ensure your agent's logging configuration never prints secret values to console or files

Use environment-specific secrets

Separate secrets for production, development, and staging to limit blast radius

Rotate secrets regularly

Update secrets in the dashboard periodically. Agents automatically get new values on next fetch

Set expiration dates

Use the expires_at field for temporary credentials or time-limited API keys

Monitor audit logs

Review secret_access_log in dashboard to detect unusual access patterns

Limit secret scope

Only store secrets that agents actually need. Use provider field to organize

Error Handling

404 Not Found

Secret doesn't exist for your agent. Ask your human owner to create it in the dashboard.

json
{
"error": "Secret not found",
"message": "Secret 'NONEXISTENT_KEY' not found for agent did:nervepay:agent:abc123xyz"
}

410 Gone

Secret has expired. Ask your human owner to update the expiration date or rotate the secret.

json
{
"error": "Secret expired",
"message": "Secret 'TEMP_API_KEY' expired on 2026-02-01T00:00:00Z"
}

401 Unauthorized

Signature verification failed. Check that you're using the correct private key and DID.

json
{
"error": "Unauthorized",
"message": "Invalid signature or expired timestamp"
}

Next Steps