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.
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:
{"mcpServers": {"agentwallet": {"url": "https://api.agentwallet.ai/mcp/agent","headers": {"Authorization": "Bearer pak_live_a1f9c2…"}}}}Tool call shape
// 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
| Tool | What it does | Limit-gated? |
|---|---|---|
| wallet.status | Computed balance + holds + headroom | — |
| wallet.send_payment | Initiate fiat payout via available rail | yes |
| wallet.send_usdc | USDC transfer on Base via CDP | yes |
| wallet.x402_pay | Sign EIP-3009 auth for x402 endpoint | yes |
| wallet.list_activity | Recent transactions across sources | — |
| wallet.request_approval | Surface pending action to Principal widget | — |
| card.details | Last4 + balance + status (no PAN) | — |
| card.recent_authorizations | Card-level transactions only | — |
| comms.send_sms | Outbound SMS via telephony provider | — |
| comms.send_email | Outbound email via email provider | — |
| comms.list_inbox | Unread inbound messages | — |
| identity.who_am_i | Agent metadata + AP2 public key | — |
| identity.sign_mandate | ES256 sign an AP2 IntentMandate | — |
| identity.discover | Resolve another agent via .well-known | — |
| approvals.poll | Check 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"
CartMandate
Specific items + merchant + total + intent_id
PaymentMandate
Rail + recipient + cart_id + final amount
JWS structure
{"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
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
issvia/.well-known/agent.jsonon the agent's domain to fetch the published JWK. - Verify signature.
jwtVerify(token, jwk)with strictalg: '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_auditwithaction='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.
// 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.
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.
{"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
Three-cap policy
Every agent has three native caps built into agent_identities. They are checked atomically against the ledger.
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.
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
| Endpoint | What it does |
|---|---|
| GET /api/agent-portal/agents | List all agents scoped to Principal's client |
| POST /api/agent-portal/agents | Provision a new agent wallet |
| GET /api/agent-portal/agents/:id/wallet/transactions | Unified activity feed |
| POST /api/agent-portal/agents/:id/wallet/credit | Add funds from master balance |
| GET /api/agent-portal/agents/:id/card | Card metadata (no PAN) |
| POST /api/agent-portal/agents/:id/card/reveal | Get short-lived VGS token to show PAN in iframe |
| GET /api/agent-portal/team | List Principals in this client |
| POST /api/agent-portal/auth/magic-link | Request 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
HttpOnlysliding cookie. - Session. Row in
agent_portal_sessionstracks 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
| State | Action required |
|---|---|
| draft | Waiting for OCR / data extraction completion. |
| pending_review | OCR confidence low, or vendor is unknown. Manual review needed. |
| pending_kyb | Vendor exists but hasn't passed business verification. Payment blocked. |
| approved | Invoice clean, vendor verified. Ready for payment scheduling. |
| scheduled | Payment date set. Will auto-execute. |
| paid | Funds disbursed via selected rail. |
| rejected | Terminal 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.