@devlab.group/agent-commerce 1.4.0 → 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,41 +67,46 @@ 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' });
80
+ const store = receipts({ path: './receipts.sqlite' });
81
+ // Initialize before use so an incompatible SQLite schema fails at startup
82
+ await store.init();
83
+
82
84
  const gateway = await createGateway({
83
85
  config,
84
- store: receipts({ path: './receipts.sqlite' }),
86
+ store,
85
87
  paymentProviders: [],
86
88
  protocolAdapters: [],
87
89
  });
88
90
  const { url } = await gateway.listen();
89
91
  ```
90
92
 
91
- ### Optional peers - install only the rails you use
93
+ ### Optional peers
92
94
 
93
- The MCP adapter, the x402 provider and AP2 verification live on their own
94
- subpaths, because each needs a dependency the rest of the package does not -
95
- the x402 rail brings the whole EVM signing and RPC stack, which a gateway
96
- 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.
97
97
 
98
98
  | You want | Install | Import |
99
99
  | --------------------------------------- | ---------------------------------- | ------------------------------------------ |
100
100
  | gateway, config, receipts, CLI | `@devlab.group/agent-commerce` | `from '@devlab.group/agent-commerce'` |
101
101
  | expose resources as MCP tools | `+ @modelcontextprotocol/sdk` | `from '@devlab.group/agent-commerce/mcp'` |
102
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'` |
103
104
  | verify AP2 mandates, sign checkout JWTs | `+ jose @sd-jwt/core canonicalize` | `from '@devlab.group/agent-commerce/ap2'` |
104
105
  | authenticate to a CDP facilitator | `+ @coinbase/x402` | (no import - loaded on demand) |
105
106
 
107
+ MPP settles through an x402 facilitator, so its entry also needs the x402
108
+ peers.
109
+
106
110
  ```bash
107
111
  npm install @devlab.group/agent-commerce @modelcontextprotocol/sdk @x402/core @x402/evm viem
108
112
  ```
@@ -113,23 +117,17 @@ import { x402 } from '@devlab.group/agent-commerce/x402';
113
117
  import { ap2 } from '@devlab.group/agent-commerce/ap2';
114
118
  ```
115
119
 
116
- The AP2 three are small - about 1.3 MB installed between them, against roughly
117
- 63 MB for the x402 stack - but they stay optional on the same principle: a
118
- deployment that gates nothing on a mandate should not carry a JOSE stack and an
119
- 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.
120
122
 
121
123
  Peers are pinned exactly: x402's schemas and EIP-712 domains cross this
122
124
  boundary, so a version skew is a correctness problem rather than a convenience
123
- one. Import a subpath without its
124
- peer installed and Node fails at load naming the missing package - deliberately,
125
- 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.
126
127
 
127
- `@coinbase/x402` is the odd one out: it has no import of its own and is loaded
128
- dynamically, only when `facilitator.auth.type: cdp` is configured. It is worth
129
- avoiding if you can - it brings `@coinbase/cdp-sdk` and `axios`, which carry
130
- high-severity advisories, while the package itself and the other three peers
131
- audit clean. `auth.type: bearer` covers any facilitator with a static token and
132
- 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.
133
131
 
134
132
  ## Quickstart
135
133
 
@@ -149,15 +147,13 @@ npm run agent-commerce -- doctor --config config-demo.yaml # verify the whole st
149
147
  npm run demo:agent # watch an agent buy something
150
148
  ```
151
149
 
152
- <sub><b>Linux only</b>, and only if your user is not UID/GID 1000 (check with
153
- <code>id -u && id -g</code>): export <code>DOCKER_UID=$(id -u) DOCKER_GID=$(id -g)</code>
154
- before <code>docker compose up</code>. The chain-deploy step runs as that user so the
155
- deployment manifest it writes stays host-writable rather than root-owned. Docker
156
- Desktop on macOS and Windows translates permissions through its VM and does not
157
- 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>
158
154
 
