Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
5a1c351
refactor(x402): isolate mechanism peers and facilitator transport
Revinand Oct 8, 2026
626d933
feat(x402): add family-aware network options and resource offers
Revinand Oct 8, 2026
706e62a
feat(x402): give the local Anvil chain its own id eip155:31337
Revinand Oct 8, 2026
6224afc
feat(x402): select trusted offers before authorization and settlement
Revinand Oct 8, 2026
c3ba823
feat(x402): support configured EVM chains with EIP-3009
Revinand Oct 8, 2026
ec415bd
feat(x402): add exact Permit2 settlement options
Revinand Oct 8, 2026
2c0945c
feat(x402): support exact Solana settlement and replay identity
Revinand Oct 8, 2026
43fd0c5
feat(x402): support NEAR token offers and access-key replay protection
Revinand Oct 9, 2026
0d7c6a5
feat(storage): let an x402 retry take over a rejected attempt
Revinand Oct 9, 2026
00ab8b3
feat(mpp): settle MPP payments before the backend by default
Revinand Oct 9, 2026
aa21445
refactor: split oversized modules into focused files
Revinand Oct 9, 2026
3e6a9a7
docs: split protocols.md into per-protocol pages
Revinand Oct 9, 2026
af389ee
feat(gateway): expose and diagnose selected multi-network payments
Revinand Oct 9, 2026
9bb5198
feat(x402): let a resource list its own offers
Revinand Oct 9, 2026
a043645
feat(gateway): report each x402 offer's health
Revinand Oct 9, 2026
90c5679
test(x402): add multi-family public-network smoke harnesses
Revinand Oct 9, 2026
7d399e4
docs(x402): record multi-network contracts and conformance matrix
Revinand Oct 10, 2026
1ba4804
fix(x402): classify settlement outcomes and refusals by what moved
Revinand Oct 10, 2026
c6cfc06
feat(storage): keep each attempt a reclaim replaces
Revinand Oct 10, 2026
9652c8e
fix(x402): validate EIP-3009 fields and EVM addresses in one place
Revinand Oct 10, 2026
81d687b
fix(x402): catch an unpayable Solana offer and a stalled facilitator
Revinand Oct 10, 2026
858936a
fix: answer overload and provider outages with retryable errors
Revinand Oct 11, 2026
a419ba3
fix(config): refuse configurations that charge the wrong token or can…
Revinand Oct 11, 2026
4e1ce04
Merge branch 'master' into feature/x402-multi-network
Revinand Oct 11, 2026
62ed13f
chore: update the OSS Scanner threat model for multi-network x402
Revinand Oct 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ MERCHANT_API_BASE_URL=http://localhost:3000
RECEIPT_STORE_PATH=./data/receipts.sqlite

# x402 / local deterministic chain
X402_NETWORK=eip155:84532
X402_NETWORK=eip155:31337
X402_RPC_URL=http://localhost:8545

# The six variables below come from .deploy/local.json, which
Expand Down
30 changes: 27 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@ env:
# Keep this version in sync with the Foundry image digest in
# docker/chain.Dockerfile. A floating `nightly` would change test inputs.
FOUNDRY_VERSION: 'v1.7.1'
# The Solana E2E starts solana-test-validator. The Agave release is pinned by
# version and by the SHA-256 GitHub records for the Linux archive.
AGAVE_VERSION: 'v4.1.1'
AGAVE_SHA256: 'a5c8e74b8ffa9ce906872b812849057c7fb21cf036ba08f219eb335e20fa4fb3'
# NEAR E2E tests use nearcore's sandbox. No archive digest is published;
# this SHA-256 was recorded when the version was pinned.
NEAR_SANDBOX_VERSION: '2.13.4'
NEAR_SANDBOX_SHA256: '5690a4635172263cf026683f75965cac09dd916912584f12709974fed620163f'

jobs:
quality:
Expand Down Expand Up @@ -96,9 +104,25 @@ jobs:
- uses: foundry-rs/foundry-toolchain@908c540300062bd5a7e473851cdb4282204cee09 # v1.9.1
with:
version: ${{ env.FOUNDRY_VERSION }}
- name: Install the Agave CLI
run: |
archive="$RUNNER_TEMP/agave.tar.bz2"
curl -sSfL -o "$archive" \
"https://github.com/anza-xyz/agave/releases/download/${AGAVE_VERSION}/solana-release-x86_64-unknown-linux-gnu.tar.bz2"
echo "${AGAVE_SHA256} $archive" | sha256sum -c -
tar -xjf "$archive" -C "$RUNNER_TEMP"
echo "$RUNNER_TEMP/solana-release/bin" >> "$GITHUB_PATH"
- name: Install the NEAR sandbox
run: |
archive="$RUNNER_TEMP/near-sandbox.tar.gz"
curl -sSfL -o "$archive" \
"https://s3-us-west-1.amazonaws.com/build.nearprotocol.com/nearcore/Linux-x86_64/${NEAR_SANDBOX_VERSION}/near-sandbox.tar.gz"
echo "${NEAR_SANDBOX_SHA256} $archive" | sha256sum -c -
tar -xzf "$archive" -C "$RUNNER_TEMP"
echo "$RUNNER_TEMP/Linux-x86_64" >> "$GITHUB_PATH"
- run: npm ci
# These tests start local Anvil, deploy MockUSDC and run local services;
# they do not need a public RPC or hosted facilitator.
# Tests run Anvil, solana-test-validator and the NEAR sandbox locally,
# with deployed or preloaded tokens and no public RPC or facilitator.
- run: npm run test:e2e

