@devlab.group/agent-commerce 1.4.1 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  <h1 align="center">Agent Commerce Gateway</h1>
2
2
 
3
3
  <p align="center">
4
- <strong>One backend. Agent-commerce protocols on the front. No proprietary middleman.</strong>
4
+ <strong>One backend. Multiple agent-commerce protocols. Self-hosted and non-custodial.</strong>
5
5
  </p>
6
6
 
7
7
  <p align="center">
@@ -10,6 +10,7 @@
10
10
  <img alt="TypeScript" src="https://img.shields.io/badge/TypeScript-strict-3178c6">
11
11
  <img alt="MCP" src="https://img.shields.io/badge/MCP-supported-6b4fbb">
12
12
  <img alt="x402" src="https://img.shields.io/badge/x402-supported-0052ff">
13
+ <img alt="MPP" src="https://img.shields.io/badge/MPP-experimental-f0a30a">
13
14
  <img alt="A2A" src="https://img.shields.io/badge/A2A-experimental-f0a30a">
14
15
  <img alt="ACP" src="https://img.shields.io/badge/ACP-experimental-f0a30a">
15
16
  <img alt="AP2" src="https://img.shields.io/badge/AP2-experimental-f0a30a">
@@ -17,21 +18,19 @@
17
18
 
18
19
  ## What it is, in ten seconds
19
20
 
20
- You already have an HTTP API. AI agents want to **discover** it, **call** it and
21
- **pay** for it - over protocols you did not write and do not want to maintain.
21
+ You already have an HTTP API. Agent Commerce Gateway lets AI agents discover,
22
+ call and pay for it without adding those protocols to your backend.
22
23
 
23
- Agent Commerce Gateway sits in front of your existing API, in **your**
24
- infrastructure, and does that for you. You describe an endpoint in a YAML file -
25
- or generate that description from an OpenAPI document you already have - and
26
- agents get an MCP tool and an x402 paywall. Switch on the experimental adapters
27
- and the same resource is also an A2A skill, or an ACP checkout session. Switch
28
- on AP2 and a paid resource can also demand a signed mandate: proof the human
29
- behind the agent approved that exact purchase. The money goes straight to your
30
- wallet - the gateway never holds it, and never holds your keys.
24
+ Run the gateway in your infrastructure and describe each endpoint in YAML, or
25
+ generate draft resource definitions from OpenAPI. Resources can be exposed over
26
+ HTTP, MCP or experimental A2A; dedicated resources can implement experimental
27
+ ACP checkout operations. Paid resources use x402 or experimental MPP and can
28
+ also require an AP2 mandate proving that the buyer approved the purchase. Funds
29
+ go directly to the merchant wallet; the gateway never holds them or your keys.
31
30
 
32
31
  ```text
33
32
  Your existing API → Agent Commerce Gateway → AI Agent
34
- MCP · A2A · ACP · x402 · AP2 · receipts · doctor
33
+ MCP · A2A · ACP · x402 · MPP · AP2 · receipts · doctor
35
34
  ```
36
35
 
37
36
  ## Demo
@@ -68,21 +67,18 @@ npm install -g @devlab.group/agent-commerce # or install the `agent-commerce` bi
68
67
  agent-commerce doctor
69
68
  ```
70
69
 
71
- Requires **Node >= 22**. One package ships two things: the `agent-commerce`
72
- CLI (`init`, `validate`, `doctor`, `demo`) and a library for embedding the
73
- gateway in your own process. A default install is ~65 MB and pulls no
74
- blockchain or wallet dependencies at all - ~17 MB of that is the OpenAPI
75
- parser behind `import openapi`, which is a normal dependency because
76
- onboarding an existing API is the CLI's main job.
70
+ Requires **Node >= 22**. The package includes the `agent-commerce` CLI
71
+ (`init`, `validate`, `doctor`, `demo`) and a library for embedding the gateway.
72
+ The default install excludes optional protocol, payment and authorization
73
+ peers. The OpenAPI parser is a regular dependency because import is part of the
74
+ CLI.
77
75
 
78
76
  ```ts
79
77
  import { createGateway, loadConfig, receipts } from '@devlab.group/agent-commerce';
80
78
 
81
79
  const config = await loadConfig({ path: 'config.yaml' });
82
80
  const store = receipts({ path: './receipts.sqlite' });
