Deploy
Container image
Run ghcr.io/devlab-group/agent-commerce with the testnet starter configuration. Set variables, the public URL and persistent storage, then verify the deployment.
ghcr.io/devlab-group/agent-commerce:2.7.2 runs the gateway with a testnet starter configuration that charges for one API endpoint. The image and variables below are shared by the platform templates: CapRover, Dokploy, Railway and Zeabur.
docker compose up runs the quickstart: a local chain and the gateway source in development mode. The container image runs the published package.The image
The image contains the published npm package @devlab.group/agent-commerce at the same version, with pinned optional peers for x402 and MCP. Its tag matches the gateway version.
| Property | Value |
|---|---|
| Platforms | linux/amd64, linux/arm64 |
| User | node (uid 1000); root owns the package and the starter config |
| Port | 8080 |
| Health | HEALTHCHECK on GET /health (liveness) |
| Data | /app/data holds the SQLite receipt store; mount a volume |
| Config | /app/config.yaml (starter), replaceable through AGENT_COMMERCE_CONFIG |
docker run -d --name acg -p 8080:8080 -v acg-data:/app/data \
-e MERCHANT_WALLET=0xYourTestnetAddress \
-e MERCHANT_API_URL=https://api.example.com/v1/quote \
-e ADMIN_TOKEN="$(openssl rand -hex 32)" \
ghcr.io/devlab-group/agent-commerce:2.7.2The starter configuration exposes one paid GET endpoint as paid_api, over HTTP and MCP, and accepts testnet payments.
The image installs these optional peers: @x402/core, @x402/evm, viem and @modelcontextprotocol/server. MPP requires mppx; AP2 requires jose, @sd-jwt/core, canonicalize. Enabling either without its peers stops startup with CONFIG_INVALID naming the missing package. Solana and NEAR offers fail health checks in this image. For those configs, use the published package with the peers they need.
Variables
| Variable | Required | Default | Purpose |
|---|---|---|---|
MERCHANT_WALLET | yes | - | Address that receives the payments (payTo). Use a wallet you control. The gateway never holds a key for it. |
MERCHANT_API_URL | yes | - | Full URL of the endpoint to charge for, for example https://api.example.com/v1/quote. The starter calls it with GET and no input. Keep the API private where possible. |
ADMIN_TOKEN | yes | - | Bearer token for the operator routes: /api/receipts, /api/events, /api/payment-attempts, /api/transactions and manual resolution at POST /api/payment-attempts/:id/resolve. Use at least 32 random characters. The templates generate it. |
GATEWAY_PUBLIC_BASE_URL | no | http://localhost:8080 | Public URL of the gateway. The templates set it to the platform domain. See Public URL. |
PRICE_USDC | no | 0.01 | Price per call in USDC, a decimal string with up to 6 decimal places. |
X402_NETWORK | no | eip155:84532 (Base Sepolia) | CAIP-2 EVM testnet: Base Sepolia or Arbitrum Sepolia (eip155:421614). Base Sepolia is the tested configuration. |
X402_RPC_URL | no | https://base-sepolia-rpc.publicnode.com | RPC endpoint for health checks. Set it together with X402_NETWORK. |
X402_FACILITATOR_URL | no | https://x402.org/facilitator | Remote facilitator that verifies and settles payments. The default is the public testnet one and takes no credential. |
Optional variables use their defaults when unset or empty. A missing required variable stops configuration loading with a message naming it. Empty required values go through validation; the Dokploy and demo Compose files reject them before startup.
allowMainnet, which makes a mainnet X402_NETWORK fail validation. To accept mainnet payments, use your own config and review the mainnet guardrails.Your own configuration
To change the resources or enable mainnet, mount your own config.yaml and point AGENT_COMMERCE_CONFIG at it, then pass the variables your file reads. Keep these settings for the image's port mapping, healthcheck and data volume:
server.host: 0.0.0.0.agent-commerce initand the AI-assisted flow write127.0.0.1, which accepts only connections from inside the container. The built-inHEALTHCHECKcan still pass because it also connects from inside.server.port: 8080. The port mapping, the platform templates and theHEALTHCHECKall use 8080. A${GATEWAY_PORT:-8080}placeholder is fine whileGATEWAY_PORTstays unset.storage.receipts.pathunder/app/data, for the receipts to persist.
Backend and RPC URLs must be reachable from inside the container. A config from init or the AI-assisted flow points the backend at http://localhost:3000, and the init x402 RPC at http://127.0.0.1:8545. Inside the container both refer to the container itself. Replace them with reachable addresses to avoid 502 BACKEND_ERROR on calls and 503 from /ready.
docker run -d --name acg -p 8080:8080 -v acg-data:/app/data \
-v "$PWD/config.yaml:/config/config.yaml:ro" \
-e AGENT_COMMERCE_CONFIG=/config/config.yaml \
-e ADMIN_TOKEN="$(openssl rand -hex 32)" \
ghcr.io/devlab-group/agent-commerce:2.7.2The starter file is a working base. Every block is described in Configuration.
version: 1
merchant:
id: starter
name: Agent Commerce Gateway
publicBaseUrl: ${GATEWAY_PUBLIC_BASE_URL:-http://localhost:8080}
server:
port: 8080
host: 0.0.0.0
adminToken: ${ADMIN_TOKEN}
storage:
receipts:
driver: sqlite
path: /app/data/receipts.sqlite
protocols:
http:
enabled: true
mcp:
enabled: true
mountPath: /mcp
resources:
paid_api:
name: Paid API
description: One HTTP endpoint of the merchant API, paid per call in USDC.
input:
type: object
properties: {}
additionalProperties: false
backend:
type: http
method: GET
url: ${MERCHANT_API_URL}
timeoutMs: 10000
pricing:
type: fixed
amount: "${PRICE_USDC:-0.01}"
currency: USDC
expose: [http, mcp]
payments: [x402]
payments:
x402:
enabled: true
network: ${X402_NETWORK:-eip155:84532}
# Health checks only; set it together with X402_NETWORK
rpcUrl: ${X402_RPC_URL:-https://base-sepolia-rpc.publicnode.com}
payTo: ${MERCHANT_WALLET}
maxTimeoutSeconds: 300
facilitator:
mode: remote
url: ${X402_FACILITATOR_URL:-https://x402.org/facilitator}
auth:
type: nonePublic URL
The gateway compares each request's Host hostname with GATEWAY_PUBLIC_BASE_URL, also accepting localhost, 127.0.0.1 and ::1. Other hosts receive 403. Set the public URL to the domain clients use, including when a proxy sits in front.
Since 2.7.2, the first image version, GET and HEAD requests to /health skip the Host check, so platform probes can reach /health through a container address or their own hostname. /ready and other routes return 403 for an unapproved Host. Requests with an Origin header must match server.allowedOrigins, including on /health.
Data volume
Mount a volume at /app/data to persist receipts. The image runs as uid 1000, and a new named volume inherits that owner. On platforms that mount volumes as root, make the volume writable by uid 1000 or run the process as root; Railway shows the setting.
Verify a deployment
curl -s -o /dev/null -w '%{http_code}\n' https://YOUR_DOMAIN/ready
curl -si -X POST https://YOUR_DOMAIN/api/resources/paid_api/invoke \
-H 'content-type: application/json' -d '{}' | grep -i '^payment-required'/ready returns 200 when the receipt store, the MCP adapter and the payment provider checks (facilitator and RPC) pass. An unpaid invocation returns 402 with a base64-encoded PAYMENT-REQUIRED header. With the defaults, the decoded header names Base Sepolia (eip155:84532), its USDC contract (0x036CbD53842c5426634e7929541eC2318f3dCF7e) and your payTo address.
Payment flow
With the default paymentFlow of authorization, the gateway verifies the buyer's authorization, calls your API, and settles after a 2xx response. Backend failures leave the payment unsettled.
For an API with side effects, set paymentFlow: upfront in your own configuration. Payment then settles before the API runs and stays settled if the API fails. See Payment flow.
Based on: deploy/docs/deploy.md