Skip to content

Harden the x402 facilitator: spend ceiling, caller allow-list, settle idempotency #147

Description

@Miracle656

Part of #135 (SCF RFP epic). Follows #125 / #126 — this is the hardening those two leave open.

Background

POST /settle submits a Stellar transaction and, with areFeesSponsored: true, spends the facilitator's fee-payer XLM on every call. The route is registered config: { public: true }, which skips API-key auth.

The only thing bounding it today is the global limiter: 100 requests/minute per IP for unauthenticated callers. That is not nothing, but 100 sponsored transactions per minute per IP is a meaningful drain, and IP addresses are cheap.

This is fine while the fee payer holds testnet lumens. It is not fine the first time it points at a funded mainnet account, which is the direction the RFP work is going.

To be clear about what is not wrong: a facilitator's /settle is meant to be callable by resource servers, and x402 assumes that. The gap is that nothing bounds how much it can be made to spend.

What to build

Three independent controls. All three, not whichever is easiest.

1. Spend ceiling

A hard cap on facilitator outflow that does not depend on request counting:

  • per-settlement maximum (reject a payment requiring more than X stroops in fees)
  • rolling daily total per network, tracked in Redis or Postgres so it survives a restart
  • when the cap is hit: refuse further settles with a clear error, and log loudly. Degrading is correct; silently draining is not.

Per network — mainnet and testnet ceilings are not the same number.

2. Caller allow-list

An optional allow-list of resource servers permitted to settle (FACILITATOR_ALLOWED_ORIGINS, or signed caller identity). Empty means open, preserving today's behaviour, so this cannot break the demo path.

3. Settle idempotency

A replayed /settle for a payment already settled must return the original result rather than submitting a second transaction. Key on something derived from the payment payload — the nonce plus the authorisation entry, not a client-supplied id, which an attacker controls.

Key files

  • src/api/facilitator.ts (or wherever #125/#126 land)
  • src/index.ts — rate-limit registration, for a tighter per-route limit
  • src/config.ts — per-network config map, following the pattern in #113

Acceptance criteria

Each must be demonstrated by a test that fails without the control.

  • A settle whose fee exceeds the per-settlement cap is rejected before any transaction is submitted
  • Once the daily cap is reached, further settles are refused; the counter survives a process restart
  • Caps are configured and enforced per network; exhausting testnet does not block mainnet
  • With an allow-list configured, a caller outside it is refused; with it empty, behaviour is unchanged
  • Replaying an identical settle returns the original result and submits exactly one transaction — assert the submission count, not just the response body
  • /settle has its own rate limit, tighter than the global 100/min IP default
  • Refusals return a spec-valid x402 error shape, not a bare 500
  • Every rejection path is logged with enough detail to tell abuse from misconfiguration

Notes

  • Do not put the ceiling only in the rate limiter. Rate limits bound frequency; the thing being protected is a balance.
  • Fail closed. A Redis outage must not mean unlimited settling — decide the behaviour deliberately and state it in the code.
  • Worth checking against the upstream x402 spec whether a standard error code exists for "facilitator declined" before inventing one; the RFP is judged on spec compliance.

Drips Wave · Complexity: Advanced · 200 points


Required: Before submitting, join the contributor Telegram so your work can be tracked and counted toward the Stellar Wave: https://t.me/+fxHXq8f1SwlkZDBk

Metadata

Metadata

Assignees

No one assigned

    Labels

    Stellar WaveIssues in the Stellar wave programarea:x402Payment middlewaredifficulty:advancedMulti-day projecthelp wantedExtra attention is neededpoints:200Advanced tier — 200 Wave points

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions