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
16 changes: 16 additions & 0 deletions .env.facilitator.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Select the baked-in environment config: dev or prod.
FACILITATOR_SERVICE_ENV=dev

# Optional explicit config-path override; takes precedence over FACILITATOR_SERVICE_ENV.
# FACILITATOR_CONFIG_PATH=/app/config/facilitator.config.dev.yaml

# Required when resolving the onepassword references in the selected YAML.
OP_SERVICE_ACCOUNT_TOKEN=

# Optional overrides; when set, these take precedence over 1Password.
# AGENT_WALLET_PASSWORD=
# TRON_GRID_API_KEY=
# GASFREE_API_KEY_NILE=
# GASFREE_API_SECRET_NILE=
# GASFREE_API_KEY_MAINNET=
# GASFREE_API_SECRET_MAINNET=
4 changes: 2 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ venv.bak/
*.log
logs

# Config (contains sensitive info)
# Config (contains environment-specific endpoints and secret references)
facilitator.config.yaml

# Private
Expand All @@ -99,4 +99,4 @@ dist/
*.tsbuildinfo
npm-debug.log*
pnpm-debug.log*
yarn-debug.log*
yarn-debug.log*
11 changes: 6 additions & 5 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,15 @@ npm run dev # tsx watch (reload on change)
npm run build # tsc -> dist/
npm start # run compiled dist/index.js
npm run typecheck # tsc --noEmit — the primary static check (see lint note below)
npm test # vitest run (37 tests, no DB required)
npm test # vitest run (no DB required)
npm test -- test/settlement.test.ts # single file
npm test -- -t "applies the authenticated tier" # single test by name
```

Before running, copy the config template: `cp config/facilitator.config.example.yaml config/facilitator.config.yaml`.
Before running, select a config explicitly: `FACILITATOR_SERVICE_ENV=dev npm run dev`,
`FACILITATOR_SERVICE_ENV=prod npm start`, or set `FACILITATOR_CONFIG_PATH`.

**Lint:** `npm run lint` references eslint, but eslint is **not installed and not configured** — the script does not work. Use `npm run typecheck` as the static check. CI (`.github/workflows/ci.yml`, `lint-and-test` job) runs `npm ci` → `typecheck``test`.
**Lint:** run `npm run lint` alongside `npm run typecheck`. CI (`.github/workflows/ci.yml`, `lint-and-test` job) runs `npm ci` → production dependency audit → lint → typecheck → test.

## Architecture

Expand All @@ -41,13 +42,13 @@ Key seams:

- **Auth (`src/auth.ts`)** is **advisory, not a hard gate** — anonymous requests are allowed at the anonymous rate. A valid `X-API-KEY` selects the authenticated rate tier and scopes payment lookups to that seller. Keys are held in an in-memory cache refreshed periodically from the DB, checked in constant time.

- **Config & secrets (`src/config.ts`)** — YAML (`config/facilitator.config.yaml`, override path via `FACILITATOR_CONFIG_PATH`). Secrets resolve **env first, then 1Password**: any `onepassword.*` value is a `vault/item/field` ref resolved when `OP_SERVICE_ACCOUNT_TOKEN` / `onepassword.token` is set. A network listed under `facilitator.networks` is enabled.
- **Config & secrets (`src/config.ts`)** — `FACILITATOR_SERVICE_ENV=dev|prod` selects the matching baked-in YAML; `FACILITATOR_CONFIG_PATH` is an explicit override. Secrets resolve **env first, then 1Password**: any `onepassword.*` value is a `vault/item/field` ref resolved when `OP_SERVICE_ACCOUNT_TOKEN` / `onepassword.token` is set. A network listed under `facilitator.networks` is enabled.

- **Database (`src/db/`, drizzle + `pg`)** — v2 owns a new `settlements` table (created on startup), keyed on the on-chain **authorization identity** `(network, scheme, asset, payer, nonce)`, with a partial-unique index enforcing one successful settlement per authorization. The shared `sellers` / `api_keys_plus` tables are reused unchanged. v1's `payment_records` is unused.

## Conventions & gotchas

- Networks use **CAIP-2** ids (`tron:nile`, `tron:mainnet`, `bsc:testnet`, `eip155:*`). `isTron`/`isEvm` in `facilitator.ts` route by prefix.
- Networks use supported canonical **CAIP-2** ids only (for example `tron:0xcd8690dc`, `tron:0x2b6653dc`, `eip155:97`, `eip155:84532`). Friendly aliases are rejected; `facilitator.ts` routes by the registered network family.
- The `@bankofai/x402-*` packages (`x402-core`, `x402-evm`, `x402-tron`, `x402-extensions`) come from the npm registry. Pin to the tested version deliberately — bumping is a separate, deliberate upgrade (API drift risk). When the SDK's interface is awkward, surface the gap rather than silently `any`-adapting around it.
- Fees were removed from the TRON schemes in SDK `1.0.1`; there is no `base_fee` config and no `/fee/quote` endpoint.
- Stale Python artifacts (`src/**/__pycache__`, `tests/__pycache__`) are leftovers from v1 — ignore them; the live tests are TS files under `test/`.
Expand Down
5 changes: 5 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ ENV NODE_ENV=production
COPY package.json package-lock.json* ./
RUN npm ci --omit=dev
COPY --from=build /app/dist ./dist
# Bake the non-secret environment configs into the image. They contain only
# 1Password references; OP_SERVICE_ACCOUNT_TOKEN remains a runtime env variable.
# Copy explicitly so a local config/facilitator.config.yaml is never included.
COPY config/facilitator.config.dev.yaml ./config/facilitator.config.dev.yaml
COPY config/facilitator.config.prod.yaml ./config/facilitator.config.prod.yaml

# Non-root runtime user matching legacy v1 exactly: `ec2-user` at uid/gid 1000 with
# HOME /home/ec2-user. The ops `docker run` bind-mounts the provisioned agent-wallet
Expand Down
30 changes: 20 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,8 @@ A TypeScript/Node service. The earlier Python/FastAPI implementation is kept und
### Install and run

```bash
npm install
cp config/facilitator.config.example.yaml config/facilitator.config.yaml
npm run dev # tsx watch; or: npm run build && npm start
npm ci
FACILITATOR_SERVICE_ENV=dev npm run dev
```

Default listen address: `http://0.0.0.0:8001`.
Expand All @@ -51,9 +50,10 @@ Default listen address: `http://0.0.0.0:8001`.