package:
Expand Down Expand Up @@ -188,7 +212,7 @@ jobs:
// Subpaths must share CommerceError with the main entry; separate
// bundled copies would fail the instanceof check.
let caught;
try { x402({ asset: 'nope', payTo: '0x0', network: 'eip155:84532' }); } catch (e) { caught = e; }
try { x402({ asset: 'nope', payTo: '0x0', network: 'eip155:31337' }); } catch (e) { caught = e; }
if (!(caught instanceof CommerceError)) throw new Error('CommerceError identity broken across subpaths');
console.log('subpath install ok: mcp + x402 compose with the main entry');
"
Expand Down
38 changes: 22 additions & 16 deletions .oss-scanner/threat_model.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,33 +4,35 @@ The full security notes are in the repository's `SECURITY.md` and `docs/security

## What this project does and where untrusted input enters

Agent Commerce Gateway is a self-hosted TypeScript gateway that sits in front of a merchant's existing HTTP API and sells calls to it to AI agents. Agents reach it over plain HTTP (`/api/resources/:id/invoke`), MCP (`/mcp`), A2A (`/a2a`) and, when enabled, ACP checkout routes. Paid resources take x402 v2 (`exact`, EVM / EIP-3009) or MPP payments, and can also require an AP2 Checkout Mandate. The gateway never holds merchant or buyer funds: payments go from the buyer to the merchant's configured address.
Agent Commerce Gateway is a self-hosted TypeScript gateway that sits in front of a merchant's existing HTTP API and sells calls to it to AI agents. Agents reach it over plain HTTP (`/api/resources/:id/invoke`), MCP (`/mcp`), A2A (`/a2a`) and, when enabled, ACP checkout routes. Paid resources take x402 v2 `exact` payments (EVM with EIP-3009 or Permit2, Solana, and NEAR as partial support) or MPP payments, and can also require an AP2 Checkout Mandate. A resource can offer several networks or tokens; the gateway settles the offer the proof pays. Solana and NEAR settle only through a remote facilitator. The gateway never holds merchant or buyer funds: payments go from the buyer to the merchant's configured address.

- **Untrusted:** everything a caller sends.
- HTTP, MCP JSON-RPC, A2A JSON-RPC and ACP request bodies.
- Path parameters, query strings and resource inputs.
- `Host` and `Origin` headers.
- Payment proofs: x402 `PAYMENT-SIGNATURE`, MPP `Authorization: Payment …` credentials, and the `_payment` field on MCP and A2A.
- Payment proofs: x402 `PAYMENT-SIGNATURE`, MPP `Authorization: Payment …` credentials, the `_payment` field on MCP and A2A, and `x402.payment.payload` in A2A x402 extension messages. An x402 proof carries an EIP-3009 or Permit2 authorization, a partially signed Solana transaction or a signed NEAR delegate action, plus the offer the buyer says it accepted.
- AP2 mandates (SD-JWT).
- **Semi-trusted:**
- responses from the payment facilitator and the RPC node;
- responses from the payment facilitator: verify and settle results, whose reason strings are sanitized, and `/supported`, which supplies the Solana fee payer;
- the EVM, Solana and NEAR RPC nodes, read for health checks and by MPP's nonce check, and Anvil, which settles local payments;
- responses from the merchant backend (the built-in executor enforces a timeout and a 1 MiB body cap).
- **Trusted:** the operator's configuration file and environment, including backend URLs, admin and ACP tokens, keys and facilitator settings.
- **Trusted:** the operator's configuration file and environment, including backend URLs, admin and ACP tokens, keys, facilitator settings and declared EVM networks, whose `kind` (testnet or mainnet) the operator asserts.

## Components that matter most / least

