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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"name": "agent-commerce",
"owner": { "name": "devlab-group", "url": "https://github.com/devlab-group" },
"description": "The Agent Commerce Gateway skill for coding agents.",
"plugins": [
{
"name": "agent-commerce",
"source": ".",
"description": "Integrate the Agent Commerce Gateway into an existing HTTP API: configure, start and verify it."
}
]
}
10 changes: 10 additions & 0 deletions .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"name": "agent-commerce",
"version": "2.1.0",
"description": "Put the Agent Commerce Gateway in front of an existing HTTP API so AI agents can call and pay for it with x402.",
"author": { "name": "devlab-group", "url": "https://github.com/devlab-group" },
"homepage": "https://github.com/devlab-group/agent-commerce#readme",
"repository": "https://github.com/devlab-group/agent-commerce",
"license": "Apache-2.0",
"keywords": ["x402", "mcp", "ai-agents", "payments", "agentic-commerce"]
}
57 changes: 57 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,40 @@ jobs:
npx agent-commerce version | grep -q '@modelcontextprotocol/server'
npx agent-commerce init --yes
npx agent-commerce validate
# `start` loads each enabled feature's peers, so the init config,
# which enables x402, must refuse and name the missing package
status=0
timeout 30 npx agent-commerce start > start.log 2>&1 || status=$?
cat start.log
test "$status" -eq 1
grep -q 'payments.x402 is enabled, but the package' start.log
# Peers load before the receipt store opens. Nothing was created
test ! -e data/receipts.db
# Free HTTP needs no peers with MCP, AP2 and payment rails disabled
cat > free.yaml <<'EOF'
version: 1
merchant: { id: free, name: Free, publicBaseUrl: "http://localhost:8081" }
server: { port: 8081, host: 127.0.0.1 }
storage: { receipts: { driver: sqlite, path: ./free.sqlite } }
protocols: { http: { enabled: true }, mcp: { enabled: false, mountPath: /mcp } }
resources:
hello:
name: Hello
input: { type: object, properties: {}, additionalProperties: false }
backend: { type: http, method: GET, url: "http://127.0.0.1:3000/hello" }
pricing: { type: free }
expose: [http]
payments: {}
EOF
./node_modules/.bin/agent-commerce start --config free.yaml &
pid=$!
for _ in $(seq 30); do
node -e "fetch('http://127.0.0.1:8081/health').then((r) => process.exit(r.ok ? 0 : 1), () => process.exit(1))" && break
sleep 1
done
node -e "fetch('http://127.0.0.1:8081/health').then((r) => { if (!r.ok) process.exit(1); })"
kill -TERM "$pid"
wait "$pid"
# The main entry must compose and serve without optional peers
node --input-type=module -e "
import { createGateway, loadConfig, receipts } from '@devlab.group/agent-commerce';
Expand Down Expand Up @@ -216,6 +250,29 @@ jobs:
if (!(caught instanceof CommerceError)) throw new Error('CommerceError identity broken across subpaths');
console.log('subpath install ok: mcp + x402 compose with the main entry');
"
# With its peers installed, `start` serves the init config's paid
# resource: a 402 challenge, which needs no chain
./node_modules/.bin/agent-commerce start &
pid=$!
for _ in $(seq 30); do
node -e "fetch('http://127.0.0.1:8080/health').then((r) => process.exit(r.ok ? 0 : 1), () => process.exit(1))" && break
sleep 1
done
node -e "
fetch('http://127.0.0.1:8080/api/resources/report/invoke', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: '{}',
}).then((r) => {
if (r.status !== 402 || !r.headers.has('payment-required')) {
console.error('invoke -> ' + r.status);
process.exit(1);
}
console.log('start ok: paid resource challenged with 402');
});
"
kill -TERM "$pid"
wait "$pid"