## Configuration

YAML config (`config/facilitator.config.yaml`; template:
[`config/facilitator.config.example.yaml`](config/facilitator.config.example.yaml)).
Path override: `FACILITATOR_CONFIG_PATH`.
Choose a YAML configuration source explicitly. Set `FACILITATOR_SERVICE_ENV=dev` or
`FACILITATOR_SERVICE_ENV=prod` to select the matching baked-in environment config, or
set `FACILITATOR_CONFIG_PATH` to an explicit YAML file; the explicit path takes
precedence. The process fails before startup when neither is set.

Required: `database.url`, `facilitator.networks` (≥1 network, listed = enabled).

Expand All @@ -63,6 +63,8 @@ is set). Relevant env vars:

| Var | Purpose |
|---|---|
| `FACILITATOR_SERVICE_ENV` | `dev` or `prod`; selects the matching baked-in config file |
| `FACILITATOR_CONFIG_PATH` | Explicit config path; overrides `FACILITATOR_SERVICE_ENV` |
| `AGENT_WALLET_PASSWORD` | Unlock the agent-wallet provider |
| `TRON_GRID_API_KEY` | TronGrid rate limits (shared across TRON networks) |
| `GASFREE_API_KEY[_NILE\|_MAINNET]` / `GASFREE_API_SECRET[...]` | GasFree relayer creds (gate `exact_gasfree`) |
Expand Down Expand Up @@ -109,9 +111,8 @@ npm, declared as `^1.0.1` in `package.json`.
docker build -t x402-facilitator .

