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.

Not the demo stackThe repository's root 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.

PropertyValue
Platformslinux/amd64, linux/arm64
Usernode (uid 1000); root owns the package and the starter config
Port8080
HealthHEALTHCHECK 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
bash
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.2

The 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

VariableRequiredDefaultPurpose
MERCHANT_WALLETyes-Address that receives the payments (payTo). Use a wallet you control. The gateway never holds a key for it.
MERCHANT_API_URLyes-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_TOKENyes-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_URLnohttp://localhost:8080Public URL of the gateway. The templates set it to the platform domain. See Public URL.
PRICE_USDCno0.01Price per call in USDC, a decimal string with up to 6 decimal places.
X402_NETWORKnoeip155:84532 (Base Sepolia)CAIP-2 EVM testnet: Base Sepolia or Arbitrum Sepolia (eip155:421614). Base Sepolia is the tested configuration.
X402_RPC_URLnohttps://base-sepolia-rpc.publicnode.comRPC endpoint for health checks. Set it together with X402_NETWORK.
X402_FACILITATOR_URLnohttps://x402.org/facilitatorRemote 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.

Testnet onlyThe starter configuration omits 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 init and the AI-assisted flow write 127.0.0.1, which accepts only connections from inside the container. The built-in HEALTHCHECK can still pass because it also connects from inside.
  • server.port: 8080. The port mapping, the platform templates and the HEALTHCHECK all use 8080. A ${GATEWAY_PORT:-8080} placeholder is fine while GATEWAY_PORT stays unset.
  • storage.receipts.path under /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.

bash
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.2

The starter file is a working base. Every block is described in Configuration.

/app/config.yamlyaml
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: none

Public 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

bash
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