83
- // Every ReceiptStore must be initialised before anything else touches it -
84
- // for the SQLite one this is where a schema written by a newer version is
85
- // caught, at boot rather than at the first query
81
+ // Initialize before use so an incompatible SQLite schema fails at startup
86
82
  await store.init();
87
83
 
88
84
  const gateway = await createGateway({
@@ -94,21 +90,23 @@ const gateway = await createGateway({
94
90
  const { url } = await gateway.listen();
95
91
  ```
96
92
 
97
- ### Optional peers - install only the rails you use
93
+ ### Optional peers
98
94
 
99
- The MCP adapter, the x402 provider and AP2 verification live on their own
100
- subpaths, because each needs a dependency the rest of the package does not -
101
- the x402 rail brings the whole EVM signing and RPC stack, which a gateway
102
- serving a free HTTP resource has no business installing.
95
+ Components with extra dependencies use separate subpaths, so a gateway serving
96
+ free HTTP resources does not install MCP, EVM, JOSE or SD-JWT packages.
103
97
 
104
98
  | You want | Install | Import |
105
99
  | --------------------------------------- | ---------------------------------- | ------------------------------------------ |
106
100
  | gateway, config, receipts, CLI | `@devlab.group/agent-commerce` | `from '@devlab.group/agent-commerce'` |
107
101
  | expose resources as MCP tools | `+ @modelcontextprotocol/sdk` | `from '@devlab.group/agent-commerce/mcp'` |
108
102
  | accept x402 payments | `+ @x402/core @x402/evm viem` | `from '@devlab.group/agent-commerce/x402'` |
103
+ | accept MPP payments | `+ mppx viem @x402/core @x402/evm` | `from '@devlab.group/agent-commerce/mpp'` |
109
104
  | verify AP2 mandates, sign checkout JWTs | `+ jose @sd-jwt/core canonicalize` | `from '@devlab.group/agent-commerce/ap2'` |
110
105
  | authenticate to a CDP facilitator | `+ @coinbase/x402` | (no import - loaded on demand) |
111
106
 
107
+ MPP settles through an x402 facilitator, so its entry also needs the x402
108
+ peers.
109
+
112
110
  ```bash
113
111
  npm install @devlab.group/agent-commerce @modelcontextprotocol/sdk @x402/core @x402/evm viem
114
112
  ```
@@ -119,23 +117,17 @@ import { x402 } from '@devlab.group/agent-commerce/x402';
119
117
  import { ap2 } from '@devlab.group/agent-commerce/ap2';
120
118
  ```
121
119
 
122
- The AP2 three are small - about 1.3 MB installed between them, against roughly
123
- 63 MB for the x402 stack - but they stay optional on the same principle: a
124
- deployment that gates nothing on a mandate should not carry a JOSE stack and an
125
- SD-JWT parser to serve a resource.
120
+ AP2 peers remain optional because a deployment without mandate-gated resources
121
+ does not need JOSE or SD-JWT support.
126
122
 
127
123
  Peers are pinned exactly: x402's schemas and EIP-712 domains cross this
128
124
  boundary, so a version skew is a correctness problem rather than a convenience
129
- one. Import a subpath without its
130
- peer installed and Node fails at load naming the missing package - deliberately,
131
- rather than starting a gateway that silently serves nothing.
125
+ one. Importing a subpath without its peer fails at load time and names the
126
+ missing package.
132
127
 
133
- `@coinbase/x402` is the odd one out: it has no import of its own and is loaded
134
- dynamically, only when `facilitator.auth.type: cdp` is configured. It is worth
135
- avoiding if you can - it brings `@coinbase/cdp-sdk` and `axios`, which carry
136
- high-severity advisories, while the package itself and the other three peers
137
- audit clean. `auth.type: bearer` covers any facilitator with a static token and
138
- installs nothing.
128
+ `@coinbase/x402` has no subpath import. It is loaded dynamically only for
129
+ `facilitator.auth.type: cdp` and brings the larger CDP dependency tree. A
130
+ facilitator with a static token can use `auth.type: bearer` without it.
139
131
 
140
132
  ## Quickstart
141
133
 
@@ -155,15 +147,13 @@ npm run agent-commerce -- doctor --config config-demo.yaml # verify the whole st
155
147
  npm run demo:agent # watch an agent buy something
156
148
  ```
157
149
 
158
- <sub><b>Linux only</b>, and only if your user is not UID/GID 1000 (check with
159
- <code>id -u && id -g</code>): export <code>DOCKER_UID=$(id -u) DOCKER_GID=$(id -g)</code>
160
- before <code>docker compose up</code>. The chain-deploy step runs as that user so the
161
- deployment manifest it writes stays host-writable rather than root-owned. Docker
162
- Desktop on macOS and Windows translates permissions through its VM and does not
163
- need this.</sub>
150
+ <sub><b>Linux:</b> if your UID/GID is not 1000, export
151
+ <code>DOCKER_UID=$(id -u) DOCKER_GID=$(id -g)</code> before
152
+ <code>docker compose up</code>. This keeps the generated deployment manifest
153
+ host-writable. Docker Desktop does not need this.</sub>
164
154
 
165
- That is the whole thing. The stack is a private Anvil chain, a mock USDC token,
166
- a demo merchant API, the gateway and a dashboard - all local and disposable.
155
+ The disposable local stack includes Anvil, MockUSDC, a demo merchant API, the
156
+ gateway and a dashboard.
167
157
 
168
158
  To stop and wipe state: `docker compose down -v`.
169
159
 
@@ -174,7 +164,7 @@ To stop and wipe state: `docker compose down -v`.
174
164
  │ AI Agent │
175
165
  └───────────────────────────────────────┬──────────────────────────────────────┘
176
166
  │ MCP · A2A · ACP · HTTP
177
- │ PAYMENT-SIGNATURE
167
+ │ payment proof
178
168
  │ Agent-Authorization
179
169
  ┌───────────────────────────────────────▼──────────────────────────────────────┐
180
170
  │ Agent Commerce Gateway (yours) │
@@ -184,7 +174,7 @@ To stop and wipe state: `docker compose down -v`.
184
174
  │ ┌────────────────────┬──────────────┴──┬────────────────┐ │
185
175
  │ ▼ ▼ ▼ ▼ │
186
176
  │ AuthorizationProvider PaymentProvider BackendExecutor ReceiptStore │
187
- │ (ap2) (x402) (bounded HTTP) (SQLite) │
177
+ │ (ap2) (x402, mpp) (bounded HTTP) (SQLite) │
188
178
  └───────────────────────────────────────┬──────────────────────────────────────┘
189
179
  │
190
180
  ┌──────────▼─────────┐
@@ -192,11 +182,10 @@ To stop and wipe state: `docker compose down -v`.
192
182
  └────────────────────┘
193
183
  ```
194
184
 
195
- Every protocol adapter converges on **one execution pipeline**. That is what
196
- makes payment enforcement a property of the system rather than something each
197
- adapter has to remember. Money never passes through the box: the buyer pays the
198
- merchant directly on chain, and the gateway holds neither the funds nor a key.
199
- Full detail in [docs/architecture.md](docs/architecture.md).
185
+ Every protocol adapter uses one execution pipeline, so payment enforcement does
186
+ not depend on the adapter. The buyer pays the merchant directly on-chain; the
187
+ gateway holds neither the funds nor a buyer or merchant key. See
188
+ [Architecture](docs/architecture.md).
200
189
 
201
190
  ## Configure a resource
202
191
 
@@ -231,57 +220,56 @@ Already have an OpenAPI description? Generate the resources from it
231
220
  agent-commerce import openapi ./openapi.yaml
232
221
  ```
233
222
 
234
- It writes a reviewable `resources:` fragment - path, query and JSON body
235
- mapped, schemas converted to what the gateway actually enforces - and
236
- deliberately leaves `pricing` and `expose` out, because an OpenAPI document has
237
- no opinion on what an operation costs or who may see it. Credentials are never
238
- imported. See [OpenAPI import](docs/openapi-import.md) for the exact supported subset.
223
+ It writes a reviewable `resources:` fragment with path, query and JSON body
224
+ bindings and supported schemas. It omits `pricing`, `expose` and credentials;
225
+ the source document cannot decide price or exposure. See
226
+ [OpenAPI import](docs/openapi-import.md) for the supported subset.
239
227
 
240
228
  See [docs/configuration.md](docs/configuration.md).
241
229
 
242
230
  ## Protocol support
243
231
 
244
- | Protocol | Status | Pinned revision |
245
- | --------- | ------------ | -------------------------------------------------------- |
246
- | **MCP** | Supported | `@modelcontextprotocol/sdk@1.30.0` |
247
- | **x402** | Supported | x402 v2 (`@x402/core`, `@x402/evm`), scheme `exact`, EVM |
248
- | **HTTP** | Supported | native routes |
249
- | **A2A** | Experimental | A2A v1.0.0, binding `JSONRPC`, method `SendMessage` |
250
- | **ACP** | Experimental | ACP `2026-04-17`, REST checkout + discovery |
251
- | **AP2** | Experimental | AP2 `v0.2.0`, Direct Checkout Mandate verification |
252
- | UCP · MPP | Planned | - |
253
-
254
- [AP2](docs/ap2.md) is in that table because people look there, but it is an **authorization**
255
- method rather than a transport: it gates settlement on a resource that still
256
- takes a real payment, and it is the verifying half only - the gateway holds no
257
- signing key and issues no Checkout Receipt.
258
-
259
- "Planned" means **no code ships for it**. "Experimental" means the code ships,
260
- is tested against the protocol's own official artifacts, and serves a narrow
261
- named subset: [A2A](docs/protocols.md#a2a) and [ACP](docs/protocols.md#acp) are
262
- both off by default and documented in full there, as are
263
- [MCP](docs/protocols.md#mcp) and [x402](docs/protocols.md#x402). ACP serves the
264
- five stable checkout operations and advertises `services: ["checkout"]` and
265
- nothing more; its `payment_data` stays with the merchant's own checkout and is
266
- never turned into an x402 payment.
267
-
268
- Each adapter reports its own `supportedSpec`, `capabilities` and `unsupported`
269
- list at runtime through `GET /.well-known/agent-commerce` and
270
- `agent-commerce doctor`, so the claim is checkable rather than marketing.
232
+ | Protocol | Status | Pinned revision |
233
+ | -------- | ------------ | ------------------------------------------------------------------------- |
234
+ | **HTTP** | Supported | native routes |
235
+ | **MCP** | Supported | `@modelcontextprotocol/sdk@1.30.0` |
236
+ | **x402** | Supported | x402 v2 (`@x402/core`, `@x402/evm`), scheme `exact`, EVM |
237
+ | **MPP** | Experimental | `mppx@0.10.1`, `charge` + `evm` + EIP-3009, USDC on Base Sepolia and Base |
238
+ | **A2A** | Experimental | A2A v1.0.0, binding `JSONRPC`, method `SendMessage` |
239
+ | **ACP** | Experimental | ACP `2026-04-17`, REST checkout + discovery |
240
+ | **AP2** | Experimental | AP2 `v0.2.0`, Direct Checkout Mandate verification |
241
+ | UCP | Planned | no implementation |
242
+
243
+ [AP2](docs/ap2.md) is an authorization method, not a transport. It verifies a
244
+ mandate before a paid resource settles; the gateway does not issue the mandate
245
+ or its Checkout Receipt.
246
+
247
+ The experimental entries above ship off by default with documented subsets:
248
+ [A2A](docs/protocols.md#a2a), [ACP](docs/protocols.md#acp),
249
+ [AP2](docs/ap2.md#not-implemented) and [MPP](docs/protocols.md#mpp). Supported
250
+ [MCP](docs/protocols.md#mcp) and [x402](docs/protocols.md#x402) are also scoped.
251
+ ACP exposes only its five checkout operations; merchant `payment_data` is never
252
+ converted into a gateway payment proof.
253
+
254
+ `GET /.well-known/agent-commerce` reports `supportedSpec`, `capabilities` and
255
+ `unsupported` for registered adapters and providers. `agent-commerce doctor`
256
+ prints the `unsupported` lists for A2A, ACP and AP2.
271
257
 
272
258
  ## Payment model
273
259
 
274
260
  - **Non-custodial.** The gateway never holds funds, and never asks for a
275
261
  merchant or buyer private key. `payTo` is your address.
276
262
  - **Fail closed.** Missing, malformed, expired, replayed, wrong-amount,
277
- wrong-recipient, wrong-network and wrong-asset payments all fail - each with a
278
- test.
263
+ wrong-recipient, wrong-network and wrong-asset payments are rejected.
279
264
  - **Replay-safe twice over.** EIP-3009 stops a double spend on-chain; the
280
265
  gateway additionally reserves a `replayKey` derived from the authorisation
281
266
  before it settles anything.
282
- - **Real settlement in CI.** The end-to-end test asserts the buyer's balance
267
+ - **On-chain settlement in CI.** The end-to-end test asserts the buyer's balance
283
268
  falls and the merchant's rises by exactly the price, with a real transaction
284
269
  hash in the receipt. A log line saying "payment successful" would not count.
270
+ - **One selected rail per resource.** A resource can name several rails, such as
271
+ `payments: [x402, mpp]`; the first enabled one serves it, and nothing selects
272
+ or falls back between rails per request.
285
273
  - **Authorization is separate from payment.** A resource can also require an
286
274
  AP2 mandate, verified before settlement and spendable exactly once. It never
287
275
  moves money and never unlocks a resource on its own - the payment still has
@@ -294,19 +282,20 @@ not this process. No code changes, and no "live mode" to switch on.
294
282
 
295
283
  ### It has actually settled
296
284
 
297
- Not a roadmap entry. Both of these moved 0.01 USDC from a buyer to a merchant
298
- through a remote facilitator:
285
+ These transactions each moved 0.01 USDC from a buyer to a merchant through a
286
+ remote facilitator:
299
287
 
300
- | Network | Transaction |
301
- | ------------ | --------------------------------------------------------------------------------------------------------------------- |
302
- | Base Sepolia | [`0xea41b234c4…`](https://sepolia.basescan.org/tx/0xea41b234c4645a4d335589ec9753646aa7cccd1b97e9e15823b88bff7b54a247) |
303
- | Base | [`0x57ec81c2a3…`](https://basescan.org/tx/0x57ec81c2a360d14d59a43cf4e24be09a6bd75cbe6185016372895bda73e42763) |
288
+ | Rail | Network | Transaction |
289
+ | ---- | ------------ | --------------------------------------------------------------------------------------------------------------------- |
290
+ | x402 | Base Sepolia | [`0xea41b234c4…`](https://sepolia.basescan.org/tx/0xea41b234c4645a4d335589ec9753646aa7cccd1b97e9e15823b88bff7b54a247) |
291
+ | x402 | Base | [`0x57ec81c2a3…`](https://basescan.org/tx/0x57ec81c2a360d14d59a43cf4e24be09a6bd75cbe6185016372895bda73e42763) |
292
+ | MPP | Base Sepolia | [`0x5d9fcd111b…`](https://sepolia.basescan.org/tx/0x5d9fcd111b615bb8fc9e56d55096f119fc81c3c72c334f813cfaecee402d0ac3) |
293
+ | MPP | Base | [`0xf4255bd7ec…`](https://basescan.org/tx/0xf4255bd7ec46ed06505dedfb5f9537924bd2633816e7952ec6f2b468395b1087) |
304
294
 
305
- In both, the gateway held no key, signed nothing and paid no gas - the buyer
306
- signed an EIP-3009 authorisation offline holding no ETH, and the facilitator
307
- broadcast it. Each run reads the buyer and merchant balances and the
308
- transaction receipt back off the chain afterwards; the gateway's own report of
309
- success is not the proof.
295
+ The buyer signed each EIP-3009 authorisation without ETH; the remote facilitator
296
+ paid gas and broadcast it. The gateway path received no buyer or merchant key.
297
+ The public-network smoke suites verify balance changes and fetch the transaction
298
+ receipt from the chain instead of trusting the gateway's response.
310
299
 
311
300
  Reproduce with `npm run test:testnet` / `npm run test:mainnet` - both spend
312
301
  real funds, skip themselves without credentials, and never run in CI.
@@ -322,17 +311,14 @@ gateway on a public network.
322
311
  | `local` | this process, with an Anvil dev key | the local dev chain only |
323
312
  | `remote` | an HTTP facilitator you point at | anywhere |
324
313
 
325
- With `remote`, the gateway holds **no signing key at all**. The buyer signs an
326
- EIP-3009 authorisation offline - no ETH required - and the facilitator pays the
327
- gas. A facilitator cannot redirect your money: the authorisation names its
328
- recipient, amount and chain, so it can broadcast exactly that transfer or
329
- nothing. What it can do is see every authorisation you handle, and stop
330
- answering.
314
+ With `remote`, the gateway holds no facilitator signing key. The buyer signs an
315
+ EIP-3009 authorisation without ETH, and the facilitator pays gas. Because the
316
+ authorisation fixes the recipient, amount and chain, the facilitator cannot
317
+ redirect the transfer. It can see authorisations and refuse service.
331
318
 
332
- Three auth types: `none`, `bearer` (a static token, installs nothing) and `cdp`
333
- (Coinbase Developer Platform, which signs a fresh JWT per request). Anything
334
- else is refused at config load rather than sent nothing. You can also run your
335
- own - `remote` does not care who operates the endpoint.
319
+ Remote facilitator auth supports `none`, `bearer` and `cdp`. CDP signs a fresh
320
+ JWT per request and conditionally loads `@coinbase/x402`; bearer auth needs no
321
+ extra peer. Other types are rejected at config load.
336
322
 
337
323
  ### Base Sepolia
338
324
 
@@ -366,8 +352,7 @@ together, and reported by `doctor`, `health()` and `/.well-known`.
366
352
 
367
353
  ### Base mainnet
368
354
 
369
- Real funds, so nothing is defaulted. Every one of these is checked at config
370
- load, and the gateway will not start without them:
355
+ Mainnet moves real funds. Config validation requires:
371
356
 
372
357
  | Required | |
373
358
  | ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
@@ -375,13 +360,13 @@ load, and the gateway will not start without them:
375
360
  | `facilitator.mode: remote` | `local` needs a funded gas key inside this process |
376
361
  | an HTTPS `facilitator.url` | |
377
362
  | `allowUnauthenticatedFacilitator: true` | only if that facilitator takes no credential |
378
- | a non-development `payTo` | |
379
- | `asset` = USDC on Base, `assetName: "USD Coin"` | **not** `"USDC"` - that deployment predates the rename, and the buyer signs the name into their EIP-712 domain |
363
+ | a non-development destination (`payTo` or `recipient`) | |
364
+ | Base USDC with `assetName: "USD Coin"` and `assetVersion: "2"` | the buyer signs this EIP-712 domain; `"USDC"` is wrong on Base |
380
365
 
381
366
  Full config in [`examples/base-mainnet/`](examples/base-mainnet/), and
382
367
  [`examples/base-mainnet-payai/`](examples/base-mainnet-payai/) for an
383
- unauthenticated facilitator. `npm run test:mainnet` proves it end to end and
384
- spends real USDC on every run.
368
+ unauthenticated facilitator. The mainnet smoke suites cover x402 and MPP and
369
+ spend real USDC when their opt-ins and credentials are present.
385
370
 
386
371
  `agent-commerce validate` reports any of the above before anything starts, and
387
372
  `doctor` prints `LIVE MAINNET MODE - REAL FUNDS`.
@@ -404,17 +389,15 @@ INFO A2A disabled
404
389
  INFO ACP disabled
405
390
  INFO AP2 disabled
406
391
  PASS Payments x402 v2 (scheme=exact) enabled - LOCAL dev chain (eip155:84532, chain id shared with Base Sepolia), destination=0x7099…79C8, facilitator=local
407
- INFO Payments (MPP) planned - not implemented in this release
392
+ INFO Payments (MPP) MPP not configured
408
393
  PASS Storage sqlite schema v1 writable; receipts=2
409
394
  PASS Protocol versions reported by gateway /.well-known/agent-commerce
410
395
 
411
396
  Score: 7/7 checks passed
412
397
  ```
413
398
 
414
- That is real output, not an illustration. `doctor` also cross-checks the
415
- gateway's *live* settlement configuration against what your local config
416
- resolves to, and fails if they disagree - a diagnostic that passes while the
417
- system is misconfigured is worse than none.
399
+ `doctor` cross-checks the gateway's live settlement configuration against the
400
+ resolved local config and fails if they differ.
418
401
 
419
402
  Exits non-zero if anything fails. `--json` for machines.
420
403
 
@@ -452,12 +435,12 @@ See [CONTRIBUTING.md](CONTRIBUTING.md).
452
435
  **Now** - MCP, x402 v2, settlement on the local chain, Base Sepolia and Base
453
436
  mainnet, receipts, doctor, deterministic demo, experimental A2A v1.0.0 and ACP
454
437
  `2026-04-17` checkout adapters, experimental AP2 v0.2.0 mandate verification,
455
- and experimental OpenAPI import.
438
+ experimental MPP `charge` payments, and experimental OpenAPI import.
456
439
 
457
- **Next** - a `doctor` GitHub Action · UCP · MPP · autonomous-mode AP2 (open
458
- mandates, agent key binding, constraint evaluation) · more of ACP (carts, feed,
459
- delegated payment) · Shopify and WooCommerce examples · PostgreSQL · richer
460
- observability · multi-file and remote OpenAPI sources.
440
+ **Next** - a `doctor` GitHub Action · UCP ·
441
+ autonomous-mode AP2 (open mandates, agent key binding, constraint evaluation) ·
442
+ more of ACP (carts, feed, delegated payment) · Shopify and WooCommerce examples ·
443
+ PostgreSQL · richer observability · multi-file and remote OpenAPI sources.
461
444
 
462
445
  New protocols land only after the adapter model survives real use. Scope
463
446
  discipline is a release requirement, not a mood.
@@ -467,8 +450,8 @@ discipline is a release requirement, not a mood.
467
450
  | | |
468
451
  | ---------------------------------------------- | ------------------------------------------- |
469
452
  | [Architecture](docs/architecture.md) | how the pieces fit |
470
- | [Payment flow](docs/payment-flow.md) | the paid round trip, and every way it fails |
471
- | [Protocols](docs/protocols.md) | exactly what is and is not supported |
453
+ | [Payment flow](docs/payment-flow.md) | the paid round trip and failure paths |
454
+ | [Protocols](docs/protocols.md) | supported and unsupported protocol features |
472
455
  | [AP2](docs/ap2.md) | mandate verification and the trust model |
473
456
  | [Configuration](docs/configuration.md) | `config.yaml` reference |
474
457
  | [OpenAPI import](docs/openapi-import.md) | generate resources from an existing API |
package/dist/ap2.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { E as EnabledAp2Config } from './types-CJtwLu1c.js';
2
2
  export { A as AP2_CHECKOUT_MANDATE_VCT, a as AP2_CHECKOUT_PROFILE, b as AP2_DEFAULT_CLOCK_SKEW_SECONDS, c as AP2_MAX_CLOCK_SKEW_SECONDS, d as AP2_SIGNING_ALGORITHM, e as AP2_SPEC_VERSION, f as Ap2AuthorizationConfig, g as Ap2Mode, h as Ap2TrustedIssuer, i as Ap2TrustedKey } from './types-CJtwLu1c.js';
3
- import { A as AuthorizationProvider } from './authorization-CPDcpJVa.js';
4
- import { C as Clock, L as Logger } from './runtime-CKq3d_3K.js';
3
+ import { A as AuthorizationProvider } from './authorization-DA7mjy_O.js';
4
+ import { C as Clock, L as Logger } from './runtime-DC5XQ9Cc.js';
5
5
 
6
6
  /**
7
7
  * A private ES256 key: either a private JWK (the pair of the public one in the
@@ -139,8 +139,8 @@ interface Ap2ReplayStore {
139
139
  * The AP2 authorization provider: the seam between core's generic contract and
140
140
  * the mandate machinery.
141
141
  *
142
- * Owns the replay database, so the gateway owns this object's lifetime and
143
- * closes it on shutdown.
142
+ * The provider owns its replay store. The caller that creates the provider must
143
+ * call `close()`; `createGateway` does not close authorization providers.
144
144
  */
145
145
 
146
146
  interface Ap2AuthorizationProviderOptions {
@@ -151,7 +151,7 @@ interface Ap2AuthorizationProviderOptions {
151
151
  readonly replayStore?: Ap2ReplayStore;
152
152
  }
153
153
  interface Ap2AuthorizationProvider extends AuthorizationProvider {
154
- /** Closes the replay database */
154
+ /** Closes the replay store */
155
155
  close(): void;
156
156
  }
157
157
  /**
package/dist/ap2.js CHANGED
@@ -1,6 +1,6 @@
1
- import { AP2_CHECKOUT_MANDATE_VCT, AP2_SIGNING_ALGORITHM, AP2_DIGEST_ALGORITHM, AP2_CHECKOUT_PROFILE, AP2_SPEC_VERSION, AP2_KEY_TYPE, AP2_JWK_CURVE, openSqliteDatabase } from './chunk-GF6KHHKL.js';
2
- export { AP2_CHECKOUT_MANDATE_VCT, AP2_CHECKOUT_PROFILE, AP2_DEFAULT_CLOCK_SKEW_SECONDS, AP2_MAX_CLOCK_SKEW_SECONDS, AP2_SIGNING_ALGORITHM, AP2_SPEC_VERSION } from './chunk-GF6KHHKL.js';
3
- import { systemClock, NOOP_LOGGER, CommerceError, PACKAGE_VERSION } from './chunk-ZEGFLRGV.js';
1
+ import { AP2_CHECKOUT_MANDATE_VCT, AP2_SIGNING_ALGORITHM, AP2_DIGEST_ALGORITHM, AP2_CHECKOUT_PROFILE, AP2_SPEC_VERSION, AP2_KEY_TYPE, AP2_JWK_CURVE, openSqliteDatabase } from './chunk-US622BKF.js';
2
+ export { AP2_CHECKOUT_MANDATE_VCT, AP2_CHECKOUT_PROFILE, AP2_DEFAULT_CLOCK_SKEW_SECONDS, AP2_MAX_CLOCK_SKEW_SECONDS, AP2_SIGNING_ALGORITHM, AP2_SPEC_VERSION } from './chunk-US622BKF.js';
3
+ import { systemClock, NOOP_LOGGER, CommerceError, PACKAGE_VERSION } from './chunk-5GEWKNIH.js';
4
4
  import { SignJWT, importPKCS8, importJWK, jwtVerify } from 'jose';
5
5
  import canonicalize from 'canonicalize';
6
6
  import { decodeSdJwt, splitSdJwt, getClaims } from '@sd-jwt/core';
@@ -81,6 +81,15 @@ async function computeInputHash(input) {
81
81
  const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(canonical));
82
82
  return Buffer.from(digest).toString("base64url");
83
83
  }
84
+ var EVM_ADDRESS = /^0x[0-9a-f]{40}$/i;
85
+ function sameCoordinate(declared, expected) {
86
+ if (typeof declared === "string" && expected !== void 0) {
87
+ if (EVM_ADDRESS.test(declared) && EVM_ADDRESS.test(expected)) {
88
+ return declared.toLowerCase() === expected.toLowerCase();
89
+ }
90
+ }
91
+ return declared === expected;
92
+ }
84
93
  function asRecord(value) {
85
94
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
86
95
  }
@@ -115,7 +124,7 @@ async function bindMandateToPurchase(mandate, context, errorContext) {
115
124
  ]) {
116
125
  const declared = profile[claim];
117
126
  if (declared === void 0 && expected === void 0) continue;
118
- if (declared !== expected) throw ap2Rejected("purchase_mismatch", errorContext);
127
+ if (!sameCoordinate(declared, expected)) throw ap2Rejected("purchase_mismatch", errorContext);
119
128
  }
120
129
  return {
121
130
  resourceId: profile["resource_id"],
@@ -226,7 +235,6 @@ var AP2_UNSUPPORTED = [
226
235
  "digest algorithms other than sha-256",
227
236
  // Roles this gateway does not play
228
237
  "mandate issuance",
229
- "merchant checkout JWT issuance",
230
238
  "AP2 over the ACP checkout adapter"
231
239
  ];
232
240
  function buildAp2Descriptor(implementationVersion) {
@@ -277,17 +285,17 @@ function createAp2ReplayStore(options) {
277
285
  const reserveTx = db.transaction((request) => {
278
286
  const at = new Date(now()).toISOString();
279
287
  const existing = selectByReference.get(request.reference);
280
- if (existing !== void 0) {
281
- if (existing.state !== "released") {
282
- return { kind: "replayed", state: existing.state };
283
- }
284
- reReserve.run({ reference: request.reference, request_id: request.requestId, at });
285
- return { kind: "reserved" };
288
+ if (existing !== void 0 && existing.state !== "released") {
289
+ return { kind: "replayed", state: existing.state };
286
290
  }
287
291
  const sameCheckout = selectLiveByJti.get(request.checkoutJti, request.reference);
288
292
  if (sameCheckout !== void 0) {
289
293
  return { kind: "replayed", state: sameCheckout.state };
290
294
  }
295
+ if (existing !== void 0) {
296
+ reReserve.run({ reference: request.reference, request_id: request.requestId, at });
297
+ return { kind: "reserved" };
298
+ }
291
299
  insert.run({
292
300
  reference: request.reference,
293
301
  checkout_jti: request.checkoutJti,