← Limit Ladder
API
Tokens

Run Limit Ladder from your own code

Both metered lanes are available over HTTP and each returns one JSON object. The division of labour: the page normalises every published rate, catches burst equal to sustained, works out refill times and the per-region multiplier, diffs documented against enforced limits, reads a pasted throttle response, and ranks the algorithm, the header family and the fairness layers, then hands it all to the run as facts. The run writes the policy or the rollout; the page then holds it to its own facts (llreply.js) and builds the example 429 from the reply's tiers - the model writes no header values.

Base URL and the response shape

Every endpoint lives under https://api.skillsafe.ai/v1/app-api, and every response has one shape:

{ "ok": true,  "data":  { ... } }
{ "ok": false, "error": { "code": "...", "message": "..." } }

The token is minted for this app (the guest call takes {"slug":"limit-ladder"}), so no slug header is needed afterwards. Send it as Authorization: Bearer YOUR_TOKEN. The run body is the input object, sent bare.

StatusCodeMeaning
400validation_errorThe body is not a JSON object, or a required key (task, api, limits, facts) is missing. The page always sends all five of task, api, limits, carry, facts; carry is an empty string for policy.
401unauthorizedNo token, or a stale one. Mint a guest token or sign in again.
402insufficient_creditsThe balance is under min_credits. Price with /estimate first.
403forbiddenA guest token tried to run: both lanes are metered and need a personal sign-in.
404not_foundUnknown job id.
429rate_limitedBack off and retry.
5xxserver_errorTransient. Retry with the same Idempotency-Key so a retry never double-bills.

The task field comes first

Two lanes, chosen by task, in the order the work runs - the limits, then how they change:

A missing or unknown task is answered as policy. Both lanes send the same five fields. The facts are the page's checks, one row per line, and a line starting "must:" is a rule the reply has to hold. The page's handoff carries the policy's decisions into carry for the rollout lane.

The input

