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
| question | result | how |
|---|---|---|
| Keyless by-id read on a real id | Works. 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 limit | Concurrency-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 → inactive | Automatic, 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.
completedandinactiveare cached forever and never re-fetched.
The optional background reconciler, when it does run, follows a bounded jittered schedule:
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.
maxUsage: N
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.