Rate Limits
MAGMA rate-limits requests to protect the protocol and keep the shared devnet API responsive. Limits are IP-based by default; a few classes key on a more specific identifier (wallet, API key) so one noisy actor can't exhaust a shared pool.
Default quotas
| Route class | Limit | Keyed on |
|---|---|---|
| Public reads & narrative feed | ~60 req/min | IP address |
| Admin / operations | ~120 req/min | IP address |
These are the steady-state quotas for general use. Several endpoints apply tighter, purpose-specific sub-limits on top of the default, because they are expensive or abuse-prone:
| Endpoint class | Sub-limit | Keyed on |
|---|---|---|
| Narrative submission / backing | ~10 req/min | wallet address |
AI text polish (/v1/narratives/polish-text) | 3 per day | wallet address |
Voice polish (/v1/narratives/polish-voice) | 3 per day | wallet (shared pool) |
Identity verification (/v1/verify/*) | ~20 req/min | IP address |
Agent feed (/v1/agent/feed) | ~60 req/min | API key |
Agent registration (/v1/agent/register) | ~5 per hour | IP address |
Quotas are approximate and may be tuned during beta. Treat the numbers above as
order-of-magnitude guidance and rely on the response headers and 429 body —
not hard-coded constants — to drive your client's backoff.
How limits are signalled
Standard rate-limit headers accompany responses so you can stay under quota
without tripping a 429:
| Header | Meaning |
|---|---|
x-ratelimit-limit | Your ceiling for the current window. |
x-ratelimit-remaining | Requests left in the current window. |
x-ratelimit-reset | Seconds until the window resets. |
retry-after | On a 429, seconds to wait before retrying. |
When you exceed a limit, the API returns HTTP 429 Too Many Requests. The
body identifies the condition and tells you how long to wait:
{
"statusCode": 429,
"error": "Too Many Requests",
"message": "Rate limit exceeded. Retry in 42s.",
"retryAfter": 42
}
Some endpoints that enforce a daily quota (AI/voice polish) return a domain-specific code and a reset timestamp instead:
{
"error": "rate_limit_exceeded",
"message": "Daily polish limit reached.",
"resets_at": "2026-04-10T00:00:00.000Z"
}
Handling 429s
- Read
retry-after(orretryAfterin the body) and wait at least that long. - Use exponential backoff with jitter for repeated
429s. - Cache read responses client-side; the feed, APYs, and rates change on the order of minutes, not milliseconds.
- For continuous narrative ingestion, prefer the Agents feed over tight polling of the public feed.
async function getWithRetry(url: string, init?: RequestInit, attempt = 0): Promise<Response> {
const res = await fetch(url, init);
if (res.status !== 429) return res;
const retryAfter = Number(res.headers.get('retry-after') ?? 2 ** attempt);
await new Promise((r) => setTimeout(r, retryAfter * 1000 + Math.random() * 250));
return getWithRetry(url, init, attempt + 1);
}
Need more throughput?
Elevated quotas are organized into access tiers. See Tiers for the tier comparison and how to request elevated access. During beta, all keys sit on the Free tier and paid upgrades are not yet active.