FieldRequiredWhat
taskyespolicy or rollout. If it is missing or neither, do policy and set "task": "policy".
apiyesthe API's name and purpose and the interview: the paradigm, greenfield or existing, abuse pressure, a noisy neighbour, a fragile downstream, internal use, regions, what the edge emits, whether callers use your SDKs, the timing and the effort ceiling; "not given" means not given.
limitsyesthe tier table as the page read it (each tier's key, sustained rate, burst, refill time, scope and findings), the carve-outs, the enforced-versus-documented diff, the algorithm and headers today, the guards, the pasted throttle response as read, the overrides and the change policy.
carrysend it (empty for policy)rollout task only: the policy's decisions, when the policy task ran first; may be empty.
factsyesthe page's checks, one line each as area | id | status | evidence, and the rungs the page ranked. A line starting "must:" applies to your reply.
retry_notenooptional; if present, your previous reply was unusable - follow it.

The rules the model applies

From the source skill, @samber/api-rate-limit-policy - the only standard the run applies:

House rules

The replies

policy

Write the published rate-limit policy - the redesign, not a transcript of today. Where the page flags a defect, publish the fixed value: choosing a burst, a scope or which of two drifting numbers to publish is a design decision, not an invented figure. Say in the tier's note what changed. When documented and enforced numbers differ, publish the one the gateway will enforce from now on and say so.

  1. summary - one sentence, at most 40 words: what an integrator cannot predict or observe today, and the first fix.
  2. metering - unit (requests, cost-points, concurrency or both), why (one sentence).
  3. algorithm - rung (the facts' rung: token, fixed, sliding or leaky), behavior (one sentence a caller can plan by: what happens to idle time and to bursts), why (one sentence).
  4. tiers - the published tier table, one object per tier: tier, key (ip, user, key, org, service or app), sustained (a number per unit, such as 5000/hour or 2/second), burst (a number, different from the sustained count), scope (global or per-region), note (one sentence, or "").
  5. carveouts - per-endpoint-class limits: objects with class and limit (a number per unit); [] when none apply.
  6. fairness - rung (the facts' rung: layers or deferred), layers (short strings: the layers in order; [] when deferred), shedding (one sentence: what a caller observes under load shedding, or that there is none).
  7. secondary - exists (true or false), signal (one sentence: how it signals when it fires, or "").
  8. headers - rung (the facts' rung: legacy, both or none), reset (epoch or seconds, or "" with no headers), why (one sentence). Write no header values - the page builds the example from your tiers.
  9. throttle - status (429), retry_after (delay-seconds), code (the machine-readable error code), which_limit (one sentence: how a caller tells which limit was hit), graphql (one sentence on a body-level GraphQL signal, or "" for REST only).
  10. sla - one sentence: the numbers are operational hints, not an SLA.
  11. caveats - at most 4 short strings; [] when there is nothing to say.
{
  "task": "policy",
  "summary": "one sentence",
  "metering": { "unit": "requests", "why": "one sentence" },
  "algorithm": { "rung": "token", "behavior": "one sentence", "why": "one sentence" },
  "tiers": [ { "tier": "free", "key": "ip", "sustained": "60/hour", "burst": 20, "scope": "global", "note": "" } ],
  "carveouts": [ { "class": "exports", "limit": "2/minute" } ],
  "fairness": { "rung": "layers", "layers": [ "tenant ceiling" ], "shedding": "one sentence" },
  "secondary": { "exists": false, "signal": "" },
  "headers": { "rung": "legacy", "reset": "seconds", "why": "one sentence" },
  "throttle": { "status": 429, "retry_after": "delay-seconds", "code": "rate_limit_exceeded", "which_limit": "one sentence", "graphql": "" },
  "sla": "one sentence",
  "caveats": [ ]
}

The page holds it to its own facts (check() in llreply.js; any warning makes the reply flagged): the reply answers the task it was sent; no URL; no backticks; no percentage; a summary of at most 40 words; the metering unit for the paradigm; the ranked algorithm with a behaviour sentence that describes it (a leaky bucket banks nothing); every listed tier present, each with a sustained rate written as a number per unit, a burst different from the sustained count, a key and a scope (a per-region limit says what it means globally); carve-outs as rates; the ranked fairness rung with its layers; a documented secondary guard when one exists; the ranked header family and the reset semantics; 429, Retry-After in delay-seconds, a code, which limit was hit when several apply, a GraphQL signal when GraphQL is in scope; hints, not an SLA.

rollout

Write the override path and the change rollout - prescriptive, fixing what the page flags. Channels and process steps are recommendations, not claims about today.

  1. summary - one sentence, at most 40 words.
  2. override - shape (ticket, spend, plan or burst), keyed_to (key, org or app - the organisation holding the contract when tiers key on different identities), process (one sentence: how a request is made and how long it takes), plan_implies (one sentence: whether a higher plan implies higher limits), untouchable (one sentence: which limit no override changes), reserve (one sentence: the right to lower an override).
  3. notice - days (at least 42), emergency (one sentence: emergency reductions reserved in writing), channels (short strings).
  4. phases - exactly three objects, in order: phase (observe, baseline, refine), action (one sentence), exit (one sentence: what moves it to the next phase).
  5. retry - two to four strings: the published retry guidance.
  6. gates - exactly two objects: gate (machine-readable or parity, each once), test (one sentence: how it is checked).
  7. trends - two to four strings, watched, never gated.
  8. caveats - at most 4 short strings; [] when there is nothing to say.
{
  "task": "rollout",
  "summary": "one sentence",
  "override": { "shape": "ticket", "keyed_to": "org", "process": "one sentence", "plan_implies": "one sentence", "untouchable": "one sentence", "reserve": "one sentence" },
  "notice": { "days": 42, "emergency": "one sentence", "channels": [ "changelog" ] },
  "phases": [ { "phase": "observe", "action": "one sentence", "exit": "one sentence" } ],
  "retry": [ "Honour Retry-After" ],
  "gates": [ { "gate": "machine-readable", "test": "one sentence" } ],
  "trends": [ "per-tier 429 rate" ],
  "caveats": [ ]
}

The page requires: the reply answers the task it was sent; the same text rules; a named override shape (never contact-sales with no process) keyed to a key, org or app, with its process, plan, untouchable-limit and reserve sentences; at least 42 days' notice and emergency reductions reserved; exactly observe, baseline, refine in order, each with an action and an exit; retry guidance with Retry-After and jitter; the two gates once each with a test; two to four trends.

Worked examples

Three runs from the page's free samples (invented APIs): the policy for a REST commerce API in three regions, the rollout for the same API carrying that policy, and the policy for a GraphQL API under scraping. Each input is exactly what the page sends; each reply is the saved reply the sample replays, shipped unedited - but the facts and long strings are shortened in this display only, and every cut is listed under the example. The last line is the page's own check of the full reply.

policy - A REST commerce API in three regions

The page read it as: integrators throttle blind (8 fail, 6 warn, 10 pass), 3 tiers; algorithm by rank: token; headers by rank: legacy; fairness: layers; failing checks: naming, twonumbers, regions, drift, secondary, tenant, resetmatch, override.

Input (facts is 2910 characters; the first 1395 are shown):

{
 "task": "policy",
 "api": "API: Zarvocart\nWhat it does: Catalogue, cart and order APIs for online shops; agencies run nightly catalogue syncs for many shops at once.\nParadigm: REST\nLimits: already enforced\nAbuse or scraping pressure: no; a noisy neighbour has degraded others: yes; a fragile downstream: no; internal and low-stakes: no\nRegions: 3; the edge emits IETF structured fields: no; most callers use our own SDKs: no; timing: A contract run for years; low effort ceiling: no",
 "limits": "Tiers (tier | key | sustained | burst | scope | the page's findings):\n- free | keyed on IP address | 60 per hour (0.017/s) | burst 60 (refills in 60 min) | scope not stated | burst equals sustained - legitimate spikes have no headroom; the scope is not stated, and the API runs in 3 regions\n- paid | keyed on API key | 100 per second (100/s) | burst 200 (refills in 2 s) | region | enforced per region: real global throughput is 3x the published number\n- enterprise | keyed on organisation | 500 per second (500/s) | burst 1000 (refills in 2 s) | global\nEndpoint-class carve-outs:\n- catalogue export | 2 per minute\nEnforced vs documented: paid - documented 100 per second, enforced 80 per second\nAlgorithm: Token bucket; idle time banks burst: no; headers: The legacy X-RateLimit-* trio; reset semantics: seconds\nSecondary guard: yes, documented no; load shedding: no; tenant ceiling no, per-user limit no\nThrottle response pasted: status 429, Retry-After date (Sun, 01 Nov 2026 07:28:00 GMT), legacy headers 3 of 3, IETF fields none, code rate_limit_exceeded, which-limit header none\nOverrides: \"Contact sales\" with no process; keyed to the principal no; notice before lowering a limit: none; reset semantics changed silently: no; limits cited as an SLA: no; retry guidance published: no",
 "carry": "",
 "facts": "The page's checks (8 fail, 6 warn, 10 pass, 0 n/a):\n- Metering and algorithm | unit | pass | requests per window\n- Metering and algorithm | algo | pass | Token bucket\n- Metering and algorithm | naming | fail | called a token bucket, but idle time banks no burst - that is a leaky bucket\n- Tiers | twonumbers | fail | free (burst equals sustained - legitimate spikes have no headroom)\n- Tiers | keys | pass | free by IP address; paid by API key; enterprise by organisation\n- Tiers | regions | fail | free states no scope; paid is per region - 300 per second globally\n- Tiers | drift | fail | paid: documented 100 per second, enforced 80 per second\n- Tiers | secondary | fail | exists but is discoverable only by tripping it\n- Tiers | sla | pass | hints\n- Fairness | tenant | fail | one shared pool - noisy neighbour by design\n- Fairness | peruser | warn | one user can spend the whole tenant budget\n- Fairness | carveouts | pass | catalogue export 2 per minute\n- Headers and the throttle response | resetsem | pass | seconds remaining\n- Headers and the throttle response | status | pass | 429\n- Headers and the throttle response | retryafter | warn | an HTTP-date - clients parseInt it into NaN and retry-storm\n- Headers and the throttle response | headersseen | pass | legacy: 3 of 3; no IETF fields\n- Headers and the throttle response | resetmatch | fail | documented as seconds, but 1793604480\n\n[... 1515 more characters cut from this display ...]"
}

Reply:

{
 "task": "policy",
 "summary": "Integrators cannot tell which limit stopped them, parse the HTTP-date Retry-After into NaN, and meet a paid tier enforced below its documented rate; first send Retry-After in delay-seconds and publish the enforced numbers.",
 "metering": {
  "unit": "requests",
  "why": "Zarvocart is a REST API, so every call counts as one request against the caller's tier."
 },
 "algorithm": {
  "rung": "token",
  "behavior": "Each key's bucket holds its burst and refills at its sustained rate, so idle time banks burst capacity up to the bucket depth and a full bucket absorbs a spike at once.",
  "why": "The default rung: burst and sustained fall straight out of the bucket, and nightly catalogue syncs that start after a quiet day are rewarded rather than cut off."
 },
 "tiers": [
  {
   "tier": "free",
   "key": "ip",
   "sustained": "60/hour",
   "burst": 120,
   "scope": "global",
   "note": "Burst raised from 60 to 120 to give headroom above the hourly count, the same two-to-one depth the paid and enterprise tiers already have."
  },
  {
   "tier": "paid",
   "key": "key",
   "sustained": "80/second",
   "burst": 200,
   "scope": "per-region",
   "note": "Published at the enforced 80 per second rather than the documented 100; enforced separately in each of the 3 regions, so a caller spread across all three can reach 240 per second."
  },
  {
   "tier": "enterprise",
   "key": "org",
   "sustained": "500/second",
   "burst": 1000,
   "scope": "global",
   "note": "One budget per organisation across all 3 regions."
  }
 ],
 "carveouts": [
  {
   "class": "catalogue export",
   "limit": "2/minute"
  }
 ],
 "fairness": {
  "rung": "layers",
  "layers": [
   "tenant ceiling per organisation",
   "per-key limit inside the tenant's allocation",
   "catalogue export carve-out outside the general budget"
  ],
  "shedding": "There is no load shedding; every throttle comes from a published limit."
 },
 "secondary": {
  "exists": true,
  "signal": "It answers 429 with Retry-After in delay-seconds and code rate_limit_exceeded, and the error body names the secondary guard as the limit hit."
 },
 "headers": {
  "rung": "legacy",
  "reset": "seconds",
  "why": "The legacy trio is what clients already parse, and Reset is documented as seconds remaining, so the gateway must stop emitting epoch values in it."
 },
 "throttle": {
  "status": 429,
  "retry_after": "delay-seconds",
  "code": "rate_limit_exceeded",
  "which_limit": "The error body names the limit that was hit: the tier, the catalogue export carve-out or the secondary guard.",
  "graphql": ""
 },
 "sla": "These numbers are operational hints for planning traffic, not an SLA or a capacity guarantee.",
 "caveats": [
  "The gateway banks no idle time today, which is leaky-bucket behaviour; the token-bucket promise holds only once idle time refills the bucket.",
  "Reset values currently read as epoch timestamps although seconds is documented; switching them is a change to announce, not a silent fix.",
  "The free tier's enforcement scope was never stated; publishing it as global needs the gateway to enforce it globally.",
  "No published retry guidance exists yet; the rollout has to publish it alongside this table."
 ]
}

What step 5 prints for it: token | legacy | free 60/hour burst 120 | paid 80/second burst 200 | enterprise 500/second burst 1000

The page's check: kept - nothing to flag

rollout - A REST commerce API in three regions

The input carries the saved policy's decisions (R.laneForm fills carry from the policy reply), as the page's handoff does.

Input (facts is 2870 characters; the first 1395 are shown):

{
 "task": "rollout",
 "api": "API: Zarvocart\nWhat it does: Catalogue, cart and order APIs for online shops; agencies run nightly catalogue syncs for many shops at once.\nParadigm: REST\nLimits: already enforced\nAbuse or scraping pressure: no; a noisy neighbour has degraded others: yes; a fragile downstream: no; internal and low-stakes: no\nRegions: 3; the edge emits IETF structured fields: no; most callers use our own SDKs: no; timing: A contract run for years; low effort ceiling: no",
 "limits": "Tiers (tier | key | sustained | burst | scope | the page's findings):\n- free | keyed on IP address | 60 per hour (0.017/s) | burst 60 (refills in 60 min) | scope not stated | burst equals sustained - legitimate spikes have no headroom; the scope is not stated, and the API runs in 3 regions\n- paid | keyed on API key | 100 per second (100/s) | burst 200 (refills in 2 s) | region | enforced per region: real global throughput is 3x the published number\n- enterprise | keyed on organisation | 500 per second (500/s) | burst 1000 (refills in 2 s) | global\nEndpoint-class carve-outs:\n- catalogue export | 2 per minute\nEnforced vs documented: paid - documented 100 per second, enforced 80 per second\nAlgorithm: Token bucket; idle time banks burst: no; headers: The legacy X-RateLimit-* trio; reset semantics: seconds\nSecondary guard: yes, documented no; load shedding: no; tenant ceiling no, per-user limit no\nThrottle response pasted: status 429, Retry-After date (Sun, 01 Nov 2026 07:28:00 GMT), legacy headers 3 of 3, IETF fields none, code rate_limit_exceeded, which-limit header none\nOverrides: \"Contact sales\" with no process; keyed to the principal no; notice before lowering a limit: none; reset semantics changed silently: no; limits cited as an SLA: no; retry guidance published: no",
 "carry": "Algorithm: token - Each key's bucket holds its burst and refills at its sustained rate, so idle time banks burst capacity up to the bucket depth and a full bucket absorbs a spike at once.\nTiers: free 60/hour burst 120 by ip (global); paid 80/second burst 200 by key (per-region); enterprise 500/second burst 1000 by org (global)\nHeaders: legacy, Reset as seconds; throttle 429 with Retry-After in seconds, code rate_limit_exceeded\nFairness: layers - tenant ceiling per organisation, per-key limit inside the tenant's allocation, catalogue export carve-out outside the general budget\nSecondary guard: It answers 429 with Retry-After in delay-seconds and code rate_limit_exceeded, and the error body names the secondary guard as the limit hit.",
 "facts": "The page's checks (8 fail, 6 warn, 10 pass, 0 n/a):\n- Metering and algorithm | unit | pass | requests per window\n- Metering and algorithm | algo | pass | Token bucket\n- Metering and algorithm | naming | fail | called a token bucket, but idle time banks no burst - that is a leaky bucket\n- Tiers | twonumbers | fail | free (burst equals sustained - legitimate spikes have no headroom)\n- Tiers | keys | pass | free by IP address; paid by API key; enterprise by organisation\n- Tiers | regions | fail | free states no scope; paid is per region - 300 per second globally\n- Tiers | drift | fail | paid: documented 100 per second, enforced 80 per second\n- Tiers | secondary | fail | exists but is discoverable only by tripping it\n- Tiers | sla | pass | hints\n- Fairness | tenant | fail | one shared pool - noisy neighbour by design\n- Fairness | peruser | warn | one user can spend the whole tenant budget\n- Fairness | carveouts | pass | catalogue export 2 per minute\n- Headers and the throttle response | resetsem | pass | seconds remaining\n- Headers and the throttle response | status | pass | 429\n- Headers and the throttle response | retryafter | warn | an HTTP-date - clients parseInt it into NaN and retry-storm\n- Headers and the throttle response | headersseen | pass | legacy: 3 of 3; no IETF fields\n- Headers and the throttle response | resetmatch | fail | documented as seconds, but 1793604480\n\n[... 1475 more characters cut from this display ...]"
}

Reply:

{
 "task": "rollout",
 "summary": "Zarvocart swaps contact sales for a ticket keyed to the organisation, promises six weeks' notice before any limit is lowered, and brings in the tenant ceiling and per-key limits by logging before enforcing.",
 "override": {
  "shape": "ticket",
  "keyed_to": "org",
  "process": "An enterprise organisation files a limit ticket naming the budget it needs and why, and gets an answer within a lead time published on the limits page.",
  "plan_implies": "Yes: free, paid and enterprise each publish higher numbers, and the ticket only raises an organisation's budget above the enterprise row.",
  "untouchable": "No override lifts the secondary guard or the catalogue export carve-out.",
  "reserve": "Zarvocart may lower an override after the notice period, or at once under the emergency reservation."
 },
 "notice": {
  "days": 42,
  "emergency": "Zarvocart reserves in writing the right to lower a limit without notice when it protects the platform, and says so in the changelog when it does.",
  "channels": [
   "changelog",
   "email to each organisation's technical contact",
   "limits page"
  ]
 },
 "phases": [
  {
   "phase": "observe",
   "action": "Log what the tenant ceiling, the per-key limit and the secondary guard would throttle for each organisation, enforcing nothing new.",
   "exit": "The logs cover a full cycle of the agencies' nightly catalogue syncs."
  },
  {
   "phase": "baseline",
   "action": "Enforce the tenant ceiling and per-key limit at a high ceiling above every peak seen in the logs, returning the documented 429.",
   "exit": "The per-tier 429 rate has settled and the noisy neighbour no longer degrades other tenants."
  },
  {
   "phase": "refine",
   "action": "Tighten the ceilings toward the observed data, announcing every lowering on six weeks' notice.",
   "exit": "The published table matches what the gateway enforces for every tier, carve-out and guard."
  }
 ],
 "retry": [
  "Honour Retry-After, which is always a number of seconds to wait.",
  "Without it, back off exponentially with full jitter.",
  "Keys that keep retrying inside the Retry-After window trip the secondary guard."
 ],
 "gates": [
  {
   "gate": "machine-readable",
   "test": "Trip every tier, the catalogue export carve-out and the secondary guard in a test; each must return 429, Retry-After in seconds, code rate_limit_exceeded and the name of the limit hit."
  },
  {
   "gate": "parity",
   "test": "Drive each tier to its limit and compare the enforced sustained rate, burst and scope with the published table, secondary guard included."
  }
 ],
 "trends": [
  "per-tier 429 rate",
  "retries inside the Retry-After window",
  "limit-related tickets",
  "override requests per quarter"
 ],
 "caveats": [
  "Publishing paid at 80 per second lowers the documented number, so it takes the six weeks' notice even though 80 is what is enforced.",
  "Switching Reset from epoch values to seconds and Retry-After from a date to seconds both change what clients see; announce them.",
  "The ticket's lead time is not set yet; it has to be a number before the process is published."
 ]
}

What step 5 prints for it: ticket keyed to org | 42 days notice | observe | baseline | refine

The page's check: kept - nothing to flag

policy - A GraphQL API under scraping

The page read it as: integrators throttle blind (3 fail, 6 warn, 15 pass), 2 tiers; algorithm by rank: sliding; headers by rank: both; fairness: layers; failing checks: status, retryafter, gql200.

Input (facts is 2819 characters; the first 1398 are shown):

{
 "task": "policy",
 "api": "API: Quibblegraph\nWhat it does: A public GraphQL API over a product catalogue; competitors scrape it, and partner apps sync it hourly.\nParadigm: GraphQL; GraphQL cost formula published: no\nLimits: already enforced\nAbuse or scraping pressure: yes; a noisy neighbour has degraded others: no; a fragile downstream: no; internal and low-stakes: no\nRegions: 1; the edge emits IETF structured fields: yes; most callers use our own SDKs: no; timing: A contract run for years; low effort ceiling: no",
 "limits": "Tiers (tier | key | sustained | burst | scope | the page's findings):\n- free | keyed on user or token | 1000 per minute (16.7/s) | burst 2000 (refills in 120 s) | global\n- partner | keyed on app installation | 5000 per minute (83.3/s) | burst 10000 (refills in 2 min) | global\nEndpoint-class carve-outs: none\nEnforced vs documented: no drift\nAlgorithm: Fixed window; idle time banks burst: unknown; headers: The legacy X-RateLimit-* trio; reset semantics: epoch\nSecondary guard: no; load shedding: yes, admitted in the policy no; tenant ceiling yes, per-user limit yes\nThrottle response pasted: status 200, Retry-After missing, legacy headers 3 of 3, IETF fields none, code THROTTLED, which-limit header none, a throttle inside 200; GraphQL throttles inside 200, documented no\nOverrides: Spend-based auto-promotion; keyed to the principal yes; notice before lowering a limit: 30 days; reset semantics changed silently: no; limits cited as an SLA: no; retry guidance published: yes",
 "carry": "",
 "facts": "The page's checks (3 fail, 6 warn, 15 pass, 0 n/a):\n- Metering and algorithm | unit | warn | GraphQL is metered in cost points - publish the cost formula\n- Metering and algorithm | algo | pass | Fixed window\n- Metering and algorithm | boundary | warn | up to 2000 points in one minute across a boundary for free, and throttled clients retry together at the reset\n- Tiers | twonumbers | pass | free: 1000 per minute, burst 2000; partner: 5000 per minute, burst 10000\n- Tiers | keys | pass | free by user or token; partner by app installation\n- Tiers | drift | pass | no drift\n- Tiers | secondary | pass | none\n- Tiers | sla | pass | hints\n- Fairness | tenant | pass | per tenant\n- Fairness | peruser | pass | yes\n- Fairness | carveouts | warn | none listed\n- Fairness | shedding | warn | a well-behaved tenant can be shed under fleet stress - a fairness promise that omits it overclaims\n- Headers and the throttle response | resetsem | pass | an epoch timestamp\n- Headers and the throttle response | status | fail | a throttle inside HTTP 200 - a status-code-only client reads it as success\n- Headers and the throttle response | retryafter | fail | no Retry-After\n- Headers and the throttle response | headersseen | pass | legacy: 3 of 3; no IETF fields\n- Headers and the throttle response | code | pass | code THROTTLED\n- Headers and the throttle response | which | warn | several limits, no header\n\n[... 1421 more characters cut from this display ...]"
}

Reply:

{
 "task": "policy",
 "summary": "A status-code client reads Quibblegraph's throttle as success, gets no Retry-After and cannot price a query without a cost formula; first answer every throttle with 429 and Retry-After in delay-seconds, then publish the formula.",
 "metering": {
  "unit": "cost-points",
  "why": "GraphQL queries vary widely in cost, so each is priced in cost points against a budget, which only works once the cost formula is published."
 },
 "algorithm": {
  "rung": "sliding",
  "behavior": "Points are counted over a trailing window that moves with every query, so there is no reset boundary to double up on, idle time banks nothing, and Retry-After says exactly when enough points age out.",
  "why": "Competitors scrape the catalogue, and the boundary-proof sliding window ends the double spend and the reset stampede of the fixed window."
 },
 "tiers": [
  {
   "tier": "free",
   "key": "user",
   "sustained": "60000 points/hour",
   "burst": 2000,
   "scope": "global",
   "note": "Up to 2000 points in any trailing minute and 60000 in any trailing hour, the same 1000 per minute average and 2000 burst published today."
  },
  {
   "tier": "partner",
   "key": "app",
   "sustained": "300000 points/hour",
   "burst": 10000,
   "scope": "global",
   "note": "Keyed on the app installation: up to 10000 points in any trailing minute and 300000 in any trailing hour, the same 5000 per minute average as today."
  }
 ],
 "carveouts": [],
 "fairness": {
  "rung": "layers",
  "layers": [
   "tenant ceiling per app installation",
   "per-user limit inside the tenant's budget"
  ],
  "shedding": "Under fleet stress the platform sheds load by system state, not by caller, so a tenant inside its budget can still be refused; a shed request carries its own error code, never THROTTLED."
 },
 "secondary": {
  "exists": false,
  "signal": ""
 },
 "headers": {
  "rung": "both",
  "reset": "epoch",
  "why": "The edge emits the IETF fields cheaply, so the legacy trio stays for existing clients and RateLimit-Policy lets a client pace itself before its first 429; Reset stays an epoch timestamp."
 },
 "throttle": {
  "status": 429,
  "retry_after": "delay-seconds",
  "code": "THROTTLED",
  "which_limit": "The error body names the budget that was hit, the tenant ceiling or the per-user limit.",
  "graphql": "A throttled query answers HTTP 429 with THROTTLED in the errors array, never inside a 200, and the limits page documents that body-level code as loudly as the status."
 },
 "sla": "These budgets are operational hints for pacing queries, not an SLA.",
 "caveats": [
  "No cost formula is published, so a caller cannot yet predict a query's price; the budgets need it beside them.",
  "Moving the throttle from 200 to 429 changes what existing clients see; announce it before it lands.",
  "A sliding window costs more compute per query than the fixed window it replaces."
 ]
}

What step 5 prints for it: sliding | both | free 60000 points/hour burst 2000 | partner 300000 points/hour burst 10000

The page's check: kept - nothing to flag

Build the input yourself

Load the engine in node (global.window = global; require("./llkit.js"); require("./llreply.js")), then:

const K = LimitKit, R = LimitReply;
const f = K.form(Object.assign(K.blank(), { name: "Acme", tiers: "free | IP address | 60/hour | 20 | global\npaid | API key | 50/second | 100 | global",
  response: "HTTP/1.1 429 Too Many Requests\nRetry-After: 2\n\n{}" }));
// f.tierRows (each tier normalised: perSec, refill, issues), f.resp (the throttle response read), f.algoR, f.headR, f.fair
// K.limitsMd(f) - the published table; K.exampleHeaders(f) - an example 429 in the ranked header family
const b = K.buildInput(f, "policy");                         // or "rollout" (R.laneForm(form, "rollout", policy) fills carry)
const wire = K.toWire(b.input);                              // POST this to /estimate and /run
// then: const reply = R.normalize(R.parseReply(job.output.output), "policy");  // reply.example is built by the page
// R.check(reply, b)                                         // {status: kept or flagged, notes}

Get a token for the steps below from /tokens.html.

Step 1 — a tiny client helper

One function that sends JSON and unwraps the response. Paste a personal token from /tokens.html, or leave it empty and mint a guest token in step 2.

BASE="https://api.skillsafe.ai/v1/app-api"
TOKEN="YOUR_TOKEN"   # from https://limit-ladder.skillsafe.ai/tokens.html
api() { # api METHOD PATH [JSON-BODY]
  curl -s -X "$1" "$BASE$2" -H "Content-Type: application/json" \
    ${TOKEN:+-H "Authorization: Bearer $TOKEN"} ${3:+--data "$3"}
}

Step 2 — a token

A guest token is enough for /me and /estimate. A run is metered, so it needs a personal token: sign in on /tokens.html and copy it from there.

# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN=$(curl -s -X POST "$BASE/guest" -H "Content-Type: application/json" -d '{"slug":"limit-ladder"}' | jq -r '.data.token')

Step 3 — GET /me

Who the token is (subject_type: user or guest) and the credit balance.

api GET /me | jq '.data | {subject_type, credits}' 

Step 4 — POST /estimate

Free. The body is the run input itself, the bare object (never wrapped in {"input": ...}). hold_credits is a reservation, not the price; min_credits is what the balance must cover; model_alias reads gpt-terra.

# input.json: the run input object - {"task": "policy", "api": "API: Zarvocart\nParadigm: REST\n...", "limits": "Tiers (tier | key | sustained | burst | scope | the page's findings):\n- free | keyed on IP address | 60 per hour ...", "carry": "", "facts": "The page's checks (8 fail, ...):\n- Metering and algorithm | unit | pass | ..."}
# - see the input table
api POST /estimate "$(cat input.json)" | jq '.data | {hold_credits, min_credits, model, model_alias, markup_bps, warnings}' 

Step 5 — POST /run, then poll the job

Send an Idempotency-Key built from the lane and the input: a retry with the same key never bills twice. The page uses limit-ladder:<task>:<inputHash>:a<attempt> - the lane is part of the key, retry_note is left out of the hash - and its one automatic reformat retry is a new attempt number, named in the page as one extra run. A policy reply carries summary, metering, algorithm, tiers, carveouts, fairness, secondary, headers, throttle, sla and caveats; a rollout reply carries summary, override, notice, phases, retry, gates, trends and caveats. The code below prints the gist of either: the algorithm and each tier, or the override shape and each phase.

KEY="limit-ladder:$(jq -r .task input.json):$(shasum -a 256 input.json | cut -c1-16):a1"
JOB=$(curl -s -X POST "$BASE/run" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
  -H "Idempotency-Key: $KEY" --data @input.json | jq -r '.data.job_id')
while :; do
  J=$(api GET "/jobs/$JOB"); S=$(echo "$J" | jq -r '.data.status')
  case "$S" in succeeded|failed|cancelled) break;; esac; sleep 2
done
echo "$J" | jq -r '.data.output.output' > reply.json   # one JSON object, the reply contract
echo "$J" | jq '.data | {charged_credits, truncated}'
jq -r 'if .task == "rollout" then (.override.shape, (.phases[] | "\(.phase): \(.action)")) 
       else (.algorithm.rung, (.tiers[] | "\(.tier): \(.sustained), burst \(.burst)")) end' reply.json

Step 6 — POST /run-stream

The same run as Server-Sent Events, with the same input.json. Browsers receive progress ticks rather than text deltas; the final done event carries the reply.

# Server-Sent Events: tick/delta events while the job runs, then a final event with the job
curl -N -s -X POST "$BASE/run-stream" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
  -H "Idempotency-Key: limit-ladder:$(jq -r .task input.json):$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json