Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 23 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
<img alt="MCP" src="https://img.shields.io/badge/MCP-supported-6b4fbb">
<img alt="x402" src="https://img.shields.io/badge/x402-supported-0052ff">
<img alt="A2A" src="https://img.shields.io/badge/A2A-experimental-f0a30a">
<img alt="ACP" src="https://img.shields.io/badge/ACP-experimental-f0a30a">
</p>

## What it is, in ten seconds
Expand All @@ -21,19 +22,18 @@ You already have an HTTP API. AI agents want to **discover** it, **call** it and
Agent Commerce Gateway sits in front of your existing API, in **your**
infrastructure, and does that for you. You describe an endpoint in a YAML file -
or generate that description from an OpenAPI document you already have - and
agents get an MCP tool and an x402 paywall. The money goes straight to your
wallet - the gateway never holds it, and never holds your keys.
agents get an MCP tool and an x402 paywall. Switch on the experimental adapters
and the same resource is also an A2A skill, or an ACP checkout session. The
money goes straight to your wallet - the gateway never holds it, and never holds
your keys.

```text
Your existing API → Agent Commerce Gateway → AI Agent
MCP · x402 · receipts · doctor
MCP · A2A · ACP · x402 · receipts · doctor
```

## Demo

<!-- TODO(release): 15-30s GIF - left: buyer agent terminal, right: dashboard,
overlay: on-chain settlement. Replace this block before tagging. -->

```text
[agent] Discovering resources over MCP...
[agent] Found: market_report - Premium Market Report (0.01 USDC)
Expand Down Expand Up @@ -232,13 +232,17 @@ See [docs/configuration.md](docs/configuration.md).
| **x402** | Supported | x402 v2 (`@x402/core`, `@x402/evm`), scheme `exact`, EVM |
| **HTTP** | Supported | native routes |
| **A2A** | Experimental | A2A v1.0.0, binding `JSONRPC`, method `SendMessage` |
| UCP | Planned | - |
| ACP · MPP · AP2 | Planned | - |
| **ACP** | Experimental | ACP `2026-04-17`, REST checkout + discovery |
| UCP · MPP · AP2 | Planned | - |

"Planned" means **no code ships for it**. "Experimental" means the code ships,
is tested against the official SDK, and serves a narrow named subset - A2A is
off by default and documented in full at
[docs/protocols.md](docs/protocols.md#a2a). Each adapter reports its own
is tested against the protocol's own official artifacts, and serves a narrow
named subset - A2A and ACP are both off by default and documented in full at
[docs/protocols.md](docs/protocols.md#a2a) and
[docs/protocols.md](docs/protocols.md#acp). ACP serves the five stable checkout
operations and advertises `services: ["checkout"]` and nothing more; its
`payment_data` stays with the merchant's own checkout and is never turned into
an x402 payment. Each adapter reports its own
`supportedSpec`, `capabilities` and `unsupported` list at runtime via
`GET /.well-known/agent-commerce` and `agent-commerce doctor` - so the claim is
checkable, not marketing. Detail: [docs/protocols.md](docs/protocols.md).
Expand Down Expand Up @@ -371,8 +375,9 @@ $ npm run agent-commerce -- doctor --config config-demo.yaml
PASS Config valid - 2 resource(s), merchant "Demo Data Store" (using local chain manifest .deploy/local.json for X402_ASSET, X402_ASSET_NAME, X402_ASSET_VERSION, X402_ASSET_DECIMALS, MERCHANT_WALLET, X402_FACILITATOR_PRIVATE_KEY)
PASS Gateway healthy and ready at http://127.0.0.1:8080
PASS Backend 2/2 backend host(s) reachable
PASS Protocols http=on mcp=on (/mcp) a2a=off
PASS Protocols http=on mcp=on (/mcp) a2a=off acp=off
INFO A2A disabled
INFO ACP disabled
PASS Payments x402 v2 (scheme=exact) enabled - LOCAL dev chain (eip155:84532, chain id shared with Base Sepolia), destination=0x7099…79C8, facilitator=local
INFO Payments (MPP) planned - not implemented in this release
PASS Storage sqlite schema v1 writable; receipts=2
Expand Down Expand Up @@ -418,13 +423,13 @@ See [CONTRIBUTING.md](CONTRIBUTING.md).

## Roadmap

**Now (v1.2.0)** - MCP, x402 v2, settlement on the local chain, Base Sepolia
and Base mainnet, receipts, doctor, deterministic demo, an experimental
A2A v1.0.0 adapter, and experimental OpenAPI import.
**Now** - MCP, x402 v2, settlement on the local chain, Base Sepolia and Base
mainnet, receipts, doctor, deterministic demo, experimental A2A v1.0.0 and ACP
`2026-04-17` checkout adapters, and experimental OpenAPI import.

**Next** - a stronger conformance suite · a `doctor` GitHub Action · UCP ·
MPP · ACP · AP2 · Shopify and WooCommerce examples · PostgreSQL · richer
observability · multi-file and remote OpenAPI sources.
**Next** - a `doctor` GitHub Action · UCP · MPP · AP2 · more of ACP (carts,
feed, delegated payment) · Shopify and WooCommerce examples · PostgreSQL ·
richer observability · multi-file and remote OpenAPI sources.

New protocols land only after the adapter model survives real use. Scope
discipline is a release requirement, not a mood.
Expand Down
8 changes: 8 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,14 @@ customer, not an intruder, so `POST /api/resources/:id/invoke`, `/mcp`,
`GET /api/resources`, `GET /health` and `GET /.well-known/agent-commerce` are
open. Paid resources are protected by payment, not by authentication.

**ACP is the exception among agent routes.** When `protocols.acp` is enabled,
every checkout route under its mount requires
`Authorization: Bearer <protocols.acp.auth.token>`, compared in constant time;
ACP's own discovery document at `/.well-known/acp.json` stays public and
contains no configuration at all. Request `Signature` verification is not
implemented and a `Signature` header never substitutes for the bearer token, so
ACP must be deployed behind TLS. See [docs/security.md](docs/security.md#acp).

**The operator routes are different.** `GET /api/receipts`, `GET /api/events`
and `GET /api/events/stream` expose the merchant's commerce ledger — payer and
payee addresses, amounts, settlement transaction hashes, resource ids and
Expand Down
6 changes: 2 additions & 4 deletions biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,8 @@
"!contracts/out",
"!contracts/lib",
"!package-lock.json",
"!.claude",
"!**/.claude",
"!graphify-out",
"!**/graphify-out"
"!src/protocols/acp/spec",
"!tests/fixtures/acp"
]
},
"formatter": {
Expand Down
13 changes: 13 additions & 0 deletions config.example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,19 @@ protocols:
a2a:
enabled: false
mountPath: /a2a
# ACP (Agentic Commerce Protocol), stable snapshot 2026-04-17 — experimental,
# off by default. Enabling it serves the five checkout routes under mountPath
# and the specification-fixed discovery document at /.well-known/acp.json.
# Clients must send `Authorization: Bearer …`, `API-Version: 2026-04-17`, and
# an `Idempotency-Key` on every POST. Only the `checkout` service is served:
# carts, feed, orders, delegated payment and authentication, webhooks and the
# ACP MCP binding are not implemented.
#
# Enabling it needs the five mapped resources too - a complete, validating
# config is in examples/acp-checkout/. See docs/protocols.md#acp.
acp:
enabled: false
mountPath: /acp

resources:
# --- free resource: proves the gateway fronts an existing API -------------
Expand Down
85 changes: 81 additions & 4 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,80 @@ shape can never settle a payment and then fail to reach the backend.
Generated by [`agent-commerce import openapi`](openapi-import.md); nothing
about the field is OpenAPI-specific.

## `protocols.acp`

Experimental, off by default, and absent from a config that predates it. The
block is optional; when `enabled` is `true` the rest of it is required.

```yaml
protocols:
acp:
enabled: true
mountPath: /acp # default
auth:
type: bearer # the only scheme in this release
token: ${ACP_BEARER_TOKEN}
idempotency:
path: ./data/acp-idempotency.sqlite
retentionHours: 24 # default, and the floor
checkout:
operations: # all five, each on its own resource
createCheckoutSession: acp_checkout_create
updateCheckoutSession: acp_checkout_update
getCheckoutSession: acp_checkout_get
completeCheckoutSession: acp_checkout_complete
cancelCheckoutSession: acp_checkout_cancel
discovery: # optional; omitted from discovery when absent
documentationUrl: https://merchant.example.com/docs/acp
supportedCurrencies: [usd]
supportedLocales: [en-US]
interventionTypes: [3ds]
```

Every mapped resource must exist, carry `expose: [acp]`, and be **free** with no
`payments` - ACP checkout carries the merchant's own purchase payment, and
charging to invoke the operation as well would put two unrelated payment layers
on one call. Each resource serves exactly one operation; a resource mapped twice
is refused.

The canonical envelope a resource receives is fixed per operation - `path`
carrying `{ checkout_session_id }`, `body` carrying the ACP document - so bind
it explicitly:

```yaml
input:
type: object
properties:
path:
type: object
properties:
checkout_session_id: { type: string }
required: [checkout_session_id]
additionalProperties: false
body: { type: object }
required: [path]
additionalProperties: false
backend:
type: http
method: POST
url: ${MERCHANT_API_BASE_URL}/checkout_sessions/{checkout_session_id}
inputBindings:
path: path
body: body
```

Config checks that shape at load: a resource whose schema forbids a key the
adapter always sends, or requires one the operation never sends, is rejected
before it can fail at request time. A complete, validating configuration is in
[examples/acp-checkout](../examples/acp-checkout).

`retentionHours` may not go below 24: a shorter window would let a replayed
`Idempotency-Key` past an expired record and run a checkout twice. The
idempotency database is its own file - it never shares a table with receipts or
the x402 replay defence.

See [protocols.md](protocols.md#acp) for the wire contract.

## Payments

```yaml
Expand Down Expand Up @@ -265,10 +339,13 @@ told you is ignored.
unsupported `version` · unresolved `${VAR}` · duplicate resource ids ·
`pricing.type: dynamic` · a paid resource with no `payments` · a resource naming
an unconfigured or disabled payment method · `expose` values outside
`[http, mcp, a2a]` (UCP is planned, not supported) · `expose: [mcp]` while
`protocols.mcp.enabled` is false (likewise `a2a`) · two enabled protocol mounts
that overlap · a mount that claims a route the gateway already serves,
including the A2A Agent Card path · an invalid or zero `payTo`/`asset`.
`[http, mcp, a2a, acp]` (UCP is planned, not supported) · `expose: [mcp]` while
`protocols.mcp.enabled` is false (likewise `a2a` and `acp`) · two enabled
protocol mounts that overlap · a mount that claims a route the gateway already
serves, including the A2A Agent Card and ACP discovery paths · an ACP checkout
mapping that is incomplete, names a missing or non-`acp` resource, reuses one
resource for two operations, or maps a paid one · an ACP idempotency retention
below 24 hours · an invalid or zero `payTo`/`asset`.

