Skip to content

Repository files navigation

AP2 Example App

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 the issue_payment_token tool) to reach the AP2 Credential Provider, which verifies the Closed Payment Mandate and issues a payment token. truvera-api is 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 (see docs/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.

Prerequisites

  • Docker, to run wallet-server (two instances) and truvera-api — published images: docknetwork/truvera-wallet-mcp, docknetwork/truvera-api-mcp. See docs/getting-started.md §7 for exact docker run commands 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.md for which models are actually reliable enough for this flow and which caveats apply) or an ANTHROPIC_API_KEY (set AGENT_PROVIDER=anthropic to use it). Neither is needed for Mock mode.

Setup

npm install
cp .env.example .env.local   # see .env.example for every available knob

Running

Two 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:3002

If 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.

Testing

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 lint

Architecture

src/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.

User vs. Agent wallets

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.

One addition on top of the spec: a receipt always comes back to you

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 separate checkout_receipt.json/buildCheckoutReceipt family, 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: its reference field 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.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

Packages

Used by

Contributors

Languages