# Local fork demo. Anvil forks Base mainnet so the real USDC # contract (EIP-3009 transferWithAuthorization) behaves correctly locally. # # All private keys below hold fake value only. Never reuse them anywhere: # anvil dev account #0 -> facilitator signer (broadcasts settlements, pays gas) # anvil dev account #2 -> pay-to (receives revenue) # client -> a fresh keypair generated for this repo, funded with # minted USDC by fund-usdc. # The client is NOT an anvil dev account on purpose: the publicly known dev # accounts carry EIP-7702 delegations on real Base mainnet, so on the fork # USDC signature checks treat them as ERC-1271 contract wallets and reject # plain ECDSA payment authorizations. services: anvil: image: ghcr.io/foundry-rs/foundry@sha256:0c00cb0bda1ab1b91c9a6bf60f4c76c09c1a8870824b6d4718afbabacf6f9a17 container_name: sluice-anvil # --block-time keeps chain time tracking wall clock; without it anvil's # timestamp freezes at the fork block and EIP-3009 validAfter checks fail # ("authorization is not yet valid"). command: - "anvil --fork-url ${BASE_FORK_RPC:-https://mainnet.base.org} --host 0.0.0.0 --block-time 5" ports: - "127.0.0.1:8545:8545" healthcheck: test: ["CMD-SHELL", "cast block-number --rpc-url http://127.0.0.1:8545"] interval: 5s timeout: 5s retries: 24 fund-usdc: # One-shot: mints test USDC to the client account on the fork. image: ghcr.io/foundry-rs/foundry@sha256:0c00cb0bda1ab1b91c9a6bf60f4c76c09c1a8870824b6d4718afbabacf6f9a17 container_name: sluice-fund-usdc depends_on: anvil: condition: service_healthy volumes: - ./scripts/fund-usdc.sh:/fund-usdc.sh:ro environment: RPC: http://anvil:8545 CLIENT_ADDRESS: "0xa7490FFD6fFAF9C629a1E1Be4875E6b7700943DA" command: - "sh /fund-usdc.sh" facilitator: image: ghcr.io/x402-rs/x402-facilitator@sha256:4ebe09d824bf6a5f342add8ae22eb44a2c56894ad22e7d3f5b83ba38023226b9 container_name: sluice-facilitator depends_on: anvil: condition: service_healthy volumes: - ./config/facilitator.json:/app/config.json:ro environment: FACILITATOR_PRIVATE_KEY: "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" # The facilitator exports OTLP over HTTP (not gRPC) -> collector :4318 OTEL_EXPORTER_OTLP_ENDPOINT: http://otel-collector:4318 OTEL_SERVICE_NAME: facilitator origin: # The metered origin. By default the built-in demo stand-in, so a fresh # clone of this repo alone runs the full paid loop; docker-compose.firn.yml # overrides this same service to build Firn instead (a true swap — same # service name, so nothing extra runs or gates the gateway). Pluggable: # any HTTP origin works. build: context: . container_name: sluice-origin entrypoint: ["/usr/local/bin/demo-origin"] environment: BIND: "0.0.0.0:3000" RUST_LOG: info otel-collector: image: otel/opentelemetry-collector-contrib:0.116.1@sha256:d0ebf65280da2e1b1491d1b93648281afd353d4b9ea19160090303cec9a233bd container_name: sluice-otel-collector command: ["--config=/etc/otelcol/config.yaml"] volumes: - ./config/otel-collector.yaml:/etc/otelcol/config.yaml:ro prometheus: image: prom/prometheus:v3.1.0@sha256:6559acbd5d770b15bb3c954629ce190ac3cbbdb2b7f1c30f0385c4e05104e218 container_name: sluice-prometheus volumes: - ./config/prometheus.yml:/etc/prometheus/prometheus.yml:ro depends_on: - otel-collector grafana: image: grafana/grafana:11.4.0@sha256:d8ea37798ccc41061a62ab080f2676dda6bf7815558499f901bdb0f533a456fb container_name: sluice-grafana ports: - "127.0.0.1:3001:3000" environment: GF_AUTH_ANONYMOUS_ENABLED: "true" GF_AUTH_ANONYMOUS_ORG_ROLE: Viewer GF_SECURITY_ADMIN_PASSWORD: admin volumes: - ./config/grafana/provisioning:/etc/grafana/provisioning:ro - ./config/grafana/dashboards:/var/lib/grafana/dashboards:ro depends_on: - prometheus - postgres postgres: image: postgres:17-alpine@sha256:18cfe3ef5e6815560c98237d6216d1e5119702fb0f3894c8785dd58b8bbe5d73 container_name: sluice-postgres environment: POSTGRES_USER: sluice POSTGRES_PASSWORD: sluice POSTGRES_DB: sluice volumes: - postgres-data:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U sluice -d sluice"] interval: 5s timeout: 3s retries: 12 indexer: # Settlement receipts -> Postgres. Applies migrations/ at startup. build: context: . container_name: sluice-indexer entrypoint: ["/usr/local/bin/indexer"] depends_on: postgres: condition: service_healthy environment: DATABASE_URL: postgres://sluice:sluice@postgres:5432/sluice BIND: "0.0.0.0:8090" # Shared secret for /receipts; dev-only value, matches the gateway's. INDEXER_TOKEN: sluice-dev-indexer-token RUST_LOG: info gateway: build: context: . container_name: sluice-gateway depends_on: - facilitator - origin - indexer ports: - "127.0.0.1:8080:8080" volumes: # The whole directory, not the single file: a file bind mount pins the # inode at container start, so editors/sed -i/git that replace the file # would update the host copy while the container silently keeps the old # one — defeating live reload. A directory mount resolves per access. - ./config:/app/config:ro environment: ORIGIN_URL: http://origin:3000 FACILITATOR_URL: http://facilitator:8080 PAY_TO: "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC" RULES_PATH: /app/config/rules.json # API key -> caller id map; dev-only keys, real ones stay off-machine. CALLERS_PATH: /app/config/callers.json STRIP_PREFIX: /firn INDEXER_URL: http://indexer:8090 INDEXER_TOKEN: sluice-dev-indexer-token RUST_LOG: info,gateway=debug client: # Test client; not part of the stack. Run with: # docker compose run --rm client build: context: . profiles: ["client"] entrypoint: ["/usr/local/bin/client"] depends_on: gateway: condition: service_started fund-usdc: condition: service_completed_successfully environment: GATEWAY_URL: http://gateway:8080 CLIENT_PRIVATE_KEY: "0x69935b0e457ed4de0c989163f1c1aa7cf899de6cff42b1829a42f4fa6a607da3" CLIENT_API_KEY: sluice-dev-key-tenant-a volumes: postgres-data: