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.
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
Part of #135 (SCF RFP epic). Follows #125 / #126 — this is the hardening those two leave open.
Background
POST /settlesubmits a Stellar transaction and, withareFeesSponsored: true, spends the facilitator's fee-payer XLM on every call. The route is registeredconfig: { 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
/settleis 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:
Xstroops in fees)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
/settlefor 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 limitsrc/config.ts— per-network config map, following the pattern in #113Acceptance criteria
Each must be demonstrated by a test that fails without the control.
/settlehas its own rate limit, tighter than the global 100/min IP defaultNotes
Required: Before submitting, join the contributor Telegram so your work can be tracked and counted toward the Stellar Wave: https://t.me/+fxHXq8f1SwlkZDBk