build:
name: Build and quickstart test
Expand Down
38 changes: 34 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,12 +70,24 @@ agent-commerce doctor
```

Requires **Node >= 22**. The package includes the `agent-commerce` CLI
(`init`, `validate`, `doctor`, `demo`, `import openapi`, `version`) and a library
for embedding the gateway.
(`init`, `validate`, `start`, `doctor`, `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` and `doctor` 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';

Expand Down Expand Up @@ -135,6 +147,24 @@ missing package.
`facilitator.auth.type: cdp` and brings the larger CDP dependency tree. A
facilitator with a static token can use `auth.type: bearer` without it.

## Integrate with an AI coding agent

The repository includes an experimental [Agent Skills](https://agentskills.io)
skill, [`skills/agent-commerce`](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@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
Expand Down Expand Up @@ -281,8 +311,8 @@ Minor releases may break experimental configurations, integrations or types.
This applies to MPP, A2A, ACP, AP2 and `agent-commerce import openapi`.
Runtime adapters and providers report `status: experimental` in
`GET /.well-known/agent-commerce`. Pin an exact version and test upgrades
before deploying them. 2.1 also changes some supported x402 behavior and
types; see [Upgrading from 2.0](docs/migration-2.1.md).
before deploying them. 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

Expand Down
3 changes: 3 additions & 0 deletions config.example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,9 @@ resources:
currency: USDC
expose: [http, mcp]
payments: [x402]
# What the backend returns, declared in each x402 challenge before payment;
# an HTTP delivery keeps the backend's own Content-Type when it is text
# mimeType: application/json

payments:
x402:
Expand Down
17 changes: 15 additions & 2 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,14 @@ fall back after a failure. Config load rejects a paid resource with no enabled
listed rail. A resource can replace the x402 rail's offers with
[its own](#a-resources-own-offers).

`mimeType` names what the backend returns, such as `text/html` or
`text/csv; header=present`. Each x402 challenge for the resource declares it as
`resource.mimeType` before the buyer pays, `application/json` when unset. An
HTTP delivery then carries the backend's own `Content-Type` when it is text and
fits the body (see [security.md](security.md#backend-response-relay)); this
setting does not change it. Config load accepts `mimeType` only on a fixed-price
resource that lists `x402` in `payments`.

`url` supports `{param}` templating from validated input; values are
URL-encoded. Remaining input becomes query string for `GET`/`DELETE` and a JSON
body otherwise - unless `backend.inputBindings` says otherwise, below.
Expand Down Expand Up @@ -708,12 +716,17 @@ serves, including the A2A Agent Card and ACP discovery paths · an ACP checkout
mapping that is incomplete, names a missing or non-`acp` resource, reuses one
resource for two operations, or maps a paid one · an ACP idempotency retention
below 24 hours · an invalid or zero `payTo`/`asset`/`recipient` · an
`x402.accepts` list that is empty or on a resource that cannot use it · an MPP
`challengeSecret` with length below 32 or a multi-line `realm`.
`x402.accepts` list that is empty or on a resource that cannot use it · a
`mimeType` that is not a media type, or on a resource that is not fixed-price
with `x402` in `payments` · an MPP `challengeSecret` with length below 32 or a
multi-line `realm`.

It exits non-zero on any of them.

## Environment

`${VAR}` works in any string value, and `${VAR:-default}` supplies a fallback.
`agent-commerce start`, `validate` and `doctor` also read variables from a file
with `--env-file <path>`; a variable already set in the environment wins over
the file.
See [`.env.example`](../.env.example) for the variables the demo uses.
44 changes: 29 additions & 15 deletions docs/contracts.md
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,14 @@ persistence must not fail the commerce flow.
- `CreatePaymentProofOptions` dropped the deprecated `rpcUrl`, which the helper
ignored. TypeScript rejects it on a fresh object literal; callers passing a
variable with extra properties should remove it as well.
- A resource's `mimeType` sets the media type its x402 challenges declare as
`resource.mimeType`; `application/json` remains the default.
`X402ProviderOptions.resourceMimeTypes` and
`GatewayConfig.payments.x402.resourceMimeTypes` carry it by resource id.
- `POST /api/resources/:id/invoke` delivers the backend's `Content-Type`, with
`charset=utf-8`, when it is a text type that fits the relayed body; text no
longer always goes out as `text/plain`. Every text delivery also carries
`Content-Security-Policy: sandbox` and `X-Content-Type-Options: nosniff`.

### Authorization and AP2

Expand Down Expand Up @@ -404,6 +412,8 @@ export interface X402ProviderOptions extends X402OfferOptions {
readonly paymentFlow?: X402PaymentFlow;
/** Per-resource overrides of `paymentFlow`, keyed by resource id */
readonly resourcePaymentFlows?: Readonly<Record<string, X402PaymentFlow>>;
/** Each resource's challenge `resource.mimeType`; defaults to 'application/json' */
readonly resourceMimeTypes?: Readonly<Record<string, string>>;
/** Offers that replace the provider's for a resource, keyed by resource id */
readonly resourceOffers?: Readonly<Record<string, readonly X402OfferOptions[]>>;
readonly facilitator: X402FacilitatorConfig;
Expand Down Expand Up @@ -580,7 +590,7 @@ export interface Ap2TrustedKey {
```

The replay database is separate from payment replay storage. The caller that
creates the provider must close it. `main.ts` calls `close()` after
creates the provider must close it. `runGateway` calls `close()` after
`gateway.close()`; `gateway.close()` does not close authorization providers.