It exits non-zero on any of them.

Expand Down
2 changes: 2 additions & 0 deletions docs/contracts.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ the generated file is right and this table is stale.
- **Removed from the wire:** `/.well-known/agent-commerce` no longer publishes `payments.x402.facilitator.url`. A facilitator endpoint can carry a tenant path or an API key, exactly like `rpcUrl`, which the same route already withholds. It gained `payments.x402.mode` (`local` | `testnet` | `mainnet`) instead - chain id 84532 belongs to both the local dev chain and public Base Sepolia, so the network id alone cannot say which one a client is talking to.
- **Additive:** optional `BackendHandler.inputBindings` (`{ path?, query?, body? }`), naming the top-level input properties that carry each part of the backend request. *Use case:* `POST /users/{userId}/orders?notify=true` with a JSON body - path, query and body at once - which the leftover rule cannot express, because on a body-capable method everything not consumed by the URL template becomes the body. *Alternative considered:* infer the split from the input schema's property names; rejected, since the shape a merchant's backend expects is operator configuration, not something to guess from a schema, and guessing wrong on a paid resource is payment-without-delivery. *Compatibility:* absent means the legacy mapping, byte-for-byte; no consumer changes. When present, only named groups are forwarded - unmapped top-level input never reaches the backend. `validateBackendRequestShape` resolves both modes through the same function, so every shape error (missing/invalid path parameter, non-object group, query collision with the configured URL) is still an `INPUT_INVALID` raised before pricing.
- **Additive:** `PROTOCOL_NAMES`, the `ProtocolName` values as a runtime array. *Use case:* config validation and the OpenAPI importer's `--expose` both have to check a protocol name at runtime, and config was carrying its own hardcoded `new Set(['http','mcp','a2a'])`. *Alternative considered:* deriving `ProtocolName` from the array instead; rejected because it makes the surface printer expand the type into a literal union at every use site, turning a no-op into a noisy contract diff. *Compatibility:* additive value export, typed `readonly ProtocolName[]` so an unsupported name cannot enter it. No consumer changes.
- **Additive:** `ProtocolName` gains `'acp'` (experimental); config gains `protocols.acp` (disabled by default, mount `/acp`) and accepts `expose: [acp]`. *Use case:* the ACP checkout adapter. *Shape:* unlike `mcp`/`a2a`, the normalised `protocols.acp` is discriminated on `enabled` - an enabled block carries `auth`, `idempotency` and all five `checkout.operations` mappings, so the adapter needs no optional-field assertions and a half-configured checkout lifecycle is refused at load rather than advertised through ACP discovery. *Compatibility:* additive union member; a config with no `protocols.acp` block parses unchanged.
- **Additive (main entry):** `createAcpAdapter`, `AcpAdapterOptions`, `ACP_SPEC_VERSION`, `ACP_API_VERSION`, `ACP_WELL_KNOWN_PATH`. *Use case:* a consumer running `createGateway` needs the adapter to mount. *Why the main entry and not a subpath:* a subpath is a peer-dependency boundary, not a category - the ACP adapter needs no peer, only `ajv`/`ajv-formats` (real dependencies) and its own vendored schema. *Cost:* the pinned schema is inlined into `dist/index.js` (+~124 kB; package 396 kB -> 479 kB). The CLI bundle is unaffected - `doctor` reads only the ACP constants and descriptor, never the validator.
---

# Integration contract - exact factory signatures
Expand Down
Loading
Loading