Skip to main content
Glama
README.md
<h1 align="center">Agent Commerce Gateway</h1>

<p align="center">
  <strong>One backend. Multiple agent-commerce protocols. Self-hosted and non-custodial.</strong>
</p>

<p align="center">
  <a href="#quickstart"><img alt="Quickstart" src="https://img.shields.io/badge/quickstart-5%20minutes-2ea44f"></a>
  <a href="LICENSE"><img alt="License" src="https://img.shields.io/badge/license-Apache--2.0-blue"></a>
  <img alt="TypeScript" src="https://img.shields.io/badge/TypeScript-strict-3178c6">
  <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="MPP" src="https://img.shields.io/badge/MPP-experimental-f0a30a">
  <img alt="A2A" src="https://img.shields.io/badge/A2A-experimental-f0a30a">
  <img alt="ACP" src="https://img.shields.io/badge/ACP-experimental-f0a30a">
  <img alt="AP2" src="https://img.shields.io/badge/AP2-experimental-f0a30a">
</p>

## What it is, in ten seconds

You already have an HTTP API. Agent Commerce Gateway lets AI agents discover,
call and pay for it without adding those protocols to your backend.

Run the gateway in your infrastructure and describe each endpoint in YAML, or
generate draft resource definitions from OpenAPI. Resources can be exposed over
HTTP, MCP or experimental A2A; dedicated resources can implement experimental
ACP checkout operations. Paid resources use x402 or experimental MPP and can
also require an AP2 mandate proving that the buyer approved the purchase. Funds
go directly to the merchant wallet, and the gateway needs neither the buyer's
nor the merchant's private key.

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

## Demo

```text
[agent] Agent Commerce Gateway: deterministic buyer demo
[buyer] balance before  100 MockUSDC
[agent] merchant balance before 0 MockUSDC
[gateway] connected to http://localhost:8080/mcp
[agent] discovered 2 tool(s): weather_basic, market_report
[agent] paid tool: "market_report", free tool: "weather_basic"
[gateway] 402 payment required: 0.01 USDC to 0x7099…79C8
[buyer] challenge checked before signing: pays 10000 units of 0x5fbd…0aa3 to 0x7099…79C8
[buyer] signed x402 payment proof (LOCAL DEVELOPMENT ONLY - DO NOT FUND)
[gateway] delivered "market_report"
[receipt] id=receipt_484b…1eba requestId=mcp_4292…c930 status=settled settlementTx=0x0b19…1c7f
[buyer] balance after   99.99 MockUSDC  (Δ -0.01)
[agent] merchant balance after  0.01 MockUSDC  (Δ 0.01)
[agent] on-chain balance changes match the expected payment amount
[gateway] delivered free tool "weather_basic": {"city":"Paris","temperatureC":-4,…}
[agent] demo complete: free and paid delivery both verified
```

This excerpt of `npm run demo:agent` omits the address lines and shortens long
values. Balances, IDs and hashes differ from run to run.

The dashboard at <http://localhost:5173> polls the gateway's events route and
shows the same request within a few seconds.

## Install

```bash
npx @devlab.group/agent-commerce --help # no install needed
npm install -g @devlab.group/agent-commerce # or install the `agent-commerce` binary
agent-commerce doctor
```

Requires **Node >= 22**. The package includes the `agent-commerce` CLI
(`init`, `validate`, `start`, `doctor`, `reconcile`, `resolve`, `demo`,
`import openapi`, `version`) and a library for embedding the gateway.
The default install excludes optional protocol, payment and authorization
peers. The OpenAPI parser is a regular dependency because import is part of the
CLI.

```bash
agent-commerce start --config config.yaml --env-file .env
```

`start` runs the configured gateway in the foreground until SIGTERM or SIGINT.
Install the optional peers for the features you enable, listed below. Missing
startup imports stop the gateway with an error naming the package; missing
Solana, NEAR or CDP peers fail provider health checks or payment requests.
`start`, `validate`, `doctor`, `reconcile` and `resolve` accept `--env-file <path>` to
load environment variables before reading the config. Variables already set in
the environment take precedence. To embed the gateway in your own process, use
the library:

