Architecture
ProveTheBuy has one job at order time (decide whether an agent's order is provably authorised) and one job at dispute time (prove it). Everything runs inside the merchant's own server; the hosted cloud, when it arrives, only stores and automates.
flowchart TB
subgraph Agent side
U[Shopper] -->|signs mandate| W[Wallet or issuer]
U --> AG[AI agent]
W --> AG
end
AG -->|"HTTP request<br/>Signature-Input + Signature<br/>body: order, mandate, payment"| MW
subgraph Merchant server
MW["Middleware<br/>capture raw bytes"] --> V[Verifier]
V --> KD[Key directory<br/>cached JWKS]
V --> RS[Replay store<br/>memory or Redis]
V --> MV[Mandate verifiers<br/>Verifiable Intent, AP2]
V --> PT[Payment token limits]
V --> PO[Policy]
PO --> D{Decision}
D --> L[(Evidence ledger<br/>hash chain)]
D --> CO[Your checkout handler]
end
KD -.->|HTTPS| VISA[Visa / Mastercard<br/>key directories]
L -.->|later| EP[Evidence pack builder]
DSP[Dispute webhook] -.-> EP
Components
| Component | Source | What it does |
|---|---|---|
| Middleware | packages/verify/src/express.ts |
Keeps the request body byte-for-byte as signed, rebuilds the public URL behind proxies, attaches req.provethebuy, and returns 403 / 409 only in enforce mode. |
| HTTP signatures | src/httpsig.ts |
RFC 9421 parsing, signature base, and Ed25519 / RSA-PSS / ECDSA verification. No custom crypto: everything goes through Node's WebCrypto. |
| Key directory | src/keys.ts |
Loads JWKS from URLs or inline, caches them, and records a SHA-256 snapshot of the exact key used, so the record can be re-verified after rotation. |
| Replay store | src/replay.ts |
Remembers each nonce for the signature's lifetime. In memory for one server; Redis (SET NX EX) for several. |
| Mandate verifiers | src/mandates/ |
Mastercard Verifiable Intent (3-layer SD-JWT) and AP2 cart and intent mandates. Each returns its own checks and a constraint result. |
| Body objects | src/bodyObjects.ts |
Visa TAP consumer-recognition and payment-container objects, bound to the header signature by nonce and key. |
| Payment limits | src/payment.ts |
Stripe shared payment token / ACP allowance: amount, currency, merchant, expiry. |
| Order hash | src/order.ts |
RFC 8785 canonical JSON, then SHA-256, base64url. The value that ties a mandate to one exact cart. |
| Policy | src/policy.ts |
Turns checks into allow, step_up or block. Versioned by content hash, so every record says which policy decided it. |
| Ledger | src/ledger.ts |
Append-only records, each hashing the previous one, with a Merkle root for anchoring. FileLedger writes JSON Lines. |
| Evidence | src/evidence.ts |
Re-verifies a stored record against its key snapshot and writes the evidence pack for a given reason code. |
| Reputation | src/reputation.ts |
Per-agent order, mismatch and dispute counts from the merchant's own ledger. |
Data model
The ledger holds two record types, verification and dispute, in one chain.
{
"type": "verification",
"record_id": "rec_o4Vx8KrZkA0OGlwm2FaNYV",
"merchant": "acct_northwind_coffee",
"received_at": "2026-10-13T06:56:12.000Z",
"request": { "method": "POST", "authority": "shop.northwind.example", "path": "/checkout", "raw_sha256": "…" },
"agent": {
"protocol": "visa-tap", "keyid": "shopassist-k1", "tag": "agent-payer-auth",
"directory": "visa", "created": 1791874563, "keySnapshotSha256": "…"
},
"checks": [ { "name": "signature_valid", "status": "pass", "detail": "ed25519 signature verified over @authority, @path." } ],
"mandate": { "type": "verifiable-intent", "mode": "autonomous", "valid": true, "constraintsOk": true },
"order": { "order_hash": "mLrSoxZ1…", "order_id": "ord_5u1lw8", "amount": 3850, "currency": "usd", "payment_intent": "pi_b4jc0pcf" },
"policy_version": "v1",
"decision": "allow",
"enforced": false,
"reasons": ["Verified agent, valid mandate, order inside its limits."],
"artifacts": { "request": { /* raw signed request */ }, "key_jwk": { /* public key snapshot */ }, "context": { /* order, mandate, payment */ } },
"prev_hash": "…",
"hash": "…"
}
hash is SHA-256 over the canonical JSON of the record without hash, so editing any field, or removing a record, breaks every hash after it. provethebuy ledger verify and the playground's sidebar both check this.
Where it runs
| Piece | Runs in | Notes |
|---|---|---|
| Verifier, ledger, evidence | Your server | Node 18.17+. Signature checks are local once keys are cached, so they add a few milliseconds. |
| Key directories | Visa, Mastercard | Fetched over HTTPS and cached (default 10 minutes). |
| Cloud (planned) | Hosted | Receives ledger records asynchronously. It can never slow down or block a checkout. |