Skip to main content
Ward never tests the shared, public facilitator.goplausible.xyz instance — every result it produces is about a real, self-hosted facilitator process Ward controls end to end. This page covers how that container is built and how to work with it directly.

Why a self-hosted facilitator at all

Testing correctness guarantees adversarially means intentionally sending malicious payloads. Doing that against shared production infrastructure would be irresponsible regardless of intent — a self-hosted, disposable instance is what makes “adversarial” safe to run at all.

What gets built

docker/facilitator.Dockerfile builds and runs GoPlausible’s x402-avm facilitator exactly as published — nothing patched, nothing mocked.
1

Clone at build time

The upstream facilitator ships as a pnpm-workspace-internal package (@x402-avm/core-facilitator-typescript, depending on other @x402-avm/* packages via workspace:*) with no standalone Dockerfile of its own — it’s meant to run from inside the monorepo. The image clones GoPlausible/x402-avm at branch-v2-algorand-publish at build time to get that full workspace.
2

Install and build only the facilitator's dependency tree

pnpm --filter "@x402-avm/core-facilitator-typescript..." run build builds the facilitator and its actual workspace dependencies — not every example app in the monorepo (a plain pnpm build would also try to build unrelated apps like a Next.js example that fails without app-specific env vars Ward never needs).
3

Preserve the full repo path structure at runtime

The workspace’s node_modules/@x402-avm/* entries are pnpm symlinks with relative targets reaching outside examples/typescript into a sibling typescript/packages/ tree. The final image copies the entire cloned repo, not a flattened subset, so those symlinks keep resolving.
4

Start the real facilitator process

pnpm start inside examples/typescript/facilitator, exposing port 4022.

Running it directly

ward init runs exactly this command for you, then waits for the health check and verifies one real payment — see ward init. You rarely need to invoke Compose by hand, but it’s a plain Docker Compose service if you do.

Configuration

The container reads four environment variables, all sourced from your .env (see docker-compose.yml): Full details in Environment Variables.

Health check

Polls the facilitator’s own GET /supported endpoint — the same endpoint Ward’s U4 invariant reads to discover what the facilitator declares support for.
The upstream facilitator example is a genuinely multi-chain service — it process.exit(1)s at startup if either key is missing, even though only the Algorand (ExactAvmScheme) path is ever registered or exercised by anything Ward does. scripts/fund-accounts.ts generates a random, well-formed throwaway hex string (EVM) and base58 string (SVM) purely to satisfy that check — no real key material, no funding, no chain interaction on either network. This is safe specifically because those signers are never invoked by anything in Ward’s request path.

Point the paid API at your own instance