Payments

Rate limits and the polling design

What the live API actually does, and how polling is shaped so it never matters.

What we measured

Moove documents that requests are limited per key and per IP, whichever is reached first, and publishes no numbers. So we measured, 2026-09-07, against production with a real key.measured

questionresulthow
Keyless by-id read on a real idWorks. Full object returned.Created a link with a key, read it with no X-API-Key header.
Does that route share the authenticated bucket?No — separate buckets.300 public reads at 24 req/s: zero 429. The authenticated route tripped at ~141 in the same window.
Per-key limitConcurrency-sensitive, not a fixed quota. 429 after ~141 requests at 30 concurrent (~27 req/s). Paced load at 5, 10 and 20 req/s ran clean. Recovery immediate.Burst ramp to first 429, a paced sweep, a recovery poll.
Retry-After or RateLimit-* on a 429?Neither. No rate-limit headers of any kind.Captured every header on an induced 429: Date, Content-Type, Content-Length, Connection, server.
Expiry → inactiveAutomatic, lazily.A 90-second link read active at 100s and inactive at 120s.

The polling design

  • Never inline. A challenge returns immediately; nothing waits on a human inside a tool call.
  • Poll the public by-id read, not the authenticated list.It costs nothing against the key budget, and Moove’s own docs say the list endpoint is for reconciliation, not for watching one link.
  • Demand-driven first.The agent’s retry is the trigger. A floor of 2 seconds between polls of the same charge means a tight retry loop cannot become a poll loop — a test asserts ten rapid retries produce one read.
  • Terminal is terminal. completed and inactive are cached forever and never re-fetched.

The optional background reconciler, when it does run, follows a bounded jittered schedule:

reconciler schedule — POLL_SCHEDULE_MS and POLL_JITTER in packages/moove/src/poller.ts
3s, 6s, 12s, 24s, 48s, then every 60s, ±30% jitter,
until the charge expires (60 minutes by default)

Charges older than the expiry are swept to abandoned without an API call. The 401 fallback to an authenticated read exists because the public route is deliberately absent from the published OpenAPI document and could change without a schema diff.

The limiter, and why its defaults stay low

Two independent adaptive token buckets — keyed at 2 req/s, public at 8 req/s — additive increase on sustained success, halve on an observed 429. With no headers to read, backoff has to be blind, and this converges on the real limit without knowing it.

The measured headroom did not change the defaults
This is a library other people run from their own infrastructure, against their own keys, sharing an IP with whatever else they run. A ceiling measured once, from one machine, on one account, is not a budget the library should spend on someone else’s behalf. It adapts upward if the headroom is really there.

maxUsage: N

Unresolved
Whether a multi-use link reports partial progress is unresolved. The object exposes only maxUsage, status and receivedAmount — no usage counter — so there is no field that could show k of N, and whether status flips at the Nth payment or earlier needs a real payment against a multi-use link, which has not been done.

It does not bear on the design. Tollbooth only ever issues maxUsage: 1 links — one per charge — because Moove exposes nothing that identifies a payer, so a link shared between two buyers would be unattributable whatever a counter said. See what a link id reveals.

Tollbooth

A paywall layer for MCP servers. An agent calls a paid tool, a human pays, the agent retries. Every number on this page is one we measured, at the precision we measured it.