Chapters 05 · 07 · 09 · 10 · 11 · 12

Build agents that
actually transact.

A per-agent MCP server. AP2 over JWS. x402 over EIP-3009. Shared Payment Tokens and PCI-vaulted card tokens. Every protocol an autonomous agent needs, wired into one Express process and one ledger.

~15 Per-agent MCP tools1,100+ Tool API endpoints3 Mandate types4 Outbound rails

The per-agent MCP server.

Each agent has its own MCP endpoint at /mcp/agent, scoped to one row in agent_identities. Drop the URL and the agent's pak_… key into any MCP-compatible client and the agent is online — wallet, card, channels, AP2 signing, x402, all surfaced as tools.

shipped

/mcp/agent — per-agent endpoint

Scoped to one agent_identities.id. ~15 tools: wallet status, send payment, list activity, request approval, sign mandate. Authenticated by pak_… bearer.

shipped

Discovery at /.well-known/agent.json

Public document advertising the agent's DID, AP2 public key, supported rails, and tool catalog — so external agents and merchants can discover and verify before they transact.

Bootstrap an MCP client

Drop the URL + key into any MCP-compatible client (any LLM client, coding agent, or your own host). The server auto-advertises its tool catalog via JSON-RPC tools/list:

~/.config/mcp.jsonjson
{"mcpServers": {"agentwallet": {"url": "https://api.agentwallet.ai/mcp/agent","headers": {"Authorization": "Bearer pak_live_a1f9c2…"}}}}

Tool call shape

JSON-RPC over HTTPjsonrpc
// Request{"jsonrpc": "2.0", "id": 42,"method": "tools/call","params": {"name": "wallet.send_payment","arguments": {"recipient_handle": "[email protected]","amount_cents": 12500,"currency": "USD","memo": "Invoice #INV-208"}}}// Response — auto-approved (under per-txn cap){"jsonrpc": "2.0", "id": 42,"result": {"content": [{ "type": "text","text": "Sent $125.00 USD via ACH. ref=pay_01J…" }],"isError": false}}

Per-agent tool catalog

ToolWhat it doesLimit-gated?
wallet.statusComputed balance + holds + headroom
wallet.send_paymentInitiate fiat payout via available railyes
wallet.send_usdcUSDC transfer on Base via CDPyes
wallet.x402_paySign EIP-3009 auth for x402 endpointyes
wallet.list_activityRecent transactions across sources
wallet.request_approvalSurface pending action to Principal widget
card.detailsLast4 + balance + status (no PAN)
card.recent_authorizationsCard-level transactions only
comms.send_smsOutbound SMS via telephony provider
comms.send_emailOutbound email via email provider
comms.list_inboxUnread inbound messages
identity.who_am_iAgent metadata + AP2 public key
identity.sign_mandateES256 sign an AP2 IntentMandate
identity.discoverResolve another agent via .well-known
approvals.pollCheck status of a pending approval

AP2. Three mandates, one chain.

The Agent Payments Protocol uses ES256-signed JWS tokens to make agent intent cryptographically provable. A complete payment is a chain of three signed mandates: Intent → Cart → Payment. Every signature is verified against the agent's published JWK and audited.

AP2 mandate chain · ES256 / JWS

IntentMandate

"I authorize agent X to spend ≤ $Y for purpose Z"

Principal-signed

CartMandate

Specific items + merchant + total + intent_id

Agent-signed (under intent)

PaymentMandate

Rail + recipient + cart_id + final amount

Agent-signed (under cart)

JWS structure