159
- That is the whole thing. The stack is a private Anvil chain, a mock USDC token,
160
- 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.
161
157
 
162
158
  To stop and wipe state: `docker compose down -v`.
163
159
 
@@ -168,7 +164,7 @@ To stop and wipe state: `docker compose down -v`.
168
164
  │ AI Agent │
169
165
  └───────────────────────────────────────┬──────────────────────────────────────┘
170
166
  │ MCP · A2A · ACP · HTTP
171
- │ PAYMENT-SIGNATURE
167
+ │ payment proof
172
168
  │ Agent-Authorization
173
169
  ┌───────────────────────────────────────▼──────────────────────────────────────┐
174
170
  │ Agent Commerce Gateway (yours) │
@@ -178,7 +174,7 @@ To stop and wipe state: `docker compose down -v`.
178
174
  │ ┌────────────────────┬──────────────┴──┬────────────────┐ │
179
175
  │ ▼ ▼ ▼ ▼ │
180
176
  │ AuthorizationProvider PaymentProvider BackendExecutor ReceiptStore │
181
- │ (ap2) (x402) (bounded HTTP) (SQLite) │
177
+ │ (ap2) (x402, mpp) (bounded HTTP) (SQLite) │
182
178
  └───────────────────────────────────────┬──────────────────────────────────────┘
183
179
  │
184
180
  ┌──────────▼─────────┐
@@ -186,11 +182,10 @@ To stop and wipe state: `docker compose down -v`.
186
182
  └────────────────────┘
187
183
  ```
188
184
 
189
- Every protocol adapter converges on **one execution pipeline**. That is what
190
- makes payment enforcement a property of the system rather than something each
191
- adapter has to remember. Money never passes through the box: the buyer pays the
192
- merchant directly on chain, and the gateway holds neither the funds nor a key.
193
- 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).
194
189
 
195
190
  ## Configure a resource
196
191
 
@@ -225,57 +220,56 @@ Already have an OpenAPI description? Generate the resources from it
225
220
  agent-commerce import openapi ./openapi.yaml
226
221
  ```
227
222
 
228
- It writes a reviewable `resources:` fragment - path, query and JSON body
229
- mapped, schemas converted to what the gateway actually enforces - and
230
- deliberately leaves `pricing` and `expose` out, because an OpenAPI document has
231
- no opinion on what an operation costs or who may see it. Credentials are never
232
- 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.
233
227
 
234
228
  See [docs/configuration.md](docs/configuration.md).
235
229
 
236
230
  ## Protocol support
237
231
 