The merchant-side checkout signer has this boundary:
Expand Down Expand Up @@ -751,6 +761,7 @@ export interface GatewayConfig {
readonly maxTimeoutSeconds: number;
readonly paymentFlow?: X402PaymentFlow; // default 'authorization'
readonly resourcePaymentFlows?: Readonly<Record<string, X402PaymentFlow>>;
readonly resourceMimeTypes?: Readonly<Record<string, string>>; // resources that set `mimeType`
readonly facilitator: X402FacilitatorConfig;
readonly allowMainnet?: boolean;
readonly allowUnauthenticatedFacilitator?: boolean;
Expand Down Expand Up @@ -789,16 +800,16 @@ unset `adminToken` makes operator routes return 404; an empty

## Gateway HTTP surface

| Route | Behavior |
| --------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `GET /health` | Returns 200 after the Host check and, when present, the Origin check pass; a rejected Host or Origin returns 403. |
| `GET /ready` | Returns 503 if the store, an adapter, or either provider kind reports `fail`; `warn` remains ready. Results are briefly cached and concurrent probes share one check. |
| `GET /.well-known/agent-commerce` | Publishes merchant and sanitized adapter, provider, store, and protocol metadata. |
| `GET /api/resources` | Lists canonical resources without secrets. |
| `POST /api/resources/:id/invoke` | Invokes the HTTP surface. Payment and authorization use their defined headers; an unpaid request returns 402. |
| `GET /api/receipts?limit=` | Lists recent receipts through the operator-token gate. |
| `GET /api/events?limit=` | Lists recent events through the operator-token gate. |
| Adapter mounts and fixed routes | Delegated to enabled protocol adapters, with collision checks and per-mount limits. |
| Route | Behavior |
| --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `GET /health` | Returns 200 after the Host check and, when present, the Origin check pass; a rejected Host or Origin returns 403. |
| `GET /ready` | Returns 503 if the store, an adapter, or either provider kind reports `fail`; `warn` remains ready. Results are briefly cached and concurrent probes share one check. |
| `GET /.well-known/agent-commerce` | Publishes merchant and sanitized adapter, provider, store, and protocol metadata. |
| `GET /api/resources` | Lists canonical resources without secrets. |
| `POST /api/resources/:id/invoke` | Invokes the HTTP surface. Payment and authorization use their defined headers; an unpaid request returns 402. A delivery relays the backend's status, body and text `Content-Type`; see [security.md](security.md#backend-response-relay). |
| `GET /api/receipts?limit=` | Lists recent receipts through the operator-token gate. |
| `GET /api/events?limit=` | Lists recent events through the operator-token gate. |
| Adapter mounts and fixed routes | Delegated to enabled protocol adapters, with collision checks and per-mount limits. |

Without `server.adminToken`, both operator routes return 404. With a token,
they require `Authorization: Bearer <token>`; query-string tokens are not
Expand Down Expand Up @@ -880,7 +891,10 @@ The result is the base64 `PAYMENT-SIGNATURE` value returned to the gateway.

## Composition root

`src/gateway/main.ts` loads configuration, creates concrete providers and
adapters, starts the gateway, and closes owned resources during shutdown.
`createGateway` remains usable with injected fakes and does not depend on the
composition root.
`runGateway` in `src/gateway/run.ts` loads configuration, builds providers and
adapters, starts the gateway, and closes the server, AP2 providers and receipt
store on SIGTERM or SIGINT. `src/gateway/main.ts` and `agent-commerce start`
both call it. It imports MCP, AP2 and payment rail implementations only when
enabled. An enabled rail can share another rail's peers: MPP uses x402 for
settlement even with `payments.x402` disabled. `createGateway` accepts injected
implementations and does not import the composition root.
5 changes: 3 additions & 2 deletions docs/contributing-adapters.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@ A new name crosses the public contract and runtime wiring. After approval:
2. Add its strict config block to `src/config/schema/raw.ts`, and its
normalization to a module under `src/config/schema/` that `normalize.ts`
calls.
3. Wire a protocol adapter in `src/gateway/main.ts`, or build a payment rail in
`src/gateway/payment-providers.ts`.
3. Wire a protocol adapter in `src/gateway/run.ts`, or build a payment rail in
`src/gateway/payment-providers.ts`. Import an adapter or rail that needs an
optional peer through `importOptionalPeer`, inside its `enabled` branch.
4. For a rail, add its incoming proof and payment-challenge branches in
`src/gateway/routes.ts`; add a receipt branch only if the rail defines one.
The route already sends `Cache-Control: no-store` on every challenge and
Expand Down
Loading
Loading