PaymentMandate JWS payloadjws
{"iss": "did:agentwallet:ag_01J…", // agent identity"sub": "[email protected]","aud": "https://vendor.example/ap2/v1","iat": 1746345600,"exp": 1746349200, // 1 hour"jti": "01J9…uuidv7","mandate_type": "payment","intent_id": "01J9intent…","cart_id": "01J9cart…","rail": "x402_usdc_base","amount": { "value": "125.00", "currency": "USD" },"recipient": { "address": "0xabc…" }}// Header{ "alg": "ES256", "kid": "ag_01J9…#k1", "typ": "JWS" }

Sign + verify

server/services/ap2/sign-mandate.tstypescript
import { SignJWT, jwtVerify, importJWK } from 'jose';export async function signPaymentMandate(agentId: string, claims: PaymentMandateClaims) {const agent = await getAgentIdentity(agentId);const privJwk = await decryptAp2PrivateKey(agent.ap2PrivateKeyEncrypted);const key = await importJWK(privJwk, 'ES256');return new SignJWT(claims) .setProtectedHeader({ alg: 'ES256', kid: agent.ap2KeyId, typ: 'JWS' }) .setIssuer(`did:agentwallet:${agent.id}`) .setIssuedAt() .setExpirationTime('1h') .setJti(uuidv7()) .sign(key);}

Inbound verification flow

  • Resolve issuer. Look up iss via /.well-known/agent.json on the agent's domain to fetch the published JWK.
  • Verify signature. jwtVerify(token, jwk) with strict alg: 'ES256'.
  • Verify chain. CartMandate must reference an existing IntentMandate. PaymentMandate must reference an existing Cart. Each must be unexpired.
  • Verify policy. Cart total ≤ Intent cap. Payment amount = Cart total. Merchant in Intent allowlist (if set).
  • Audit. Insert row into ap2_mandate_audit with action='verify' and the result.
  • Execute. Hand the verified PaymentMandate to the rail processor (x402, ACH via banking partner, or Shared Payment Token).

Three external rails. One mandate per payment.

Outbound payments leave through one of three protocol stacks. The choice is decided by the merchant's capability advertisement (or by the rail the agent's policy permits).

x402 over EIP-3009

The merchant returns HTTP 402 Payment Required with a payment offer. The agent signs an EIP-3009 transferWithAuthorization (USDC on Base) for the offered amount and replays the request with the signed authorization in the X-PAYMENT header.

x402 round triphttp
// 1. Agent's first requestGET /api/dataset/v1 HTTP/1.1 Host: data.example Authorization: Bearer pak_…// 2. Merchant returns 402 with offerHTTP/1.1 402 Payment Required Content-Type: application/json{"x402": {"amount": "0.10", "currency": "USDC","network": "base","recipient": "0xabc…","validBefore": 1746349200}}// 3. Agent signs EIP-3009 (CDP-managed key) and replaysGET /api/dataset/v1 HTTP/1.1 Host: data.example X-PAYMENT: eyJ0eXAiOiJ4NDAyIiwiYWxnIjoi… // signed auth

Shared Payment Tokens (SPT)

For card-rail payments to merchants that accept network-issued single-use tokens. The agent requests an SPT, scoped to a single use and to a specific merchant + amount. The token expires; replay is structurally impossible.

SPT issuancetypescript
const spt = await issuer.paymentTokens.create({type: 'card', card: { vault_alias: card.vaultAlias }, single_use: true, restrictions: {amount: cartTotalCents, merchant: merchantId, expires_at: Math.floor(Date.now() / 1000) + 300}});// Persist for auditawait db.insert(acpSptIssuances).values({agentIdentityId, tokenId: spt.id, cartMandateId, amountCents: cartTotalCents});

PCI-vaulted card tokens — when the merchant only takes a PAN

For merchants without modern token APIs, the agent gets a vault-tokenized card alias that the outbound proxy substitutes back to a real PAN at the network edge. The agent never sees the PAN; the application server never holds it.

Discovery · /.well-known/agent.json

The ACP standard defines how an agent publishes its capabilities, rails, and AP2 public keys. Merchants and counterparty agents fetch this before transacting.

capabilities documentjson
{"agent_id": "did:agentwallet:ag_01J9…","name": "Acme Procurement Agent","public_keys": [{ "kid": "ag_01J9…#k1", "alg": "ES256","jwk": { "kty": "EC", "crv": "P-256", "x": "…", "y": "…" }}],"endpoints": {"ap2_inbound": "https://agent.acme.example/ap2/v1/payment","a2a": "https://agent.acme.example/a2a/v1"},"accepts": ["x402_usdc_base", "spt", "ach"],"erc8004": { "chainId": 8453, "tokenId": "42" }}