- **Most important:**
- **The payment path:** `src/core` (request pipeline), `src/payments/x402`, `src/payments/mpp`.
- Verify, then replay reservation, then settle, then deliver.
- Every failure must fail closed.
- Delivery must happen at most once per payment authorization.
- **The payment path:** `src/core` (request pipeline), `src/payments/x402` (offer selection, verification and settlement classification per chain family in `provider/`, replay keys in `replay-key.ts`), `src/payments/mpp`.
- Select the offer the proof pays, verify, reserve the replay key, then settle before the response is released: before the backend call under `upfront` (MPP's default), after it under `authorization` (x402's default).
- Every failure must fail closed. A settlement whose outcome is unknown withholds the response and keeps the replay key reserved (`settlement-uncertain`).
- Delivery must happen at most once per payment authorization. Replay keys follow what each chain consumes: the EIP-3009 nonce, the Permit2 owner nonce, the Solana transaction message and the NEAR access-key nonce.
- A facilitator or RPC failure must not be recorded as the buyer's refusal.
- **AP2 verification:** `src/authorization/ap2`.
- **Protocol adapters:** `src/protocols/mcp`, `src/protocols/a2a`, `src/protocols/acp`, `src/protocols/http.ts`.
- **Access control and routing:** `src/gateway`.
- Host-header (DNS-rebinding) validation in `access-control.ts`.
- The admin token on `/api/receipts` and `/api/events`.
- The ACP bearer token.
- **Storage** (`src/storage`): receipts, events, replay keys.
- **Storage** (`src/storage`): receipts, events, payment attempts with their replay keys, and the history of attempts a retry reclaimed.
- **Secret redaction** in logs and receipts.
- **Lower priority:**
- `src/cli` (operator tooling);
Expand All @@ -39,19 +41,20 @@ Agent Commerce Gateway is a self-hosted TypeScript gateway that sits in front of
- **Out of scope:**
- `demo/`, `examples/`, `scripts/`, `contracts/` (local demo chain);
- the deliberately public Anvil development keys;
- the internals of third-party SDKs (x402, MCP SDK, mppx). How the gateway uses them is in scope.
- the internals of third-party SDKs (`@x402/*`, `@solana/kit`, the MCP SDK, mppx). How the gateway uses them is in scope.

## How to exercise it

- `npm test` runs the unit, integration and conformance suites (Vitest). The local-chain suites spawn Anvil, which this image installs.
- `npm test` runs the unit, integration and conformance suites (Vitest). A few of them spawn Anvil, which this image installs.
- `npm run test:e2e` settles payments on Anvil, `solana-test-validator` and a NEAR sandbox. This image installs only Anvil, so the Solana and NEAR suites skip themselves.
- `tests/conformance/` drives the MCP, A2A and ACP adapters with the official client SDKs.
- `config-demo.yaml` describes a complete local setup. `npm run agent-commerce -- doctor --config config-demo.yaml` checks it.

## How we rate severity

- **Critical:**
- a paid resource delivered without a successfully verified and settled payment;
- a payment that settles to a recipient, amount, asset or network other than the configured one;
- a payment that settles to a recipient, amount, asset or network other than one of the resource's configured offers;
- one payment authorization or AP2 mandate unlocking more than one delivery;
- an AP2 requirement bypassed;
- any path that discloses a private key.
Expand All @@ -60,17 +63,20 @@ Agent Commerce Gateway is a self-hosted TypeScript gateway that sits in front of
- an ACP bearer-token bypass;
- a bypass of the Host-header validation;
- credentials, payment proofs or tokens written to logs or receipts despite redaction;
- code execution or path traversal reachable from untrusted input.
- **Medium:** denial of service from unauthenticated input that gets past the configured limits (body size, timeouts, batch handling); disclosure of non-secret configuration.
- code execution or path traversal reachable from untrusted input;
- a buyer asked to pay again after a settlement that may have landed, such as an unknown outcome recorded as refused.
- **Medium:** denial of service from unauthenticated input that gets past the configured limits (body size, timeouts, batch handling, the 256-task A2A pending-payment store); disclosure of non-secret configuration.
- **Low:** findings that need a malicious or careless operator configuration; overly detailed error messages without secrets.

## Anything to leave alone

`SECURITY.md` → "What the gateway does not protect against" lists behavior that is documented and intended. Please do not report it:
`SECURITY.md` → "What the gateway does not protect against" and the protocol pages under `docs/protocols/` list behavior that is documented and intended. Please do not report it:
- SSRF through backend URLs the operator configured;
- no buyer vetting or KYC;
- irreversible on-chain settlement;
- the documented divergence between payment and delivery in the `upfront` and `authorization` flows;
- reserved authorizations never being released;
- an authorization staying reserved after a settlement whose outcome is unknown; only a rejected x402 attempt can be reserved again, and the chain still spends it once;
- NEAR network binding through the offer and the facilitator's RPC only, since the delegate signature names no chain;
- unpaid A2A extension tasks living in process memory, so a restart or another instance loses them;
- a compromised host;
- the demo dashboard's `VITE_ADMIN_TOKEN` placeholder.
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ moves money and never unlocks a resource on its own. Required before review:
rejection reason that is not an oracle for trust policy
- [ ] nothing of the proof itself in a receipt, an event or a log

AP2 is the worked example: [`docs/ap2.md`](docs/ap2.md).
AP2 is the worked example: [`docs/protocols/ap2.md`](docs/protocols/ap2.md).

## Publishing

Expand Down
Loading
Loading