A proof-of-architecture prototype demonstrating the "Remote Control" agentic commerce model: an AI agent that can discover, negotiate, and execute a payment on behalf of a user — without ever touching the user's payment credentials.
Step 1: INTENT Step 2: NEGOTIATION Step 3: SIGNING Step 4: EXECUTION
User → Agent Agent → Merchant Agent → Device Agent → Merchant
"Book flight <$150" via UCP (mock) (WebSocket push) (with Signed Proof)
Agent gets INTENT Stages TX proposal User reviews + SCA Proof + DPC used
NOT credentials Agent never sees DPC Device signs TX Confirmation returned
Core security invariant: The agent never holds, sees, or touches the Digital Payment Credential (DPC) or private key — ever. The agent only holds a payment_instrument_id reference.
# Requires Node 20+ and pnpm
# 1. Install dependencies
pnpm install
# 2. Copy and set environment variables
cp .env.example .env
# Edit .env and add your ANTHROPIC_API_KEY
# 3. Start all three services (three terminal tabs recommended)
pnpm --filter merchant-mock start # port 3001
pnpm --filter agent start # port 3000 + WebSocket
pnpm --filter wallet-ui dev # port 5173Open http://localhost:5173 in your browser.
-
Open wallet-ui at
http://localhost:5173- Wallet initializes, generates ECDSA keypair, registers with agent
- Mock DPC card displayed: Chase ••••4242
-
In the intent input, type:
Book me a one-way flight NYC to LA under $150Select Chase ••••4242 from the payment picker. Click Submit.
-
Agent parses intent via Claude API → discovers merchant offers → selects cheapest → verifies merchant JWS signature → generates CheckoutMandate → sends signed push to wallet.
-
Transaction review screen appears showing:
- Your card section (DPC fields: issuer, masked number, holder)
- Payment details section (TransactionalData: payee, amount, currency, nonce, expiry)
-
Click Approve → wallet signs PaymentMandate → agent forwards both mandates to merchant → merchant verifies both independently → confirmation screen.
Alternate flows:
- Click Reject → state transitions to
REJECTED, clean abort, no signed artifacts - Click Counter-propose → adjust max price → agent re-negotiates with new terms, old TX invalidated
- Wait for TTL to elapse → wallet shows "Transaction expired", sign button disabled
┌─────────────────────────────────────────────────────────────┐
│ CLOUD TRUST ZONE │
│ │
│ ┌─────────────────────┐ HTTP ┌─────────────────────┐ │
│ │ Agent Service │◄──────────►│ Merchant Mock │ │
│ │ (port 3000) │ │ (port 3001) │ │
│ │ │ │ │ │
│ │ • Intent parsing │ │ • UCP endpoints │ │
│ │ • UCP negotiation │ │ • JWS signing │ │
│ │ • CheckoutMandate │ │ • Mandate verify │ │
│ │ • Push signing │ │ • Mock catalog │ │
│ └──────────┬──────────┘ └─────────────────────┘ │
└─────────────│───────────────────────────────────────────────┘
│ WebSocket (signed PushEnvelope)
┌─────────────▼───────────────────────────────────────────────┐
│ DEVICE TRUST ZONE │
│ │
│ ┌─────────────────────────────────────────────────────────┐│
│ │ Wallet UI (port 5173) ││
│ │ ││
│ │ • DPC storage (NEVER leaves this zone) ││
│ │ • Private key (non-extractable, SubtleCrypto) ││
│ │ • Push signature verification ││
│ │ • Transaction review UI ││
│ │ • PaymentMandate generation (device-side signing) ││
│ └─────────────────────────────────────────────────────────┘│
└─────────────────────────────────────────────────────────────┘
See docs/architecture.md for full detail.
Two distinct signed artifacts, verified by different parties:
| Mandate | Signed by | Verified by | Covers |
|---|---|---|---|
CheckoutMandate |
Agent (cloud) | Merchant | Hash of checkout terms |
PaymentMandate |
Wallet (device) | PSP via Merchant | Payment authorization |
These are intentionally separate — the merchant doesn't need to see payment credentials; the PSP doesn't need to see cart contents.
Once dev.ucp.shopping.ap2_mandate appears in the negotiated capability intersection, the session is security-locked. The agent cannot fall back to a simpler flow. If AP2 cannot be completed, the transaction must abort.
On startup, the wallet generates an ECDSA P-256 keypair and registers its public key with the agent (POST /agent/register-wallet-key). The agent caches this and forwards it to the merchant before any transaction begins. The merchant uses this key to verify PaymentMandate signatures at execute time.
packages/
├── shared/ # TypeScript types only — no runtime code
│ └── src/types/
│ ├── intent.ts # IntentManifest
│ ├── transaction.ts # TransactionalData
│ ├── mandates.ts # CheckoutMandate, PaymentMandate, SignedProof
│ ├── push.ts # PushEnvelope
│ ├── state.ts # AgentTransactionState + VALID_TRANSITIONS
│ └── ucp.ts # UCP protocol types
│
├── agent/ # Express + socket.io, port 3000
│ └── src/
│ ├── agent-server.ts # All routes + WebSocket
│ ├── crypto.ts # ECDSA via Node crypto.subtle
│ ├── intent-parser.ts # Claude API + regex fallback
│ ├── ucp-negotiator.ts # UCP discover/checkout/counter-propose
│ ├── jws-verify.ts # Merchant JWS signature verification
│ └── transaction-store.ts # State machine + AP2 lock
│
├── merchant-mock/ # Express, port 3001
│ └── src/
│ ├── server.ts # UCP endpoints + key management
│ ├── catalog.ts # Mock flight offers
│ └── crypto.ts # ECDSA + JWS generation
│
└── wallet-ui/ # Vite + React 18, port 5173
└── src/
├── App.tsx # Main app + WebSocket + flow logic
├── types/dpc.ts # DigitalPaymentCredential (ONLY here)
├── lib/
│ ├── crypto.ts # SubtleCrypto (browser-native)
│ ├── dpc-store.ts # Mock DPC store
│ ├── push-verifier.ts # Verify agent push signatures
│ ├── signed-proof.ts # Generate SignedProof
│ └── websocket.ts # Socket.io client
└── components/
├── IntentInput.tsx # NL input + payment picker
├── DPCCard.tsx # Card display (DPC fields only)
├── TransactionReview.tsx # Review + approve/reject/counter-propose
└── Confirmation.tsx # Result screen
Important:
DigitalPaymentCredentialis defined only inwallet-ui/src/types/dpc.ts. It is never imported byagentormerchant-mock. This is enforced at the TypeScript type level.
# Run all tests across all packages
pnpm test
# Run tests for a specific package
pnpm --filter agent test
pnpm --filter merchant-mock test
pnpm --filter wallet-ui test357 tests across 17 test suites — all passing.
Test coverage includes:
- Cryptographic operations (ECDSA keygen, sign, verify, key mismatch)
- Intent parsing (Claude API + fallback, field validation, DPC field security)
- JWS signing and verification (valid, tampered, wrong key, JWKS rotation)
- UCP negotiation (offer selection, merchant signature, expiry, counter-proposal)
- Mandate generation (CheckoutMandate shape, canonical payloads, signatures)
- State machine (all 16 states, all valid transitions, invalid transition rejection)
- AP2 lock invariant (immutability, dual-mandate enforcement)
- Nonce invalidation (permanent blocking, counter-proposal safety)
- Push verification (signature attacks, tamper detection, field validation)
- SignedProof generation (wallet signing, canonical format, verification)
ANTHROPIC_API_KEY=sk-ant-... # Required for intent parsing (falls back to regex if absent)
AGENT_PORT=3000 # Optional, defaults to 3000
MERCHANT_PORT=3001 # Optional, defaults to 3001
MERCHANT_URL=http://localhost:3001 # Optional, used by agent to reach merchant
| Component | Status | Notes |
|---|---|---|
| UCP protocol | Mocked | REST shape only — real UCP is Google's spec |
| Merchant catalog | Mocked | 3 hardcoded flight offers |
| Biometric SCA | Mocked | Button click simulates approval |
| Push notifications | Mocked | WebSocket instead of APNs/FCM |
| OpenID4VCI provisioning | Mocked | DPC pre-loaded in store |
| SD-JWT-VC encoding | Mocked | Plain JSON mandates |
| ECDSA signing | REAL | SubtleCrypto P-256, non-extractable keys |
| Mandate separation | REAL | Distinct CheckoutMandate + PaymentMandate objects |
| Merchant JWS verification | REAL | Agent verifies before presenting to user |
| Agent push signing | REAL | Wallet verifies before rendering UI |
| DPC/TransactionalData separation | REAL | Enforced in types + UI layout |
| Transaction TTL | REAL | Timestamp checks on agent + wallet |
| AP2 capability lock | REAL | State machine enforces, no fallback |
| Nonce replay protection | REAL | Checked by agent before execute |
| Counter-proposal invalidation | REAL | Old nonce permanently blocked |
See docs/security-model.md for the full attack surface analysis.
Core guarantee:
Agent knows: payment_instrument_id (reference), user intent, staged TX, CheckoutMandate
Agent never: private key, full PAN, DPC object, biometric data, PaymentMandate signing key
-
Agent Capability Declaration (ACD) — Agent presents a signed manifest to the merchant declaring: (a) acting on behalf of a verified user, (b) routing signing to a bound device, (c) cannot self-authorize, (d) AP2 support level.
-
IntentManifest formalization — Natural language is immediately structured into a typed
IntentManifestbefore any UCP interaction. Thepreferred_payment_instrument_idfield handles payment method selection formally, preventing agent assumptions. -
Counter-Proposal with nonce invalidation — Re-negotiation permanently invalidates the prior staged transaction and nonce. Neither party can use the old signed proof after re-negotiation.
-
AP2 Dual-Mandate Proof Structure — Explicit
CheckoutMandate+PaymentMandateseparation as specified in the AP2 spec (not just "a signed proof").
See docs/innovation-narrative.md for full detail.
docs/architecture.md— System architecture, component design, trust modeldocs/sequence-diagram.md— Full 4-step flow sequence diagramdocs/security-model.md— Attack surface analysis and mitigationsdocs/assumptions.md— Explicit assumptions logdocs/innovation-narrative.md— Innovation opportunities addresseddocs/ai-process.md— How AI was used in this project