238
- | Protocol | Status | Pinned revision |
239
- | --------- | ------------ | -------------------------------------------------------- |
240
- | **MCP** | Supported | `@modelcontextprotocol/sdk@1.30.0` |
241
- | **x402** | Supported | x402 v2 (`@x402/core`, `@x402/evm`), scheme `exact`, EVM |
242
- | **HTTP** | Supported | native routes |
243
- | **A2A** | Experimental | A2A v1.0.0, binding `JSONRPC`, method `SendMessage` |
244
- | **ACP** | Experimental | ACP `2026-04-17`, REST checkout + discovery |
245
- | **AP2** | Experimental | AP2 `v0.2.0`, Direct Checkout Mandate verification |
246
- | UCP · MPP | Planned | - |
247
-
248
- [AP2](docs/ap2.md) is in that table because people look there, but it is an **authorization**
249
- method rather than a transport: it gates settlement on a resource that still
250
- takes a real payment, and it is the verifying half only - the gateway holds no
251
- signing key and issues no Checkout Receipt.
252
-
253
- "Planned" means **no code ships for it**. "Experimental" means the code ships,
254
- is tested against the protocol's own official artifacts, and serves a narrow
255
- named subset: [A2A](docs/protocols.md#a2a) and [ACP](docs/protocols.md#acp) are
256
- both off by default and documented in full there, as are
257
- [MCP](docs/protocols.md#mcp) and [x402](docs/protocols.md#x402). ACP serves the
258
- five stable checkout operations and advertises `services: ["checkout"]` and
259
- nothing more; its `payment_data` stays with the merchant's own checkout and is
260
- never turned into an x402 payment.
261
-
262
- Each adapter reports its own `supportedSpec`, `capabilities` and `unsupported`
263
- list at runtime through `GET /.well-known/agent-commerce` and
264
- `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.
265
257
 
266
258
  ## Payment model
267
259
 
268
260
  - **Non-custodial.** The gateway never holds funds, and never asks for a
269
261
  merchant or buyer private key. `payTo` is your address.
270
262
  - **Fail closed.** Missing, malformed, expired, replayed, wrong-amount,
271
- wrong-recipient, wrong-network and wrong-asset payments all fail - each with a
272
- test.
263
+ wrong-recipient, wrong-network and wrong-asset payments are rejected.
273
264
  - **Replay-safe twice over.** EIP-3009 stops a double spend on-chain; the
274
265
  gateway additionally reserves a `replayKey` derived from the authorisation
275
266
  before it settles anything.
276
- - **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
277
268
  falls and the merchant's rises by exactly the price, with a real transaction
278
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.
279
273
  - **Authorization is separate from payment.** A resource can also require an
280
274
  AP2 mandate, verified before settlement and spendable exactly once. It never
281
275
  moves money and never unlocks a resource on its own - the payment still has
@@ -288,19 +282,20 @@ not this process. No code changes, and no "live mode" to switch on.
288
282
 
289
283
  ### It has actually settled
290
284
 
291
- Not a roadmap entry. Both of these moved 0.01 USDC from a buyer to a merchant
292
- through a remote facilitator:
285
+ These transactions each moved 0.01 USDC from a buyer to a merchant through a
286
+ remote facilitator:
293
287
 
294
- | Network | Transaction |
295
- | ------------ | --------------------------------------------------------------------------------------------------------------------- |
296
- | Base Sepolia | [`0xea41b234c4…`](https://sepolia.basescan.org/tx/0xea41b234c4645a4d335589ec9753646aa7cccd1b97e9e15823b88bff7b54a247) |
297
- | 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) |
298
294
 
299
- In both, the gateway held no key, signed nothing and paid no gas - the buyer
300
- signed an EIP-3009 authorisation offline holding no ETH, and the facilitator
301
- broadcast it. Each run reads the buyer and merchant balances and the
302
- transaction receipt back off the chain afterwards; the gateway's own report of
303
- 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.
304
299
 
305
300
  Reproduce with `npm run test:testnet` / `npm run test:mainnet` - both spend
306
301
  real funds, skip themselves without credentials, and never run in CI.
@@ -316,17 +311,14 @@ gateway on a public network.
316
311
  | `local` | this process, with an Anvil dev key | the local dev chain only |
317
312
  | `remote` | an HTTP facilitator you point at | anywhere |
318
313
 
319
- With `remote`, the gateway holds **no signing key at all**. The buyer signs an
320
- EIP-3009 authorisation offline - no ETH required - and the facilitator pays the
321
- gas. A facilitator cannot redirect your money: the authorisation names its
322
- recipient, amount and chain, so it can broadcast exactly that transfer or
323
- nothing. What it can do is see every authorisation you handle, and stop
324
- 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.
325
318
 
326
- Three auth types: `none`, `bearer` (a static token, installs nothing) and `cdp`
327
- (Coinbase Developer Platform, which signs a fresh JWT per request). Anything
328
- else is refused at config load rather than sent nothing. You can also run your
329
- 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.
330
322
 
331
323
  ### Base Sepolia
332
324
 
@@ -360,8 +352,7 @@ together, and reported by `doctor`, `health()` and `/.well-known`.
360
352
 
361
353
  ### Base mainnet
362
354
 
363
- Real funds, so nothing is defaulted. Every one of these is checked at config
364
- load, and the gateway will not start without them:
355
+ Mainnet moves real funds. Config validation requires:
365
356
 
366
357
  | Required | |
367
358
  | ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
@@ -369,13 +360,13 @@ load, and the gateway will not start without them:
369
360
  | `facilitator.mode: remote` | `local` needs a funded gas key inside this process |
370
361
  | an HTTPS `facilitator.url` | |
371
362
  | `allowUnauthenticatedFacilitator: true` | only if that facilitator takes no credential |
372
- | a non-development `payTo` | |
373
- | `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 |
374
365
 
375
366
  Full config in [`examples/base-mainnet/`](examples/base-mainnet/), and
376
367
  [`examples/base-mainnet-payai/`](examples/base-mainnet-payai/) for an
377
- unauthenticated facilitator. `npm run test:mainnet` proves it end to end and
378
- 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.
379
370
 
380
371
  `agent-commerce validate` reports any of the above before anything starts, and
381
372
  `doctor` prints `LIVE MAINNET MODE - REAL FUNDS`.
@@ -398,17 +389,15 @@ INFO A2A disabled
398
389
  INFO ACP disabled
399
390
  INFO AP2 disabled
400
391
  PASS Payments x402 v2 (scheme=exact) enabled - LOCAL dev chain (eip155:84532, chain id shared with Base Sepolia), destination=0x7099…79C8, facilitator=local
401
- INFO Payments (MPP) planned - not implemented in this release
392
+ INFO Payments (MPP) MPP not configured
402
393
  PASS Storage sqlite schema v1 writable; receipts=2
403
394
  PASS Protocol versions reported by gateway /.well-known/agent-commerce
404
395
 
405
396
  Score: 7/7 checks passed
406
397
  ```
407
398
 
408
- That is real output, not an illustration. `doctor` also cross-checks the
409
- gateway's *live* settlement configuration against what your local config
410
- resolves to, and fails if they disagree - a diagnostic that passes while the
411
- 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.
412
401
 
413
402
  Exits non-zero if anything fails. `--json` for machines.
414
403
 
@@ -446,12 +435,12 @@ See [CONTRIBUTING.md](CONTRIBUTING.md).
446
435
  **Now** - MCP, x402 v2, settlement on the local chain, Base Sepolia and Base
447
436
  mainnet, receipts, doctor, deterministic demo, experimental A2A v1.0.0 and ACP
448
437
  `2026-04-17` checkout adapters, experimental AP2 v0.2.0 mandate verification,
449
- and experimental OpenAPI import.
438
+ experimental MPP `charge` payments, and experimental OpenAPI import.
450
439
 
451
- **Next** - a `doctor` GitHub Action · UCP · MPP · autonomous-mode AP2 (open
452
- mandates, agent key binding, constraint evaluation) · more of ACP (carts, feed,
453
- delegated payment) · Shopify and WooCommerce examples · PostgreSQL · richer
454
- 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.
455
444
 
456
445
  New protocols land only after the adapter model survives real use. Scope
457
446
  discipline is a release requirement, not a mood.
@@ -461,8 +450,8 @@ discipline is a release requirement, not a mood.
461
450
  | | |
462
451
  | ---------------------------------------------- | ------------------------------------------- |
463
452
  | [Architecture](docs/architecture.md) | how the pieces fit |
464
- | [Payment flow](docs/payment-flow.md) | the paid round trip, and every way it fails |
465
- | [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 |
466
455
  | [AP2](docs/ap2.md) | mandate verification and the trust model |
467
456
  | [Configuration](docs/configuration.md) | `config.yaml` reference |
468
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-7NKWFGBY.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-7NKWFGBY.js';
3
- import { systemClock, NOOP_LOGGER, CommerceError, PACKAGE_VERSION } from './chunk-ZDVJ7EVL.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,