Approvals & Caps. The velocity engine.

The Approval Engine intercepts every outbound payment, evaluates the agent's policy, checks current velocity, and decides: Approve, Deny, or Pend.

The Decision Rule

IF tx_amount > per_tx_cap THEN DENY
IF tx_amount + daily_spent > daily_cap THEN DENY
IF merchant NOT IN allowlist THEN DENY
IF tx_amount > auto_approve_threshold THEN PEND
ELSE APPROVE

Three-cap policy

Every agent has three native caps built into agent_identities. They are checked atomically against the ledger.

policy gatetypescript
const [dailySpent, monthlySpent] = await Promise.all([ getSpendSince(agentId, startOfDay()), getSpendSince(agentId, startOfMonth()) ]);if (amountCents > agent.perTxnLimitCents) throw new Error('PER_TXN_EXCEEDED');if (amountCents + dailySpent > agent.dailyLimitCents) throw new Error('DAILY_EXCEEDED');if (amountCents + monthlySpent > agent.monthlyLimitCents) throw new Error('MONTHLY_EXCEEDED');

Widget state in memory

When a payment is PEND, the agent's MCP call hangs open. The approval engine spawns an ApprovalTicket in memory and pushes a widget to the Principal via AgentSim (iOS/Android push) or WhatsApp.

Memory loss on restart

Yes, pending widget approvals are lost if the Express process restarts. The agent's MCP call will timeout. The agent handles the timeout by retrying, which spawns a new approval ticket. No double-spend due to idempotency keys.

Approve spend requestWhatsApp

triage-bot-01 is requesting to pay LLM provider

$180.00

Force-approve / Admin override

An admin can override any cap or pending approval via the admin dashboard. This writes a specific admin_audit_log entry and forcefully transitions the widget state.

admin overridetypescript
await db.insert(adminAuditLog).values({adminId: req.user.id, action: 'force_approve_tx', targetId: txId, reason: req.body.reason});// Bypass policy gateawait executePayment(txId, { bypassPolicy: true });

Portal API & Auth.

The frontend dashboard is driven entirely by the /api/agent-portal router. It is RBAC-gated and distinct from the MCP and Admin endpoints.

Agent Portal API Endpoints

EndpointWhat it does
GET /api/agent-portal/agentsList all agents scoped to Principal's client
POST /api/agent-portal/agentsProvision a new agent wallet
GET /api/agent-portal/agents/:id/wallet/transactionsUnified activity feed
POST /api/agent-portal/agents/:id/wallet/creditAdd funds from master balance
GET /api/agent-portal/agents/:id/cardCard metadata (no PAN)
POST /api/agent-portal/agents/:id/card/revealGet short-lived VGS token to show PAN in iframe
GET /api/agent-portal/teamList Principals in this client
POST /api/agent-portal/auth/magic-linkRequest OTP email

Auth flow: Magic-link OTP

We removed passwords in v0.4.4. Principals authenticate via a 6-digit code sent to their email.

  • Request. Principal submits email. Server generates 6-digit code, hashes it, stores in agent_portal_otps, sends email.
  • Verify. Principal submits code. Server hashes, compares, issues 30-day HttpOnly sliding cookie.
  • Session. Row in agent_portal_sessions tracks device, IP, last seen. Revocable.

Invoice Pipeline.

For AP automation, agents can push invoices into a structured pipeline. The pipeline handles OCR, KYB checks, and eventual payout routing.

Invoice State Machine

StateAction required
draftWaiting for OCR / data extraction completion.
pending_reviewOCR confidence low, or vendor is unknown. Manual review needed.
pending_kybVendor exists but hasn't passed business verification. Payment blocked.
approvedInvoice clean, vendor verified. Ready for payment scheduling.
scheduledPayment date set. Will auto-execute.
paidFunds disbursed via selected rail.
rejectedTerminal state. Will not be paid.

KYB Blockers

An invoice cannot transition from pending_kyb to approved until the vendor's KYB status is clear. If an agent attempts to pay a vendor that fails KYB, the API returns a 403 and the mandate audit logs a KYB_FAILED error.

Compare AgentWallet