docker run -p 8001:8001 -p 9001:9001 \
-e OP_SERVICE_ACCOUNT_TOKEN="" \
-e AGENT_WALLET_PASSWORD="" \
-v "$PWD/config/facilitator.config.yaml:/app/config/facilitator.config.yaml:ro" \
-e FACILITATOR_SERVICE_ENV=dev \
-e OP_SERVICE_ACCOUNT_TOKEN \
-v "$PWD/logs:/app/logs" \
x402-facilitator
```
Expand All @@ -122,8 +123,17 @@ password is resolved from `OP_SERVICE_ACCOUNT_TOKEN` (1Password) when set;
otherwise pass it directly via `AGENT_WALLET_PASSWORD`. Port `9001` is only
needed when `monitoring.port` differs from `server.port`.

Both `config/facilitator.config.dev.yaml` and
`config/facilitator.config.prod.yaml` are baked into the image. Select one at
runtime with `FACILITATOR_SERVICE_ENV=dev` or `FACILITATOR_SERVICE_ENV=prod`; no
config-directory mount is required. `FACILITATOR_CONFIG_PATH` remains available
for an explicit custom path.
`OP_SERVICE_ACCOUNT_TOKEN` must be injected only at container runtime (for
example by the deployment platform's secret environment-variable facility);
it is never stored in the image or either YAML file.

## Status

Feature-complete and unit-tested; **not yet validated against live chains** (real
verify+settle on tron:nile / bsc:testnet and GasFree end-to-end are pending), and
verify+settle on `tron:0xcd8690dc` / `eip155:97` and GasFree end-to-end are pending), and
without integration tests yet.
48 changes: 48 additions & 0 deletions config/facilitator.config.dev.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Local runtime config for the development environment.
# Start with:
# FACILITATOR_SERVICE_ENV=dev npm run dev

database:
# node-postgres accepts this legacy SQLAlchemy/asyncpg URL form; credentials
# are injected from onepassword below before connecting.
url: "postgresql+asyncpg://sun-agent-postgresql.chqcqywoo8fb.us-east-1.rds.amazonaws.com:5432/x402_facilitator"
ssl_mode: "require"
max_open_conns: 100

onepassword:
database_user: "x402-facilitator_dev/psql/user"
database_password: "x402-facilitator_dev/psql/password"
trongrid_api_key: "x402-facilitator_dev/trongrid/trongrid_api_key"
gasfree_api_key_nile: "x402-facilitator_dev/gasfree/gasfree_api_key_nile"
gasfree_api_secret_nile: "x402-facilitator_dev/gasfree/gasfree_api_secret_nile"
agent_wallet_password: "x402-facilitator_dev/wallet/agent_wallet_password"

server:
host: "0.0.0.0"
port: 8001

logging:
dir: "logs"
filename: "app.log"
level: "INFO"

rate_limit:
api_key_refresh_interval: 60
authenticated: "1000/minute"
anonymous: "1/minute"

monitoring:
port: 9001
endpoint: "/metrics"

facilitator:
networks:
# TRON Nile testnet
tron:0xcd8690dc:
schemes: ["exact", "upto", "batch-settlement"]
# BSC Testnet
eip155:97:
schemes: ["exact", "upto", "batch-settlement"]
# Base Sepolia testnet
eip155:84532:
schemes: ["exact", "upto", "batch-settlement"]
64 changes: 0 additions & 64 deletions config/facilitator.config.example.yaml

This file was deleted.

59 changes: 59 additions & 0 deletions config/facilitator.config.prod.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Local runtime config for the production environment.
# Start with:
# FACILITATOR_SERVICE_ENV=prod npm start

database:
# node-postgres accepts this legacy SQLAlchemy/asyncpg URL form; credentials
# are injected from onepassword below before connecting.
url: "postgresql+asyncpg://sunpump-sunagent-postsql-v2.c9w4kaa66fyg.us-east-1.rds.amazonaws.com:5432/x402_facilitator_prod"
ssl_mode: "require"
max_open_conns: 100

onepassword:
database_user: "x402-facilitator/psql/user"
database_password: "x402-facilitator/psql/password"
trongrid_api_key: "x402-facilitator/trongrid/trongrid_api_key"
gasfree_api_key_nile: "x402-facilitator/gasfree/gasfree_api_key_nile"
gasfree_api_secret_nile: "x402-facilitator/gasfree/gasfree_api_secret_nile"
gasfree_api_key_mainnet: "x402-facilitator/gasfree/gasfree_api_key_mainnet"
gasfree_api_secret_mainnet: "x402-facilitator/gasfree/gasfree_api_secret_mainnet"
agent_wallet_password: "x402-facilitator/wallet/agent_wallet_password"

server:
host: "0.0.0.0"
port: 8001

logging:
dir: "logs"
filename: "app.log"
level: "INFO"

rate_limit:
api_key_refresh_interval: 5
authenticated: "1000/minute"
anonymous: "1/minute"

monitoring:
port: 9001
endpoint: "/metrics"

facilitator:
networks:
# TRON Nile testnet
tron:0xcd8690dc:
schemes: ["exact", "upto", "batch-settlement"]
# TRON Mainnet
tron:0x2b6653dc:
schemes: ["exact", "upto", "batch-settlement"]
# BSC Testnet
eip155:97:
schemes: ["exact", "upto", "batch-settlement"]
# BSC Mainnet
eip155:56:
schemes: ["exact", "upto", "batch-settlement"]
# Base Sepolia testnet
eip155:84532:
schemes: ["exact", "upto", "batch-settlement"]
# Base Mainnet
eip155:8453:
schemes: ["exact", "upto", "batch-settlement"]
Loading
Loading