The merchant-side trust gateway for AI shopping agents — one API that verifies any signed agent and tells merchants exactly what that verification means.
One install on the merchant side. One registration on the agent-issuer side. Four protocols supported out of the box — Visa Trusted Agent Protocol (the real wire format: agent-browser-auth/agent-payer-auth tags, Consumer Recognition Objects, Agentic Payment Containers, Visa-signed IdTokens), AVA's TAP-style profile (RFC 9421 + Ed25519 + x-ava-mandate), Google Agent Payments Protocol (AP2 v0.2 dSD-JWT mandate chains), and IETF Web Bot Auth (the signature scheme real ChatGPT agent traffic carries today). One protocol-agnostic VerificationResult shape downstream.
Web Bot Auth results are agent identity only — they prove which agent operator sent the request (e.g. https://chatgpt.com), not that a buyer authorized a purchase. Payment authority comes from the mandate-carrying protocols (Visa TAP, AP2).
If you're an AI agent issuer, the on-ramp is AGENT_ISSUERS.md — publish or register your public key once, and every AVA Pay merchant can verify you on any protocol you sign with.
npm install
npm run demo # AVA TAP-style profile (RFC 9421 + Ed25519 + x-ava-mandate)
npm run demo:ap2 # Google AP2 v0.2 (dSD-JWT mandate chains)
npm run demo:wba # IETF Web Bot Auth (identity-only, key-directory discovery)
npm run demo:tap # Visa Trusted Agent Protocol, real wire formatYou'll see a signed request and a ✓ trusted response in well under a millisecond, on every protocol. The same primitives the demo uses are published as @ava-pay/agent on npm — that's what production agents import.
AVA Pay/
├── packages/
│ └── agent-sdk/ # @ava-pay/agent — publishable npm package
│ ├── src/agent/{visa,ap2}.ts # signWithVisa, buildCheckoutMandateChain, …
│ ├── src/protocol/visa/ # RFC 9421 primitives (parser, base, Ed25519 verify)
│ ├── src/protocol/ap2/ # dSD-JWT chains, v0.2 mandates, JWS primitives
│ ├── src/types.ts # canonical shared types
│ └── src/index.ts # public exports
├── src/ # API server (depends on @ava-pay/agent)
│ ├── routes/verify.ts # POST /verify, schema-validated, structured logs
│ ├── verifier/
│ │ ├── interface.ts # AgentVerifier — the swap point
│ │ ├── multi.ts # MultiProtocolVerifier — header-sniffs and dispatches
│ │ ├── visa.ts # AVA TAP-style profile verifier (x-ava-mandate)
│ │ ├── visa-tap.ts # REAL Visa TAP verifier + Visa JWKS resolver
│ │ ├── ap2.ts # AP2 verifier
│ │ ├── web-bot-auth.ts # Web Bot Auth verifier + key-directory resolvers
│ │ ├── agent-directory.ts # Static/Remote + 5-min CachingAgentDirectory (kid/protocol-aware)
│ │ ├── federated-directory.ts # federated resolution chain across roots of trust
│ │ ├── mandate.ts # shared mandate scope check
│ │ └── mock.ts # non-crypto mock for plumbing tests
│ ├── directory/
│ │ ├── types.ts # DirectoryAgentRecord, DirectoryAgentKey
│ │ ├── storage.ts # InMemory + File backed stores
│ │ ├── routes.ts # /.well-known + /directory/agents (register, lookup, revoke)
│ │ └── storage-directory.ts # adapter — verifier resolves through hosted storage
│ └── server.ts # buildServer — composes verifier, directory, static landing
├── public/ # static landing page (live in-browser demo using Web Crypto)
├── examples/agent-demo.ts # runnable Node demo (all four protocols)
├── tests/ # 597 Vitest cases — real cryptography, no fakes
├── scripts/check-type-sync.ts # CI guardrail against API↔plugin type drift
├── shopify-app/ # Shopify Remix plugin (settings + traffic dashboard + theme extension)
├── AGENT_ISSUERS.md # how AI agent issuers register with the directory
└── Dockerfile + docker-compose.yml + .github/workflows/ci.yml
API POST /verify |
✅ Fastify + TypeScript + structured pino logs |
| Multi-protocol verifier | ✅ MultiProtocolVerifier sniffs the request and dispatches |
| Visa Trusted Agent Protocol (real wire format) | ✅ agent-browser-auth/agent-payer-auth tags, ed25519 + rsa-pss-sha256, 8-min window cap, nonce replay block, Consumer Recognition Object + Visa-signed PS256 IdToken (verified against mcp.visa.com/.well-known/jwks), Agentic Payment Container validation |
| AVA TAP-style profile | ✅ RFC 9421 + Ed25519, content-digest, mandatory alg/created/nonce, server-side max signature age, x-ava-mandate |
| Google AP2 (v0.2) | ✅ dSD-JWT delegation chains (RFC 9901 + draft-gco-oauth-delegate-sd-jwt): open→closed Checkout Mandate with checkout_jwt hash linkage, Payment Mandate with transaction_id linkage, pluggable constraint-evaluator registries (allowed_merchants, line_items, amount_range, allowed_payees, reference), aud + single-use-nonce binding; v0.1 Intent/Cart traffic gets typed unsupported_protocol_version |
| IETF Web Bot Auth | ✅ draft-ietf-webbotauth-httpsig-protocol-00 (formerly draft-meunier-webbotauth-httpsig-protocol/-directory): tag="web-bot-auth", Signature-Agent (both wire forms), RFC 7638 thumbprint keyids, allowlisted /.well-known/http-message-signatures-directory resolution, identity-only result (no mandate) |
| Replay protection | ✅ nonce + cart-jti dedup via ReplayGuard (in-memory now, Redis-ready interface) |
| Rate limiting | ✅ @fastify/rate-limit, RATE_LIMIT_MAX/min (default 300) per client IP; TRUST_PROXY_HOPS (default 1, Railway's edge) sets how many X-Forwarded-For hops are trusted, and /healthz echoes the derived clientIp so the setting can be checked from outside |
| Fail-closed directory writes | ✅ no DIRECTORY_REGISTRATION_TOKEN → writes disabled (opt-in dev flag AVA_ALLOW_OPEN_DIRECTORY_WRITES=1); constant-time token compare |
| Federated key resolution | ✅ chain: Visa Agentic Directory (when credentialed) → Visa public JWKS → Web Bot Auth agent cards → hosted/private allowlist; kid- + protocol- + algorithm-aware (replaced keys[0]); down roots skipped, revocations never shadowed |
| Agent Directory + caching | ✅ 5-min TTL keyed by (id, hints), caches misses, Remote/Static/StorageBacked impls |
| Hosted Agent Directory | ✅ /.well-known/ava-agent-directory, register/lookup/revoke, file-backed storage |
| Public agent SDK | ✅ @ava-pay/agent@0.2.0 — all four protocol signers + verifier primitives |
| Public landing page + live demo | ✅ public/ — Web Crypto Ed25519 in the browser, signs and verifies against a pre-seeded public demo agent |
| Agent issuer onboarding | ✅ AGENT_ISSUERS.md |
| Runnable demos | ✅ npm run demo, npm run demo:ap2, npm run demo:wba, npm run demo:tap |
| API tests | ✅ 597 with real cryptography (real TAP + AVA profile + AP2 + Web Bot Auth + federated resolution + directory + caching + replay/hardening) |
| Shopify plugin | ✅ OAuth, Polaris settings, traffic dashboard, App Proxy pass-through |
| Plugin tests | ✅ 15 |
| Dockerfile + compose with Redis | ✅ |
| CI (typecheck + tests + drift + docker build) | ✅ |
| Real Visa Agent Directory wiring | ⏳ partner-gated — VISA_AGENT_DIRECTORY_URL swaps in zero-code |
| Real Google AP2 attestation roots | ⏳ awaiting public spec finalization |
Redis-backed CachingAgentDirectory |
⏳ Redis service provisioned, swap pending |
Published @ava-pay/agent npm package |
✅ 0.2.0 live (published 2026-07-12) |
| Self-serve directory auth (DPoP) | ⏳ V1 uses bearer token; DPoP next |
npm install
npm run dev # http://localhost:3000 (logs JSON to stdout)Or in Docker:
docker compose up --build # API on :3000, Redis on :6379 (ready for the cache swap)npm test # 597 real-cryptography tests across all four protocols + directory
npm run typecheck # full TS strict check
npm run check:type-sync # API ↔ plugin VerificationFailureReason drift guard
npm run demo # in-process AVA TAP profile demo
npm run demo:tap # in-process real Visa TAP demo
npm run demo:ap2 # in-process AP2 v0.2 demo
npm run demo:wba # in-process Web Bot Auth demoWeb Bot Auth key discovery is allowlist-gated: the verifier only fetches
/.well-known/http-message-signatures-directory from origins in
WBA_ALLOWED_SIGNATURE_AGENTS (comma-separated https origins; defaults to
https://chatgpt.com, the only major agent operator publishing a live
directory as of July 2026).
When the API runs, it also exposes:
| Endpoint | Purpose |
|---|---|
GET /.well-known/ava-agent-directory |
Discovery — { service, version, count, generatedAt } |
GET /directory/agents |
Public list (revoked agents hidden) |
GET /directory/agents/:id |
Lookup |
POST /directory/agents |
Register / update — bearer-token auth (DIRECTORY_REGISTRATION_TOKEN) |
POST /directory/agents/:id/revoke |
Revoke — bearer-token auth |
Storage path is AVA_DIRECTORY_DATA (file-backed JSON, persists across restarts). Defaults to in-memory when unset.
Verifier key resolution is federated: keys resolve through a chain of roots of trust — Visa's Agentic Directory (when VISA_AGENT_DIRECTORY_URL is set) → Visa's public JWKS (mcp.visa.com) → the Web Bot Auth key directories of allowlisted signature agents → this hosted directory as the private-allowlist fallback. The first root that knows the key wins (a revocation in a higher root is definitive); a root that is down is skipped. Agents you register here are immediately resolvable by /verify — and an agent that already publishes a Web Bot Auth key card can sign Visa TAP requests with that same key (keyid = RFC 7638 thumbprint), no separate registration.
See AGENT_ISSUERS.md for the agent-side onboarding flow.
A verified result answers which agent signed this. It does not answer who is accountable for the
origin that agent claims to be. An OperatorSource answers the second question, separately, and it
can never change the first: annotateWithOperator only runs for trusted: true, attaches its answer
at result.operator, and treats a null, a throw or a missing source as identical no-ops.
WhisperOperatorSource (src/verifier/whisper-operator-source.ts) implements it against the Whisper
graph, which is public and needs no key. Each origin you pass to describe() is sent to
https://graph.whisper.online/api/query, an endpoint operated by Whisper Security, as a hostname and
its registrable parent. No request content, no buyer data and no signing-key material goes with it, and
nothing is sent at all until you call describe(). Switch it on by passing it as operator:
import { MultiProtocolVerifier } from './src/verifier/multi.js';
import { WhisperOperatorSource } from './src/verifier/whisper-operator-source.js';
const verifier = new MultiProtocolVerifier({
/* ...existing options... */
operator: new WhisperOperatorSource(), // keyless
// operator: new WhisperOperatorSource({ apiKey: process.env.WHISPER_API_KEY }),
});See docs/operator-sources/whisper.md for the record it returns,
what each field does and does not claim, and how to run it against the live graph.
See docs/RESOLVER-SOURCES.md for the contract an operator source owes
its caller.
The seam is src/verifier/interface.ts — AgentVerifier, one method. MultiProtocolVerifier (src/verifier/multi.ts) sniffs each incoming request's signature tag and dispatches: web-bot-auth → WebBotAuthVerifier, agent-browser-auth/agent-payer-auth → VisaTapVerifier (real Visa wire format), ap2-checkout-mandate header → Ap2AgentVerifier, untagged RFC 9421 → VisaAgentVerifier (AVA's TAP-style profile). Every verifier returns the same typed VerificationResult; none of them throw. Adding a protocol = one new implementation + a sniff rule. The full module map is in Repo layout above.
The Trusted Agent Protocol (TAP) is published by Visa. The on-the-wire format — RFC 9421 HTTP Message Signatures, Consumer Recognition Objects, Visa-signed IdTokens, Agentic Payment Containers — is what VisaTapVerifier already implements, validated against Visa's live public JWKS. What's partner-gated is access to Visa's hosted Agentic Directory (their authoritative source of agent public keys + revocation status).
To wire that in:
-
Apply for Visa Partners access at https://developer.visa.com (look for "Trusted Agent Protocol" / "AI Commerce"). You'll receive
VISA_API_KEYand theVISA_AGENT_DIRECTORY_URLfor your environment (sandbox vs prod). -
Set environment variables before starting the API:
export VISA_AGENT_DIRECTORY_URL=https://agent-directory.visa.com # provisioned per-tenant export VISA_API_KEY=... # your bearer token npm run dev
On boot,
buildServerdetectsVISA_AGENT_DIRECTORY_URLand switches the directory fromStaticAgentDirectory(dev) toCachingAgentDirectory(RemoteAgentDirectory(...), 5min)automatically. -
No code changes needed. Verification, mandate scoping, content-digest enforcement, and revocation handling all flow through the same
AgentVerifierinterface.
While you wait for partner access, dev mode pre-loads an empty StaticAgentDirectory. The test suite uses the same primitive, generating fresh Ed25519 keypairs at test boot — that's how every assertion in tests/verify.test.ts exercises real cryptography end-to-end.
The route emits an x-ava-verify-ms response header so we can monitor the <50 ms target end-to-end. The full Visa pipeline (parse + signature window + content-digest + cached directory hit + Ed25519 verify + mandate decode + scope check) runs in single-digit milliseconds locally; the only sub-50ms risk is a cold directory miss on first agent contact (Visa Agent Directory round-trip).
Lives in shopify-app/. Two-minute install:
cd shopify-app && cp .env.example .env— fill in Partners API keys +AVA_PAY_API_URL.npm install && npm run setup && npm run dev— Shopify CLI tunnels and opens the install on your dev store.- In your dev store theme editor: Online Store → Customize → App embeds → AVA Pay → ON.
Or, instead of step 3, paste this single line into theme.liquid:
<script async src="{{ shop.url }}/apps/ava-pay/embed.js"></script>Storefront pages now ping the App Proxy → AVA Pay /verify → mint a one-time discount code → redirect verified agents through /discount/AVA-XXXXXXXX. Merchant policy (toggle + max-cap) is configured in the embedded admin UI; full architecture diagram and dev walkthrough live in shopify-app/README.md.
MIT. See LICENSE. The published SDK, @ava-pay/agent, carries the same license.
A verifier nobody can read is a verifier nobody should trust, so the decision logic, the signature bases, and the test suite are all public. Read them before you rely on a verdict.
AVA Pay™ and AVA Ready™ are trademarks of Agentic Verification Architecture LLC. The license above covers the code, not the names.