```ts
import { createGateway, loadConfig, receipts } from '@devlab.group/agent-commerce';

const config = await loadConfig({ path: 'config.yaml' });
const store = receipts({ path: './receipts.sqlite' });
// Initialize before use so an incompatible SQLite schema fails at startup
await store.init();

const gateway = await createGateway({
  config,
  store,
  paymentProviders: [],
  protocolAdapters: [],
});
const { url } = await gateway.listen();
```

### Optional peers

Components with extra dependencies use separate subpaths, so a gateway serving
free HTTP resources does not install MCP, EVM, JOSE or SD-JWT packages.

| You want                                | Install                              | Import                                     |
| --------------------------------------- | ------------------------------------ | ------------------------------------------ |
| gateway, config, receipts, A2A/ACP, CLI | `@devlab.group/agent-commerce`       | `from '@devlab.group/agent-commerce'`      |
| expose resources as MCP tools           | `+ @modelcontextprotocol/server`     | `from '@devlab.group/agent-commerce/mcp'`  |
| accept x402 payments                    | `+ @x402/core @x402/evm viem`        | `from '@devlab.group/agent-commerce/x402'` |
| accept x402 on Solana                   | x402 peers `+ @x402/svm @solana/kit` | (no import - loaded for a Solana offer)    |
| accept x402 on NEAR                     | x402 peers `+ @x402/near`            | (no import - loaded for a NEAR offer)      |
| accept MPP payments                     | `+ mppx viem @x402/core @x402/evm`   | `from '@devlab.group/agent-commerce/mpp'`  |
| verify AP2 mandates, sign checkout JWTs | `+ jose @sd-jwt/core canonicalize`   | `from '@devlab.group/agent-commerce/ap2'`  |

The x402 entry needs `@x402/core`, `@x402/evm` and `viem` even when every
offer is on Solana or NEAR. MPP settles through an x402 facilitator, so its
entry also needs the x402 peers.

```bash
npm install @devlab.group/agent-commerce @modelcontextprotocol/server @x402/core @x402/evm viem
```

```ts
import { mcp } from '@devlab.group/agent-commerce/mcp';
import { x402 } from '@devlab.group/agent-commerce/x402';
import { ap2 } from '@devlab.group/agent-commerce/ap2';
```

AP2 peers remain optional because a deployment without mandate-gated resources
does not need JOSE or SD-JWT support.

Peers are pinned exactly: x402's schemas and EIP-712 domains cross this
boundary, so a version skew is a correctness problem rather than a convenience
one. Importing a subpath without its peer fails at load time and names the
missing package.

### Testing a custom store

`createGateway` accepts a custom `ReceiptStore`. The experimental `./testing`
entry exports conformance cases to test it. The cases use `node:assert/strict`
and import no test runner or optional peers, so you can wrap them with your
runner:

```ts
import { receiptStoreConformanceCases } from '@devlab.group/agent-commerce/testing';
import { it } from 'vitest';

const cases = await receiptStoreConformanceCases(({ clock }) => ({
  store: createMyStore({ clock }),
}));
for (const c of cases) it.skipIf(c.skip !== undefined)(c.name, () => c.run());
```

Have the factory return a new, empty store for each case. The suite calls
`init()` and `close()`. Pass the supplied `clock` to the store so expiry cases
can advance time without waiting.

Each optional method group, such as authorization holds, must be implemented
in full or omitted; one case checks this. Cases that need missing methods are
marked to skip. Durability cases also skip unless the factory returns `reopen()`,
which must close the current store and open another over the same data.
Concurrency cases submit competing reservations, resolutions, idempotency
claims and pending-task takes, then check that exactly one caller wins.

## Integrate with an AI coding agent

