Rate limits and caching
Every request is rate-limited, every data response is served fresh, and your live budget rides back on each response as headers.
Rate limits
Every request is charged the per-IP budget for its class and the per-IP budget for all requests. A request presenting a valid proof is charged the matching per-grant budgets as well, once the proof verifies.
| Per IP | Limit |
|---|---|
| All requests | 600 per 60 seconds |
| Reads | 480 per 60 seconds |
| Upvotes | 100 per 60 seconds |
| Posts, comments, and media uploads | 30 per 60 seconds |
POST /v1/key-proof/present | 10 per 60 seconds |
| Per grant | Limit |
|---|---|
| All requests | 600 per 60 seconds |
| Reads | 300 per 60 seconds |
| Upvotes | 100 per 60 seconds |
| Posts, comments, and media uploads | 30 per 60 seconds |
Reads include the media status check and
GET /v1/key-proof/status. The per-grant budget follows one grant across every IP you call from, and each
user of your app gets their own. These are current defaults and may change:
treat the response headers and the 429 retry signal as authoritative rather
than hardcoding them.
Responses carry RateLimit-Limit (the applicable limit), RateLimit-Remaining,
and RateLimit-Reset (seconds until the budget resets). A 429 Too Many Requests carries Retry-After in seconds and, in the error body,
retryAfterMs — the same delay in milliseconds; respect it and back off.
Which budget you hit
A 429 body carries a scope naming the budget that denied the request. Treat
it as an open set — back off on the advertised delay whatever it says — but it
tells you which knob to turn.
scope | What it bounds |
|---|---|
global | Every request from your IP address. You are over the all-requests row above. |
query | Reads from your IP address. |
action | Upvotes from your IP address. |
create | Posts, comments, and media uploads from your IP address. |
sensitive | POST /v1/key-proof/present from your IP address. |
authenticated | Either per-grant budget — all requests, or the class. Only a request presenting a proof sees it. |
pop-capture | Signed requests from your IP address, whether or not the proof verifies. |
Each of those carries the usual RateLimit-* headers alongside Retry-After,
except pop-capture, which carries Retry-After and no RateLimit-* headers.
A fleet signing from a single address can meet it before a per-grant limit.
The OpenAPI specification and llms.txt share the per-IP reads budget. A 429
there is a plain-text body with Retry-After and no error code.
When a request cannot be checked
Any endpoint can answer 503 Service Unavailable with the code
E_SERVICE_UNAVAILABLE when the API cannot check your request against its rate
budget. On the credential-gated routes the same 503 can also mean the
credential could not be verified (see
Authentication). Both look
identical on the wire and take the same client response.
A 503 is not a quota event: your budget is not exhausted and nothing you sent
was wrong. It carries no Retry-After and no retry delay in the body, so retry
later with exponential backoff and jitter. Branch on the error code —
E_RATE_LIMITED means slow to the advertised delay, E_SERVICE_UNAVAILABLE
means try again shortly.
Caching
Data responses are never cached: every data endpoint returns
Cache-Control: no-store. Anything served to one particular user — the gated
routes, and the member projection on the
optional-auth post routes — is
no-store permanently and can never earn a shared cache.
Only two static artifacts are cached, both with
Cache-Control: public, s-maxage=300: the generated OpenAPI specification
(/openapi.json, /v1/openapi.json, and the .yaml variants) and the
llms.txt index (/llms.txt, /v1/llms.txt).