A working example of AP2 (Agent Payments Protocol) spec v0.2, built on Truvera's tooling. A real LLM (the Shopping Agent — Ollama by default, free/local, or Anthropic; a fast, free scripted mode is also available for demos) drives a full mandate lifecycle by making genuine MCP tool calls against three servers:
wallet-server(two separate instances) — holds signing keys, issues Open/Closed Checkout and Payment Mandates. One instance is the User's own wallet (mints the User's identity, and signs the two mandate calls that authorize the Agent); the other is the Shopping Agent's own wallet (its own separate identity, plus the two calls it closes with its own key). These are deliberately never the same process or database — see "User vs. Agent wallets" below.truvera-api— the MCP server this app calls (via theissue_payment_tokentool) to reach the AP2 Credential Provider, which verifies the Closed Payment Mandate and issues a payment token.truvera-apiis itself a thin wrapper: it holds the real API key and makes the actual HTTPS call to Truvera's hosted Credential Provider backend — this app only ever sees an MCP tool call, never that key or endpoint directly (seedocs/getting-started.md§2).
Two roles outside the AP2 tool surface are mocked locally in this app:
a Merchant (issues the opaque Checkout JWT the mandates reference) and a
PSP (actually signs the Payment Receipt — truvera-api builds the
receipt content but doesn't sign it yet).
Every step's real artifacts — mandates, JWTs, hashes, verification results — are shown in the UI, not just a success checkmark, tagged with whether it's a real Truvera MCP tool call, a Truvera SDK used directly, or this demo's own mock logic.
New to AP2 or integrating it into your own app? See
docs/getting-started.md for a full developer
walkthrough (actors, mandate lifecycle, sequence diagram, security model,
and how to adapt this pattern elsewhere) — this README covers running this
specific demo.
- Docker, to run
wallet-server(two instances) andtruvera-api— published images:docknetwork/truvera-wallet-mcp,docknetwork/truvera-api-mcp. Seedocs/getting-started.md§7 for exactdocker runcommands and env vars. - A Truvera API key for
truvera-api— use the testnet endpoint (https://api-testnet.truvera.io) while developing, not production (https://api.truvera.io). - Either a running Ollama instance (default provider,
free/local — see
OLLAMA-INVESTIGATION-NOTES.mdfor which models are actually reliable enough for this flow and which caveats apply) or anANTHROPIC_API_KEY(setAGENT_PROVIDER=anthropicto use it). Neither is needed for Mock mode.
npm install
cp .env.example .env.local # see .env.example for every available knobTwo things need to be running: the MCP servers, and this app.
# Terminal 1 — the three MCP servers (see docs/getting-started.md §7 for
# exact docker run commands + env vars)
docker run ... docknetwork/truvera-wallet-mcp:<tag> # User's instance
docker run ... docknetwork/truvera-wallet-mcp:<tag> # Agent's instance
docker run ... docknetwork/truvera-api-mcp:<tag>
# Terminal 2
npm run dev # http://localhost:3002If you're actively developing wallet-server/truvera-api/the SDK themselves
(see the Prerequisites note above), npm run mcp:start (wraps
scripts/start-mcp-servers.sh; npm run mcp:stop to stop) builds and runs
them from the sibling truvera-mcp-server checkout instead of the step
above — that's how this specific repo is developed day to day, not the
integration path for a new app.
Pick something from the catalog, set a budget, choose Mock (instant, free, default) or Live LLM, and run it. The Shopping Agent drives the full mandate lifecycle — setting up the User's wallet identity, authorizing itself, checking out with the Merchant, checking your budget, and (if within it) closing the Payment Mandate and getting it tokenized and verified — and the Merchant/PSP mock signs and verifies the resulting Payment Receipt. Every step is shown live as it happens.
npm test # unit tests (pure logic, no live servers needed)
npm run mcp:start && npm run test:live # + integration tests against live MCP servers
npm run typecheck
npm run lintsrc/lib/mcp/ MCP client wiring (config, connections, tool-result parsing)
src/lib/merchant/ Merchant (checkout JWT) + PSP (payment receipt signing/verification)
src/lib/catalog/ the mock product catalog the shopping UI is built against
src/lib/presentation/ UI-facing presentation glue only, not agent logic: friendly
per-step copy + actor mapping (stepCopy.ts), JWT/SD-JWT
decoding for the "technical details" panel (decodeArtifacts.ts)
src/lib/agent/
shoppingAgent.ts the orchestrator: assembles tools, picks a provider, runs the loop
providers/ the 3 drivers (ollama, anthropic, mock) + shared loop machinery
(agentLoop.ts, stallGuards.ts, fewShotExample.ts)
flow/ the deterministic protocol sequence + safety net: flowSteps.ts
(step tracker), enforcement.ts (caller-side argument guards),
trace.ts (the audit-log data model, streamable via onEntry)
tools/ builds/adapts real MCP + custom tools into the shared
AgentTool contract (mcpToolAdapter.ts, merchantTool.ts,
protocolUtilsTool.ts)
src/app/ UI + the /api/run-demo route (streams NDJSON) tying it together
scripts/ local dev scripts for wallet-server/truvera-api (not docker-compose --
see the scripts' header comments for why)
The Shopping Agent's one real decision point: it compares the actual cart total against the User's stated budget itself, and refuses to close the Payment Mandate if it's over — not a scripted pipeline that always proceeds.
The User and the Shopping Agent each get their own, separate
wallet-server instance (separate process, separate database) — never the
same one. This matters because wallet-server custodies every private key
it generates and signs on behalf of whatever keyId a caller supplies, with
no proof-of-possession or session-to-key binding. If the User's and Agent's
identities were minted against the same instance, the Agent's own code
could self-issue a mandate under a keyId it invented and call it "the
User's" — the delegation this app teaches would be a UI narrative, not
something actually enforced. With two separate instances, the Agent's own
keyId simply doesn't exist in the User's wallet's database, so using it
there fails at the crypto/lookup layer.
Concretely: issue_open_checkout_mandate and issue_open_payment_mandate
(signed with the User's key, naming the Agent as the mandate's closer) are
only ever called against the User's own wallet connection
(USER_WALLET_TOOL_ALLOWLIST in shoppingAgent.ts). Everything else the
Agent does with its own key — its own identity, issue_closed_checkout_mandate,
issue_closed_payment_mandate — stays on the Agent's own, separate
connection (AGENT_WALLET_TOOL_ALLOWLIST).
wallet-server also ships MCP_AUTH_MODE=jwt, which gets the same
per-tenant isolation within a single process via per-tenant database paths.
This app deliberately uses two plain processes instead: standing up JWT
issuance (keypair generation, a working signing path) is real net-new
infrastructure for local dev/demo purposes, where a second process needs
none.
AP2 v0.2 as implemented here (checked against the actual schemas and the
full wallet-server/truvera-api tool surface) never calls back to the
User after the two Open Mandates are signed — "human-not-present" means the
budget cap is the authorization, with no further check-in required by the
protocol. This app adds one step beyond that anyway (the final
deliver_receipt step in the sequence diagram, stepCopy.ts), and delivers
it regardless of outcome:
- If the purchase went through: the signed, verified Payment Receipt
(
src/lib/merchant/paymentReceipt.ts). - If the Agent stopped (currently: over budget) before ever reaching the
Credential Provider: an Error-status Checkout Receipt instead
(
src/lib/merchant/checkoutReceipt.ts) — the SDK's own separatecheckout_receipt.json/buildCheckoutReceiptfamily, referencing the Closed Checkout Mandate that DOES exist at that point (the checkout was locked in even though payment never was). A Payment Receipt wouldn't fit here: itsreferencefield expects a closed payment mandate to point to, and none exists when the Agent stops before authorizing payment.
Neither is a real wallet-server/truvera-api tool call — both are this app's own addition, not a protocol call. The reasoning: you pre-authorized real spending; you should see the real outcome either way, not just grant the authorization and hear nothing back when nothing was bought.
For the full plan, decisions made, and researched tool schemas this app is
built against, see AP2-EXAMPLE-APP-HANDOFF.md in the truvera-mcp-server
repo.