← Back to Overview

Authentication Guide

Learn how to sign requests and authenticate your AI agent with Ed25519 signatures.

How Authentication Works

Agent Passport uses Ed25519 digital signatures to authenticate requests. Instead of sending API keys or tokens, your agent signs each request with its private key. The server verifies the signature using the agent's public key from their DID document.

Key benefit: No secrets are ever transmitted over the network. Even if an attacker intercepts a request, they cannot forge new requests without the private key.

Required Headers

Every authenticated request must include these headers:

HeaderDescriptionExample
Agent-DIDYour agent's DID identifierdid:nervepay:agent:7xKp...
X-Agent-SignatureBase64-encoded Ed25519 signatureed25519:abc123...
X-Agent-NonceOne-time random string (UUID recommended)550e8400-e29b...
X-Signature-TimestampUnix timestamp in seconds1706918400

Signature Payload

The signature is computed over a canonical string containing the request details:

text
{method}\n{path}\n{nonce}\n{timestamp}\n{agent_did}

For example, a GET request to /api/data would sign:

text
GET
/api/data
550e8400-e29b-41d4-a716-446655440000
1706918400
did:nervepay:agent:7xKpQm3...

Python Example

Complete example using the nacl library:

python
import nacl.signing
import base64
import base58
import time
import uuid
import requests
class AgentAuth:
"""Authenticate API requests using Agent Passport."""
def __init__(self, agent_did: str, private_key_base58: str):
self.agent_did = agent_did
# Decode the base58 private key (64 bytes: 32 seed + 32 public)
key_bytes = base58.b58decode(private_key_base58)
self.signing_key = nacl.signing.SigningKey(key_bytes[:32])
def sign_request(self, method: str, path: str) -> dict:
"""Generate authentication headers for a request."""
nonce = str(uuid.uuid4())
timestamp = str(int(time.time()))
# Create the payload to sign
payload = f"{method}\n{path}\n{nonce}\n{timestamp}\n{self.agent_did}"
# Sign the payload
signature = self.signing_key.sign(payload.encode())
signature_b64 = base64.b64encode(signature.signature).decode()
return {
"Agent-DID": self.agent_did,
"X-Agent-Signature": f"ed25519:{signature_b64}",
"X-Agent-Nonce": nonce,
"X-Signature-Timestamp": timestamp,
}
def request(self, method: str, url: str, **kwargs) -> requests.Response:
"""Make an authenticated request."""
from urllib.parse import urlparse
path = urlparse(url).path
headers = kwargs.pop("headers", {})
headers.update(self.sign_request(method, path))
return requests.request(method, url, headers=headers, **kwargs)
# Usage
agent = AgentAuth(
agent_did="did:nervepay:agent:7xKpQm3...",
private_key_base58="your-private-key-base58"
)
# Make authenticated requests
response = agent.request("GET", "https://api.example.com/data")
print(response.json())

TypeScript Example

Using tweetnacl and bs58:

typescript
import nacl from 'tweetnacl';
import bs58 from 'bs58';
import { v4 as uuidv4 } from 'uuid';
interface AuthHeaders {
'Agent-DID': string;
'X-Agent-Signature': string;
'X-Agent-Nonce': string;
'X-Signature-Timestamp': string;
}
class AgentAuth {
private agentDid: string;
private signingKey: Uint8Array;
constructor(agentDid: string, privateKeyBase58: string) {
this.agentDid = agentDid;
// Decode base58 private key
const keyBytes = bs58.decode(privateKeyBase58);
this.signingKey = keyBytes.slice(0, 64); // Full keypair for tweetnacl
}
signRequest(method: string, path: string): AuthHeaders {
const nonce = uuidv4();
const timestamp = Math.floor(Date.now() / 1000).toString();
// Create payload
const payload = `${method}\n${path}\n${nonce}\n${timestamp}\n${this.agentDid}`;
// Sign
const message = new TextEncoder().encode(payload);
const signature = nacl.sign.detached(message, this.signingKey);
const signatureB64 = Buffer.from(signature).toString('base64');
return {
'Agent-DID': this.agentDid,
'X-Agent-Signature': `ed25519:${signatureB64}`,
'X-Agent-Nonce': nonce,
'X-Signature-Timestamp': timestamp,
};
}
async fetch(url: string, options: RequestInit = {}): Promise<Response> {
const urlObj = new URL(url);
const method = options.method || 'GET';
const authHeaders = this.signRequest(method, urlObj.pathname);
return fetch(url, {
...options,
headers: {
...options.headers,
...authHeaders,
},
});
}
}
// Usage
const agent = new AgentAuth(
'did:nervepay:agent:7xKpQm3...',
'your-private-key-base58'
);
const response = await agent.fetch('https://api.example.com/data');
const data = await response.json();

cURL Example

For testing, you can manually construct the headers:

bash
# Generate signature (using openssl for demo)
NONCE=$(uuidgen)
TIMESTAMP=$(date +%s)
DID="did:nervepay:agent:7xKpQm3..."
# Make the request
curl -X GET "https://api.nervepay.xyz/v1/agent-identity/whoami" \
-H "Agent-DID: $DID" \
-H "X-Agent-Signature: ed25519:<base64-signature>" \
-H "X-Agent-Nonce: $NONCE" \
-H "X-Signature-Timestamp: $TIMESTAMP"

Server-Side Verification

If you're building a service that accepts Agent Passport authentication, here's how to verify signatures:

python
import nacl.signing
import base64
import time
import requests
def verify_agent_request(
agent_did: str,
signature: str,
nonce: str,
timestamp: str,
method: str,
path: str,
used_nonces: set, # Your nonce storage
) -> bool:
"""Verify an agent's request signature."""
# 1. Check timestamp freshness (5-minute window)
current_time = int(time.time())
request_time = int(timestamp)
if abs(current_time - request_time) > 300: # 5 minutes
return False
# 2. Check nonce hasn't been used (replay protection)
if nonce in used_nonces:
return False
# 3. Fetch agent's public key from DID document
did_response = requests.get(
f"https://api.nervepay.xyz/v1/did/resolve/{agent_did}"
)
did_doc = did_response.json()
public_key_b58 = did_doc["verificationMethod"][0]["publicKeyBase58"]
# 4. Verify the signature
import base58
public_key = base58.b58decode(public_key_b58)
verify_key = nacl.signing.VerifyKey(public_key)
# Reconstruct the payload
payload = f"{method}\n{path}\n{nonce}\n{timestamp}\n{agent_did}"
# Extract signature bytes
sig_b64 = signature.replace("ed25519:", "")
sig_bytes = base64.b64decode(sig_b64)
try:
verify_key.verify(payload.encode(), sig_bytes)
used_nonces.add(nonce) # Mark nonce as used
return True
except nacl.exceptions.BadSignature:
return False

Security Considerations

Timestamp Validation

Requests older than 5 minutes are rejected. This prevents replay attacks using captured requests.

Nonce Replay Protection

Each nonce can only be used once. Store used nonces for at least 10 minutes (the nonce expiry window) to prevent replay attacks.

Private Key Storage

Never expose your private key in client-side code, logs, or version control. Use environment variables or secure key management services.

Error Responses

Authentication failures return specific error codes:

HTTP CodeErrorDescription
401missing_headersRequired authentication headers are missing
401invalid_signatureSignature verification failed
401timestamp_expiredTimestamp is outside the 5-minute window
401nonce_reusedNonce has already been used
404agent_not_foundAgent DID does not exist

Next Steps