The repository includes an experimental [Agent Skills](https://agentskills.io)
skill, [`agent-commerce`](plugins/agent-commerce-gateway/skills/agent-commerce/SKILL.md), for adding the
gateway to an existing API. It covers Base Sepolia configuration, startup and
checks through an unpaid 402 challenge. Its instructions keep secrets out of
chat and leave payment signing to the user. The skill has automated CLI and
config checks.

```text
# Claude Code
/plugin marketplace add devlab-group/agent-commerce
/plugin install agent-commerce-gateway@agent-commerce

# Codex, Cursor and other agents (a third-party installer)
DISABLE_TELEMETRY=1 npx skills add devlab-group/agent-commerce --skill agent-commerce
```

## Quickstart

Requirements: **Node >= 22**, **npm 10**, **Docker**. Nothing else - no API
keys, no real money, no manual blockchain setup.

```bash
git clone <repo> && cd agent-commerce
npm install
docker compose up
```

Then, in a second terminal:

```bash
npm run agent-commerce -- doctor --config config-demo.yaml # verify the whole stack
npm run demo:agent # watch an agent buy something
```

<sub><b>Linux:</b> if your UID/GID is not 1000, export
<code>DOCKER_UID=$(id -u) DOCKER_GID=$(id -g)</code> before
<code>docker compose up</code>. This keeps the generated deployment manifest
host-writable. Docker Desktop does not need this.</sub>

The disposable local stack includes Anvil, MockUSDC, a demo merchant API, the
gateway and a dashboard.

To stop and wipe state: `docker compose down -v`.

### `agent-commerce demo`

From a repository clone, one command starts the stack and runs the buyer agent:

```bash
npm run agent-commerce -- demo
```

It starts the stack with `docker compose up --build --detach --wait`, which
returns once every service with a health check is healthy, then runs
`npm run demo:agent`. A failing step stops the run with exit code 1, after
printing the end of its error output and the command to rerun it by hand.
`demo` does not run `doctor`. The Linux note above applies, and the stack
keeps running in the background until `docker compose down -v`.

## How it works

```text
┌──────────────────────────────────────────────────────────────────────────────┐
│                                   AI Agent                                   │
└───────────────────────────────────────┬──────────────────────────────────────┘
                                        │  MCP · A2A · ACP · HTTP
                                        │  payment proof
                                        │  Agent-Authorization
┌───────────────────────────────────────▼──────────────────────────────────────┐
│                        Agent Commerce Gateway (yours)                        │
│                                                                              │
│                   protocol adapters  →  ExecutionPipeline                    │
│                                                 │                            │
│             ┌────────────────────┬──────────────┴──┬────────────────┐        │
│             ▼                    ▼                 ▼                ▼        │
│   AuthorizationProvider   PaymentProvider   BackendExecutor   ReceiptStore   │
│           (ap2)             (x402, mpp)     (bounded HTTP)      (SQLite)     │
└───────────────────────────────────────┬──────────────────────────────────────┘
                                        │
                             ┌──────────▼─────────┐
                             │  Your backend API  │
                             └────────────────────┘
```

Every protocol adapter uses one execution pipeline, so payment enforcement does
not depend on the adapter. See [Architecture](docs/architecture.md).

## Configure a resource

```yaml
resources:
  market_report:
    name: Premium Market Report
    backend:
      type: http
      method: GET
      url: ${MERCHANT_API_BASE_URL}/api/report
      timeoutMs: 10000
    pricing:
      type: fixed
      amount: "0.01"
      currency: USDC
    expose: [http, mcp]
    payments: [x402]
```

The gateway calls your existing backend, which needs no SDK or changes.

```bash
npm run agent-commerce -- init # generate a config interactively
npm run agent-commerce -- validate # fails loudly, exits non-zero
```

Already have an OpenAPI description? Generate the resources from it
(**experimental**):

```bash
agent-commerce import openapi ./openapi.yaml
```

It writes a reviewable `resources:` fragment with path, query and JSON body
bindings and supported schemas. The source document never decides price or
exposure: `pricing` appears only with `--free` and `expose` only with
`--expose`, and credentials are never imported. See
[OpenAPI import](docs/openapi-import.md) for the supported subset.

See [docs/configuration.md](docs/configuration.md).

## Protocol support

| Protocol     | Status       | Pinned revision                                                                                              |
| ------------ | ------------ | ------------------------------------------------------------------------------------------------------------ |
| **HTTP**     | Supported    | native routes                                                                                                |
| **MCP**      | Supported    | `2026-07-28` and `2025-11-25`, `@modelcontextprotocol/server@2.3.0`                                          |
| **x402**     | Supported    | x402 v2 (`@x402/core`, `@x402/evm`, `@x402/svm`, `@x402/near`), scheme `exact`, EVM and Solana; NEAR partial |
| **MPP**      | Experimental | `mppx@0.13.1`, `charge` + `evm` + EIP-3009, USDC on Base Sepolia and Base                                    |
| **A2A**      | Experimental | A2A v1.0.1, binding `JSONRPC`, method `SendMessage`, x402 extension                                          |
| **ACP**      | Experimental | ACP `2026-04-17`, REST checkout + discovery                                                                  |
| **AP2**      | Experimental | AP2 `v0.2.0`, Direct Checkout Mandate verification                                                           |
| **ERC-8409** | Experimental | draft `SergeevDmitry/ERCs@0ee33230`: signed quotes on x402 EVM offers                                        |
| UCP          | Planned      | no implementation                                                                                            |

[AP2](docs/protocols/ap2.md) is an authorization method, not a transport. It verifies a
mandate before a paid resource settles; the gateway does not issue the mandate
or its Checkout Receipt.

The experimental entries above ship off by default with documented subsets:
[A2A](docs/protocols/a2a.md), [ACP](docs/protocols/acp.md),
[AP2](docs/protocols/ap2.md#not-implemented), [MPP](docs/protocols/mpp.md) and
[ERC-8409 quotes](docs/protocols/x402.md#erc-8409-signed-quotes). Supported
[MCP](docs/protocols/mcp.md) and [x402](docs/protocols/x402.md) are also scoped.
ACP exposes only its five checkout operations; merchant `payment_data` is never
converted into a gateway payment proof.

`GET /.well-known/agent-commerce` reports `supportedSpec`, `capabilities` and
`unsupported` for registered adapters and providers. `agent-commerce doctor`
prints the `unsupported` lists for A2A, ACP and AP2 when they are enabled.

### Versioning

Minor releases may break experimental configurations, integrations or types:
MPP, A2A, ACP, AP2, ERC-8409 quotes, `agent-commerce import openapi`,
`PaymentProvider.reconcile`, the `ReceiptStore` methods and types added in
2.6.0, the `idempotency` resource settings with the store and provider methods
added in 2.7.0, and the `./testing` entry.

The journal's `Transaction` type, `ReceiptStore` journal methods and
`/api/transactions` response shape are supported. The `transactionId` fields,
`receiptCommitted` and flags accepted by `reconcile` and `resolve` are also
supported.

Experimental runtime adapters and providers identify themselves with
`status: experimental` in `GET /.well-known/agent-commerce`. Pin experimental
integrations to an exact version and test upgrades before deployment.
Since 2.1, some supported x402 behavior and types
differ from 2.0; see [Upgrading from 2.0 to 2.x](docs/migration-2.1.md).

## Payment model

- **Non-custodial.** Buyer payments go directly to the merchant address in
  `payTo` (`recipient` for MPP). The gateway does not require buyer or merchant
  private keys. Local facilitator mode uses a separate key to pay gas.
  Experimental ERC-8409 quotes use a dedicated, unfunded signing key.
- **Fail closed.** Missing, malformed, expired, replayed, wrong-amount,
  wrong-recipient, wrong-network and wrong-asset payments are rejected.
- **Replay-safe twice over.** The EIP-3009 or Permit2 nonce stops a double
  spend on-chain; the gateway additionally reserves a `replayKey` derived from
  the authorization before it settles anything.
- **Retry after a lost response (experimental, disabled by default).** With
  `idempotency.redelivery`, buyers can retrieve a stored response without
  another charge using the same wallet, input and x402 `payment-identifier`
  `id` or MPP `Idempotency-Key`. Matching exact resends skip verification
  until retention ends, an exception to fail closed. Responses are stored
  unencrypted in the receipt database. `idempotency.merchantKey` sends a
  derived `Idempotency-Key` to the merchant for paid operations. See
  [Retry after a lost response](docs/payment-flow.md#retry-after-a-lost-response).
- **On-chain settlement in CI.** The end-to-end test asserts the buyer's balance
  falls and the merchant's rises by exactly the price, with a real transaction
  hash in the receipt. A log line saying "payment successful" would not count.
- **One selected rail per resource.** A resource can name several rails, such as
  `payments: [x402, mpp]`; the first enabled one serves it, and nothing selects
  or falls back between rails per request.
- **Authorization is separate from payment.** A resource can also require an
  AP2 mandate, verified before settlement and spendable exactly once. It never
  moves money and never unlocks a resource on its own - the payment still has
  to be real.

## Public networks

Public networks use the same execution pipeline with a remote facilitator.
Choose the network in config; Base mainnet also requires `allowMainnet: true`.

### It has actually settled

These transactions each moved 0.01 USDC from a buyer to a merchant through a
remote facilitator:

| Rail | Network       | Method           | Transaction                                                                                                                                             |
| ---- | ------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| x402 | Base Sepolia  | EIP-3009         | [`0xea41b234c4…`](https://sepolia.basescan.org/tx/0xea41b234c4645a4d335589ec9753646aa7cccd1b97e9e15823b88bff7b54a247)                                   |
| x402 | Base Sepolia  | Permit2          | [`0xa66f56cf39…`](https://sepolia.basescan.org/tx/0xa66f56cf39daeae958b5067c99383da4974c2ae6450786c0f9088700fa6439fe)                                   |
| x402 | Base          | EIP-3009         | [`0x57ec81c2a3…`](https://basescan.org/tx/0x57ec81c2a360d14d59a43cf4e24be09a6bd75cbe6185016372895bda73e42763)                                           |
| x402 | Base          | Permit2          | [`0x79d3da1145…`](https://basescan.org/tx/0x79d3da114522d4b056b29fad34d6492ce5662b92efd8ef2fd1ac21295ab71a42)                                           |
| x402 | Solana Devnet | SPL transfer     | [`5289xbeyHJ…`](https://explorer.solana.com/tx/5289xbeyHJPX1vX9DsLwGGJaGJJCH45qUtArhoTyxdgmWbXqZSjVPuJ3jyyLG4VrT9mFTCkLKhcoNfb1tuJ5qheb?cluster=devnet) |
| x402 | Solana        | SPL transfer     | [`E7gAuG8pMA…`](https://explorer.solana.com/tx/E7gAuG8pMArQjgRvGCwqm6S4qpt4xFNuhUTxeaZ5M1M1z1VA8eybyJJAXyo9paecCKnyrxYEuppoAemhntnnzTa)                 |
| x402 | NEAR Testnet  | NEP-141 transfer | [`5tdtGLv4TB…`](https://testnet.nearblocks.io/txns/5tdtGLv4TBifK7c12hEdRuvxvNNrWjcWDH4e1ApUcE1W)                                                        |
| x402 | NEAR          | NEP-141 transfer | [`7hU3imHepV…`](https://nearblocks.io/txns/7hU3imHepVZazGv53QfWYMGnWX5sVFxJAepemmwVSzt2)                                                                |
| MPP  | Base Sepolia  | EIP-3009         | [`0x5d9fcd111b…`](https://sepolia.basescan.org/tx/0x5d9fcd111b615bb8fc9e56d55096f119fc81c3c72c334f813cfaecee402d0ac3)                                   |
| MPP  | Base          | EIP-3009         | [`0xf4255bd7ec…`](https://basescan.org/tx/0xf4255bd7ec46ed06505dedfb5f9537924bd2633816e7952ec6f2b468395b1087)                                           |

Facilitators broadcast these payments and paid their ETH, SOL or NEAR fees.
Permit2 requires an initial, gas-paying token approval from the buyer. The
NEAR runs used the suite's SDK facilitator and relayer; support remains
partial pending hosted-facilitator settlement. Buyer and merchant keys stayed
outside the gateway. The smoke suites verified settlement on-chain.

Reproduce with `npm run test:testnet`, which spends test USDC, or
`npm run test:mainnet`, which spends real USDC. Both run only by hand; see
[Base mainnet](#base-mainnet) and [smoke tests](docs/smoke-tests.md). The
suites also cover NEAR testnet and mainnet (settled) and Arbitrum Sepolia (not
yet run).

### The facilitator model

A **facilitator** verifies the buyer's authorization and broadcasts the
transfer. It is the only component that needs gas.

| `facilitator.mode` | Who signs                              | Where it is allowed                                                |
| ------------------ | -------------------------------------- | ------------------------------------------------------------------ |
| `local`            | this process, with a key you configure | a local Anvil chain; refused on mainnet, fails readiness elsewhere |
| `remote`           | an HTTP facilitator you point at       | anywhere                                                           |

With `remote`, the gateway holds no facilitator signing key. The buyer signs an
EIP-3009 authorization without ETH, and the facilitator pays gas. Because the
authorization fixes the recipient, amount and chain, the facilitator cannot
redirect the transfer. It can see authorizations and refuse service.

Remote facilitator auth supports `none`, `bearer` and `cdp`. CDP auth signs a
fresh JWT per request with the CDP API key. No auth mode needs an extra peer
package. Config load rejects other types.

### Base Sepolia

```yaml
payments:
  x402:
    enabled: true
    network: eip155:84532
    rpcUrl: https://base-sepolia-rpc.publicnode.com # health checks only
    asset: "0x036CbD53842c5426634e7929541eC2318f3dCF7e" # Circle USDC
    assetName: USDC
    assetVersion: "2"
    assetDecimals: 6
    payTo: ${MERCHANT_WALLET}
    maxTimeoutSeconds: 300
    facilitator:
      mode: remote
      url: https://x402.org/facilitator
      auth: { type: none }
```

Full config in [`examples/base-sepolia/`](examples/base-sepolia/). Test USDC
from [faucet.circle.com](https://faucet.circle.com); the buyer needs no ETH.
`npm run test:testnet` drives the whole flow and reads the result back off the
chain.

The local dev chain is `eip155:31337`, Anvil's default chain id, so the network
says whether a deployment is local, testnet or mainnet. `doctor`, `health()`
and `/.well-known` report it.

### Base mainnet

Mainnet moves real funds. Config validation requires:

| Required                                                       |                                                                |
| -------------------------------------------------------------- | -------------------------------------------------------------- |
| `allowMainnet: true`                                           | mainnet is never a default                                     |
| `facilitator.mode: remote`                                     | `local` needs a funded gas key inside this process             |
| an HTTPS `facilitator.url`                                     |                                                                |
| `allowUnauthenticatedFacilitator: true`                        | required when `facilitator.auth.type` is `none`                |
| a non-development destination (`payTo` or `recipient`)         |                                                                |
| Base USDC with `assetName: "USD Coin"` and `assetVersion: "2"` | the buyer signs this EIP-712 domain; `"USDC"` is wrong on Base |

Full config in [`examples/base-mainnet/`](examples/base-mainnet/), and
[`examples/base-mainnet-payai/`](examples/base-mainnet-payai/) for an
unauthenticated facilitator. The mainnet smoke suites cover x402 and MPP and
spend real USDC when their opt-ins and credentials are present.

`agent-commerce validate` reports any of the above before anything starts, and
`doctor` prints `LIVE MAINNET MODE - REAL FUNDS`.

> The public-network suites never run in CI, because a workflow would need a
> funded buyer key in repository secrets. Run them from the machine that holds
> the wallet. A test whose required settings are missing skips itself.

## Diagnostics

```console
$ 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 acp=off
INFO  A2A                  disabled
INFO  ACP                  disabled
INFO  AP2                  disabled
PASS  Payments             x402 v2 (scheme=exact) enabled - LOCAL dev chain (eip155:31337), destination=0x7099…79C8, facilitator=local (in-process), paymentFlow=authorization
INFO  Payments (MPP)       MPP not configured
PASS  Storage              sqlite schema v5 writable; receipts=2
PASS  Transactions         none unresolved
PASS  Protocol versions    reported by gateway /.well-known/agent-commerce

Score: 8/8 checks passed
```

`doctor` compares live settlement terms with local config for each rail and
resource: offer order, network, token, payee and transfer method, plus decimals
and payment flows when published. Mismatches fail the check. It also names
failing offers when health is published; reasons appear in gateway debug logs.

Exits non-zero if anything fails. `--json` for machines.

### `agent-commerce reconcile`

```bash
npm run agent-commerce -- reconcile --config config.yaml --dry-run
```

Resolves attempts whose settlement outcome the gateway did not learn:
`settlement-uncertain` and legacy `failed` attempts, plus `reserved` attempts in
the journal at least `--min-age` old (default `15m`). The payment provider
checks chain evidence and returns:

- **settled**: a final transfer matches the authorization. The attempt becomes
  `settled` with its transaction ID.
- **not settled**: final chain state shows the authorization unused after its
  last valid second on EVM or its `maxBlockHeight` on NEAR. The attempt becomes
  `rejected` with reason `not_settled`. Solana reconciliation does not return
  this result: blockhash expiry alone does not prove nonpayment.
- **unknown**: the evidence is inconclusive. The report includes a reason and,
  when available, a time to try again. The attempt remains unresolved.

The command builds payment providers as `start` does without starting a
listener. It does not call a facilitator, broadcast, refund or retry delivery.
Each resolution records `--actor` (default: the OS user) and the evidence.
`--dry-run` checks evidence without recording resolutions; it still opens and
initializes the receipt database. `--json` prints the report for machines.
`--limit` caps payment checks and hold updates separately. Journal entries are
checked by state: `payment-uncertain`, `payment-pending`, then
`delivery-started`, newest first within each group. Only eligible payment
attempts are considered. A capped batch of attempts without journal entries
follows.
It exits 0 after a completed run, even if attempts remain unresolved, and 1 if
the run fails to complete. If a store error stops a hold scan, the command
prints the report, preserving any saved resolutions, and exits 1.
`reconcile` and `resolve` refuse receipt databases below schema v5 without
migrating them. Back up the database, then start the gateway once before
retrying either command.

Each family needs its own evidence:

- **EVM**: uses the recorded transaction hash or searches logs for a used nonce
  near the attempt's creation time, within its validity bounds. The log search
  covers at most 200,000 blocks. If no match is found and part of the candidate
  range remains unsearched, it returns `unknown` (`search_window_too_wide`).
- **Solana**: uses the recorded transaction signature or searches a known
  payer's history near the attempt's creation time. Only successful finalized
  transactions count. A search that reaches its page cap returns `unknown`
  (`history_too_long`). Otherwise, a missing match gives `not_expired` while
  the blockhash is valid at `confirmed` commitment, then `blockhash_expired`.
- **NEAR**: uses the recorded transaction hash and relayer IDs from the
  facilitator's `/supported`. Both are needed to prove `settled`. Without
  them, final access-key state can still prove `not-settled` after expiry;
  other cases remain `unknown`.

Coverage is partial: EVM reconciliation (x402 EIP-3009 and Permit2, and MPP)
is tested on Anvil, Solana on `solana-test-validator` and NEAR on a local
sandbox. Public-network validation is pending. EVM and NEAR can report
`not_final` while a referenced transaction awaits finality.

With `authorization.ap2` enabled, reconciliation also updates AP2 holds on
resolved payments: `settled` consumes the mandate; `rejected` releases it.
It updates holds in the receipt database without verifying mandates.
The report's `holds` list (`--json`) covers pending holds whose payments are
resolved in the database. It also attempts to release `reserved` holds older
than `--min-age` whose request has no current or archived payment attempt.
The pipeline reserves an attempt before calling settlement. Scanned orphan
holds appear in `orphanHolds`; dry runs list at most one page of them.
Dry runs write no resolutions or hold changes, but still open and initialize
the database.
Later runs can retry holds left pending after earlier payment resolutions,
including those recorded by `resolve` while AP2 was disabled.

### `agent-commerce resolve`

```bash
npm run agent-commerce -- resolve <attemptId> --status settled --reason "transfer seen in explorer" --reference 0x...
```

Records an operator's verdict on one unresolved payment attempt, including
one `reconcile` left `unknown`. Only `reserved`, `settlement-uncertain` and
legacy `failed` attempts can be resolved, once each:

- `--status settled` has no expiry check. `--reference` records an unchecked
  operator claim in the resolution; it does not replace the attempt's recorded
  transaction ID.
- A `reserved` attempt younger than 15 minutes is refused (`too_recent`),
  because the gateway may still be settling it.
- `--status rejected` is refused while the recorded expiry is in the future.
  Without a valid recorded expiry, `--confirm-expired` is required. This
  includes Solana and NEAR attempts and those written before the journal.

`--actor` is a caller-supplied label and defaults to the OS user. The command
opens the receipt database directly and works with the gateway running or
stopped. With AP2 enabled, it attempts the same hold update as `reconcile`.
If the provider is disabled, its call fails, or the hold's journal write fails,
the command reports the update as pending with a warning. A later `reconcile`
run can retry it.
`POST /api/payment-attempts/:id/resolve` applies the same rules over HTTP;
the admin token controls access without identifying the operator. Exit 0
means the resolution was written; 1 means a refusal or error.

### `agent-commerce backup`

```bash
npm run agent-commerce -- backup --out ./backups/2026-11-01
```

Copies the receipt database into a new or empty directory with a `backup.json`
manifest. It also copies existing, unimported legacy files configured for enabled
AP2 or ACP. It can run alongside a live gateway. See [Storage](docs/storage.md)
for restore and shutdown instructions.

### `agent-commerce version`

```bash
npm run agent-commerce -- version
```

Prints the CLI version and, for each protocol SDK and runtime library, the
version the package declares and the area that uses it. These are the pins in
`package.json`, optional peers included, not the versions installed in
`node_modules`. `agent-commerce --version` prints the CLI version alone.

## Exposure and access

The demo binds everything to `127.0.0.1`. Before putting the gateway anywhere
reachable by anyone else, know the split:

- **Agent routes** (`/api/resources/:id/invoke`, `/mcp`, the A2A mount) are
  unauthenticated by design - paid resources are protected by payment, not by a
  password. ACP is the exception: its checkout routes require a bearer token.
- **Operator routes** (`/api/receipts`, `/api/events`, `/api/payment-attempts`,
  `/api/transactions`) expose the merchant's commerce ledger: payer addresses,
  amounts and settlement hashes. `POST /api/payment-attempts/:id/resolve`
  records a resolution. They require `server.adminToken` and return **404** if
  none is configured.
- **Browsers** are governed by `server.allowedOrigins`, an explicit allowlist
  that defaults to empty.
- **There is no rate limiting.** A free resource is an unauthenticated proxy to
  your backend at whatever rate a caller chooses. Quotas and abuse controls
  belong in your API or your edge.

[SECURITY.md](SECURITY.md) states plainly what this does and does not protect.

## Development

```bash
npm run verify # contract + lint + typecheck + test
npm run test:e2e # deterministic end-to-end, boots its own chain
```

Foundry (`anvil`, `forge`, `cast`) is needed for the chain work.
See [CONTRIBUTING.md](CONTRIBUTING.md).

## Roadmap

**Now** - MCP, x402 v2, settlement on the local chain, Base Sepolia, Base
mainnet, Solana devnet and Solana mainnet, receipts, doctor, deterministic
demo, experimental A2A v1.0.1 and ACP `2026-04-17` checkout adapters,
experimental AP2 v0.2.0 mandate verification, experimental MPP `charge`
payments, experimental OpenAPI import, and experimental retries after a lost
response.

**Next** - a `doctor` GitHub Action · UCP ·
autonomous-mode AP2 (open mandates, agent key binding, constraint evaluation) ·
more of ACP (carts, feed, delegated payment) · Shopify and WooCommerce examples ·
PostgreSQL · richer observability · multi-file and remote OpenAPI sources.

New protocols need evidence that the adapter model works in real use before
they are added to a release.

## Documentation

|                                                |                                             |
| ---------------------------------------------- | ------------------------------------------- |
| [Architecture](docs/architecture.md)           | how the pieces fit                          |
| [Payment flow](docs/payment-flow.md)           | the paid round trip and failure paths       |
| [Protocols](docs/protocols.md)                 | supported and unsupported protocol features |
| [AP2](docs/protocols/ap2.md)                   | mandate verification and the trust model    |
| [Configuration](docs/configuration.md)         | `config.yaml` reference                     |
| [Storage](docs/storage.md)                     | upgrade, backup, restore and shutdown       |
| [Upgrading from 2.0](docs/migration-2.1.md)    | changes that can break a 2.0 setup          |
| [Smoke tests](docs/smoke-tests.md)             | settling on public networks, by hand        |
| [OpenAPI import](docs/openapi-import.md)       | generate resources from an existing API     |
| [Security model](docs/security.md)             | trust boundaries, and what we do not defend |
| [Contracts](docs/contracts.md)                 | the frozen cross-area contract              |
| [Adapter guide](docs/contributing-adapters.md) | add a protocol or a payment rail            |

## License

[Apache-2.0](LICENSE).