Install
openclaw skills install agent-mandate-protocolUse A-MAP (Agent Mandate Protocol) to verify incoming agent requests, sign outgoing requests, and delegate permissions to sub-agents. Covers the full cryptog...
openclaw skills install agent-mandate-protocolA-MAP (Agent Mandate Protocol) gives AI agents cryptographic proof of what they are authorized to do — and lets services verify that proof before acting.
npm install @agentmandateprotocol/core
Use this when another agent sends you a request and you need to confirm it was authorized by a human before acting on it.
X-AMAP-Mandate, X-AMAP-Signature, X-AMAP-Timestamp,
X-AMAP-Nonce, or X-AMAP-Agent-DID headersimport { amap, InMemoryNonceStore, LocalKeyResolver } from '@agentmandateprotocol/core'
const keyResolver = new LocalKeyResolver(new Map([
['did:amap:sender-agent:1.0:abc', process.env.SENDER_PUBKEY],
]))
// Use Redis or Cloudflare KV in production — see Guardrails
const nonceStore = new InMemoryNonceStore()
try {
const result = await amap.verifyRequest({
headers: {
'X-AMAP-Agent-DID': request.headers['x-amap-agent-did'],
'X-AMAP-Mandate': request.headers['x-amap-mandate'],
'X-AMAP-Signature': request.headers['x-amap-signature'],
'X-AMAP-Timestamp': request.headers['x-amap-timestamp'],
'X-AMAP-Nonce': request.headers['x-amap-nonce'],
},
method: request.method,
path: request.path,
body: request.body,
expectedPermission: 'book_flight',
keyResolver,
nonceStore,
})
// Safe to proceed
console.log('Authorized by:', result.principal)
console.log('Effective limits:', result.effectiveConstraints)
console.log('Audit ID:', result.auditId) // always log this
} catch (err) {
// A-MAP throws on any failure — never returns { valid: false }
console.error(`Authorization failed: [${err.code}] ${err.message}`)
// Reject the request
}
On success (no error thrown):
result.principal — the human who originally authorized this chainresult.effectiveConstraints — merged limits across all hops (e.g. maxSpend: 347)result.chain — array of verified links, one per hopresult.auditId — UUID for this verification event — log it for audit trailOn failure (AmapError thrown):
err.code — specific error code (see references/error-codes.md)err.hop — which link in the chain failed (0 = root), if applicableverifyRequest() throwsresult.auditId for audit trailInMemoryNonceStore does not work behind a load balancer —
use a shared store (Redis, Cloudflare KV) in multi-instance deploymentsresult.effectiveConstraints before consequential actions
(e.g. check maxSpend before charging a card)AmapError means the agent was not authorized, the request is a replay,
the chain was forged, or the identity is being spoofed — always rejectUse this before calling any A-MAP-protected service to attach cryptographic proof that a human authorized your action.
amap.issue() or amap.delegate())AMAP_PRIVATE_KEYimport { amap } from '@agentmandateprotocol/core'
const headers = amap.signRequest({
mandateChain: myMandateChain,
method: 'POST',
path: '/api/book-flight',
body: JSON.stringify(requestBody), // omit if no body
privateKey: process.env.AMAP_PRIVATE_KEY,
})
await fetch('https://api.example.com/book-flight', {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...headers },
body: JSON.stringify(requestBody),
})
amap.signRequest() returns five headers ready to spread:
| Header | Content |
|---|---|
X-AMAP-Agent-DID | DID of the signing agent |
X-AMAP-Mandate | Base64url-encoded DelegationToken chain |
X-AMAP-Signature | Ed25519 signature over canonical payload |
X-AMAP-Timestamp | ISO8601 UTC timestamp |
X-AMAP-Nonce | 128-bit random hex string (single-use) |
See references/signed-request-format.md for the full payload schema.
AMAP_PRIVATE_KEY — always use an environment variablesignRequest() call — never reuse headersTOKEN_EXPIREDUse this when spawning a sub-agent that needs its own cryptographic proof of authorization to call external services on your behalf.
import { amap } from '@agentmandateprotocol/core'
// myToken = DelegationToken you received; myChain = full chain including myToken
let childToken
try {
childToken = await amap.delegate({
parentToken: myToken,
parentChain: myChain,
delegate: 'did:amap:sub-agent:1.0:xyz',
permissions: ['charge_card'], // must be subset of myToken.permissions
constraints: { maxSpend: 347 }, // can only tighten, never relax
expiresIn: '15m', // cannot exceed parent's remaining TTL
privateKey: process.env.AMAP_PRIVATE_KEY,
})
} catch (err) {
// AmapError thrown BEFORE signing if an invariant is violated:
// PERMISSION_INFLATION — permissions not in parent
// CONSTRAINT_RELAXATION — constraint looser than parent
// EXPIRY_VIOLATION — TTL exceeds parent's remaining time
throw err
}
// Pass the full chain to the sub-agent — not just the child token
const subAgentChain = [...myChain, childToken]
The sub-agent uses amap.signRequest({ mandateChain: subAgentChain, ... }) to
attach this chain to its outgoing requests.
| Task type | Recommended TTL |
|---|---|
| Single API call | 15s |
| One-off task | 60s |
| Short workflow | 5m |
| Extended session | Match parent — SDK enforces the ceiling |
references/delegation-invariants.md)subAgentChain (full chain), not just the new tokenexpiresIn for sub-agentschildToken.tokenId for audit trailAMAP_PRIVATE_KEY — each agent has its own keypair