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.
| Status | Code | Meaning |
|---|---|---|
| 400 | validation_error | The 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. |
| 401 | unauthorized | No token, or a stale one. Mint a guest token or sign in again. |
| 402 | insufficient_credits | The balance is under min_credits. Price with /estimate first. |
| 403 | forbidden | A guest token tried to run: both lanes are metered and need a personal sign-in. |
| 404 | not_found | Unknown job id. |
| 429 | rate_limited | Back off and retry. |
| 5xx | server_error | Transient. 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:
policy- the published policy: metering, the algorithm and its behaviour, the tier table with two numbers, a key and a scope per tier, carve-outs, fairness, the secondary guard, the header family and the throttle response.rollout- the override path, notice before a limit is lowered, the observe-baseline-refine rollout, retry guidance, the two gates and the trends.
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
| Field | Required | What |
|---|---|---|
task | yes | policy or rollout. If it is missing or neither, do policy and set "task": "policy". |
api | yes | the 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. |
limits | yes | the 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. |
carry | send it (empty for policy) | rollout task only: the policy's decisions, when the policy task ran first; may be empty. |
facts | yes | the 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_note | no | optional; 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:
- Two metering models, never blended: REST counts requests; GraphQL prices each query in cost points against a budget, with a published cost formula. When both exist, publish both and say what they share. Name the unit even when it is concurrency.
- The algorithm is what the number promises. Ranked: token bucket by default (bucket depth is the burst, refill rate the sustained quota; idle time banks burst capacity); fixed window only for internal, low-stakes limits (up to twice the limit at the window boundary, and throttled clients stampede at the reset); sliding window under scraping or adversarial pressure (boundary-proof, the most precise Retry-After, the highest compute cost); leaky bucket when a burst is a threat to a fragile downstream (constant output, idle time banks nothing). A hard date keeps whatever the gateway already enforces. Describe the behaviour, never rely on the name alone - token and leaky bucket get inverted.
- Tiers: model limits as identity or auth scope, times endpoint class, times account override, and decide whether plan is an input. Publish burst and sustained as two explicit numbers - burst equal to sustained breaks legitimate spikes. The key changes with the tier: IP for anonymous, user or token for authenticated, API key or organisation for paid, service identity for internal. Carve expensive endpoint classes out of the general budget. A secondary abuse guard is legitimate but documented, with how it signals. State whether a limit is global or per region - per-region enforcement multiplies a caller's real throughput by the region count. The numbers are operational hints, not an SLA.
- Fairness: a tenant-level ceiling, a per-user limit inside the tenant's allocation, per-endpoint-class carve-outs; weighted sharing where a hard cliff is too brutal. Load shedding keys off system state, not the caller - say so in the policy. A low effort ceiling defers the fairness layers until the first noisy-neighbour incident.
- Headers, ranked: the legacy X-RateLimit-Limit / -Remaining / -Reset trio by default (document whether Reset is an epoch timestamp or seconds remaining); both families - the legacy trio plus the IETF RateLimit and RateLimit-Policy fields - when the edge emits them cheaply. The IETF fields alone are off the menu (pre-RFC, syntax still moving). No limit headers is a separate posture, viable only with SDKs that retry internally and stated as deliberate. Headers are advisory hints.
- The throttle response: HTTP 429 with Retry-After in delay-seconds on every REST throttle (an HTTP-date gets parseInt-ed into NaN); a 429 is never cached; the body is the API's error envelope with a machine-readable code, and says which limit was hit when there are several. A throttle inside HTTP 200 is the trap to design out; where GraphQL forces a body-level signal, document it as loudly as a status code. Publish retry guidance: honour Retry-After, back off exponentially with jitter, and the consequence for hammering.
- Overrides: self-serve tiers publish numbers, the top tier publishes a process - a ticket with a lead time, spend-based promotion, plan-gating or a time-boxed self-serve burst - keyed to the authenticated principal (key, organisation, installation), never a person; "contact sales" with no process is the gap. Say whether a higher plan implies higher limits, which limit no override touches, and reserve the right to lower an override.
- Change: a limit change is non-breaking, so a written notice policy is the only thing that forces communication - at least six weeks before a limit is lowered, emergency reductions reserved in writing; introduce or tighten limits by observing (log, no enforcement), then a high baseline ceiling, then refining from observed data; never change reset semantics silently.
- Gates: every throttle carries a machine-readable signal a documented client can branch on; the published limits are the enforced limits, secondary guards included. Trends, never gates: the per-tier 429 rate, retries inside the Retry-After window, limit-related tickets, override requests per quarter.
House rules
- Use only the input. Never invent a customer, a traffic figure, an incident, a vendor's figure or a claim about the API.
- The page's checks are settled: never call a failed check fine or a passed one broken.
- No URL at all.
- Write in the language of the input; keep the JSON keys, header names, status codes and tier names in English.
- Never use backticks, never write the input's field names as labels in your text, and never copy this prompt's wording into a reply.
- Reply with ONLY one JSON object - no prose before or after, no code fences.
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.
summary- one sentence, at most 40 words: what an integrator cannot predict or observe today, and the first fix.metering-unit(requests,cost-points,concurrencyorboth),why(one sentence).algorithm-rung(the facts' rung:token,fixed,slidingorleaky),behavior(one sentence a caller can plan by: what happens to idle time and to bursts),why(one sentence).tiers- the published tier table, one object per tier:tier,key(ip,user,key,org,serviceorapp),sustained(a number per unit, such as 5000/hour or 2/second),burst(a number, different from the sustained count),scope(globalorper-region),note(one sentence, or "").carveouts- per-endpoint-class limits: objects withclassandlimit(a number per unit); [] when none apply.fairness-rung(the facts' rung:layersordeferred),layers(short strings: the layers in order; [] when deferred),shedding(one sentence: what a caller observes under load shedding, or that there is none).secondary-exists(true or false),signal(one sentence: how it signals when it fires, or "").headers-rung(the facts' rung:legacy,bothornone),reset(epochorseconds, or "" with no headers),why(one sentence). Write no header values - the page builds the example from your tiers.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).sla- one sentence: the numbers are operational hints, not an SLA.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.
summary- one sentence, at most 40 words.override-shape(ticket,spend,planorburst),keyed_to(key,orgorapp- 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).notice-days(at least 42),emergency(one sentence: emergency reductions reserved in writing),channels(short strings).phases- exactly three objects, in order:phase(observe,baseline,refine),action(one sentence),exit(one sentence: what moves it to the next phase).retry- two to four strings: the published retry guidance.gates- exactly two objects:gate(machine-readableorparity, each once),test(one sentence: how it is checked).trends- two to four strings, watched, never gated.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"}
}
import json, urllib.request
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "YOUR_TOKEN" # from https://limit-ladder.skillsafe.ai/tokens.html
def api(method, path, body=None, headers=None):
req = urllib.request.Request(BASE + path, method=method,
data=None if body is None else json.dumps(body).encode(),
headers={"Content-Type": "application/json", **({"Authorization": "Bearer " + TOKEN} if TOKEN else {}), **(headers or {})})
with urllib.request.urlopen(req) as r:
env = json.load(r)
if env.get("error"):
raise RuntimeError(env["error"])
return env["data"]
// limit-ladder.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "YOUR_TOKEN"; // from https://limit-ladder.skillsafe.ai/tokens.html
async function api(method, path, body, headers = {}) {
const r = await fetch(BASE + path, { method, headers: { "Content-Type": "application/json",
...(TOKEN ? { Authorization: "Bearer " + TOKEN } : {}), ...headers },
body: body === undefined ? undefined : JSON.stringify(body) });
const env = await r.json();
if (env.error) throw new Error(env.error.code + ": " + env.error.message);
return env.data;
}
package main
import ("bytes"; "encoding/json"; "fmt"; "net/http")
const base = "https://api.skillsafe.ai/v1/app-api"
var token = "YOUR_TOKEN" // from https://limit-ladder.skillsafe.ai/tokens.html
func api(method, path string, body any, headers map[string]string) (map[string]any, error) {
var buf bytes.Buffer
if body != nil { json.NewEncoder(&buf).Encode(body) }
req, _ := http.NewRequest(method, base+path, &buf)
req.Header.Set("Content-Type", "application/json")
if token != "" { req.Header.Set("Authorization", "Bearer "+token) }
for k, v := range headers { req.Header.Set(k, v) }
res, err := http.DefaultClient.Do(req)
if err != nil { return nil, err }
defer res.Body.Close()
var env struct { Data map[string]any; Error map[string]any }
json.NewDecoder(res.Body).Decode(&env)
if env.Error != nil { return nil, fmt.Errorf("%v", env.Error) }
return env.Data, nil
}
// Limit Ladder.java -- Java 17+, with com.fasterxml.jackson.core:jackson-databind
import com.fasterxml.jackson.databind.*;
import java.net.URI; import java.net.http.*; import java.util.*;
public class Limit Ladder {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "YOUR_TOKEN"; // from https://limit-ladder.skillsafe.ai/tokens.html
static final ObjectMapper M = new ObjectMapper();
static final HttpClient C = HttpClient.newHttpClient();
static JsonNode api(String method, String path, Object body, Map<String, String> headers) throws Exception {
var b = HttpRequest.newBuilder(URI.create(BASE + path)).header("Content-Type", "application/json");
if (!token.isEmpty()) b.header("Authorization", "Bearer " + token);
if (headers != null) headers.forEach(b::header);
b.method(method, body == null ? HttpRequest.BodyPublishers.noBody() : HttpRequest.BodyPublishers.ofString(M.writeValueAsString(body)));
JsonNode env = M.readTree(C.send(b.build(), HttpResponse.BodyHandlers.ofString()).body());
if (env.hasNonNull("error")) throw new RuntimeException(env.get("error").toString());
return env.get("data");
}
}
require "json"
require "net/http"
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "YOUR_TOKEN" # from https://limit-ladder.skillsafe.ai/tokens.html
def api(method, path, body = nil, headers = {})
uri = URI(BASE + path)
req = Net::HTTPGenericRequest.new(method, !body.nil?, true, uri.request_uri,
{ "Content-Type" => "application/json" }.merge(TOKEN.empty? ? {} : { "Authorization" => "Bearer #{TOKEN}" }).merge(headers))
req.body = JSON.generate(body) unless body.nil?
env = JSON.parse(Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }.body)
raise env["error"].to_s if env["error"]
env["data"]
end
<?php
const BASE = "https://api.skillsafe.ai/v1/app-api";
$TOKEN = "YOUR_TOKEN"; // from https://limit-ladder.skillsafe.ai/tokens.html
function api(string $method, string $path, $body = null, array $headers = []) {
global $TOKEN;
$h = ["Content-Type: application/json"];
if ($TOKEN) $h[] = "Authorization: Bearer $TOKEN";
foreach ($headers as $k => $v) $h[] = "$k: $v";
$ch = curl_init(BASE . $path);
curl_setopt_array($ch, [CURLOPT_CUSTOMREQUEST => $method, CURLOPT_HTTPHEADER => $h, CURLOPT_RETURNTRANSFER => true]);
if ($body !== null) curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
$env = json_decode(curl_exec($ch), true);
if (!empty($env["error"])) throw new Exception(json_encode($env["error"]));
return $env["data"];
}
// Program.cs -- .NET 6+ console app
using System.Net.Http.Json; using System.Text.Json; using System.Text.Json.Nodes;
static class Api {
public const string Base = "https://api.skillsafe.ai/v1/app-api";
public static string Token = "YOUR_TOKEN"; // from https://limit-ladder.skillsafe.ai/tokens.html
static readonly HttpClient C = new();
public static async Task<JsonNode?> Call(string method, string path, object? body = null, Dictionary<string, string>? headers = null) {
var req = new HttpRequestMessage(new HttpMethod(method), Base + path);
if (Token != "") req.Headers.Add("Authorization", "Bearer " + Token);
if (headers != null) foreach (var h in headers) req.Headers.Add(h.Key, h.Value);
if (body != null) req.Content = JsonContent.Create(body);
var env = JsonNode.Parse(await (await C.SendAsync(req)).Content.ReadAsStringAsync())!;
if (env["error"] is JsonNode e && e.GetValueKind() != JsonValueKind.Null) throw new Exception(e.ToJsonString());
return env["data"];
}
}
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')
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN = ""
TOKEN = api("POST", "/guest", {"slug": "limit-ladder"})["token"]
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN = "";
TOKEN = (await api("POST", "/guest", { slug: "limit-ladder" })).token;
// inside main: a guest token (enough for /me and /estimate); a run needs your personal token
token = ""
g, err := api("POST", "/guest", map[string]string{"slug": "limit-ladder"}, nil)
if err != nil { panic(err) }
token = g["token"].(string)
// inside main: a guest token (enough for /me and /estimate); a run needs your personal token
token = "";
token = api("POST", "/guest", Map.of("slug", "limit-ladder"), null).get("token").asText();
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN.replace("")
TOKEN.replace(api("POST", "/guest", { slug: "limit-ladder" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "limit-ladder"])["token"];
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
Api.Token = "";
Api.Token = (await Api.Call("POST", "/guest", new { slug = "limit-ladder" }))!["token"]!.GetValue<string>();
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}'
me = api("GET", "/me")
print(me["subject_type"], me.get("credits"))
const me = await api("GET", "/me");
console.log(me.subject_type, me.credits);
me, _ := api("GET", "/me", nil, nil)
fmt.Println(me["subject_type"], me["credits"])
JsonNode me = api("GET", "/me", null, null);
System.out.println(me.get("subject_type") + " " + me.get("credits"));
me = api("GET", "/me")
puts [me["subject_type"], me["credits"]].inspect
$me = api("GET", "/me");
echo $me["subject_type"], " ", $me["credits"] ?? "", "\n";
var me = await Api.Call("GET", "/me");
Console.WriteLine($"{me!["subject_type"]} {me["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}'
inp = json.load(open("input.json")) # {"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
est = api("POST", "/estimate", inp)
print(est["hold_credits"], est["min_credits"], est["model"], est.get("warnings"))
import { readFileSync } from "node:fs";
const input = JSON.parse(readFileSync("input.json", "utf8")); // {task: "policy" or "rollout", api, limits, carry, facts} (see the input table)
const est = await api("POST", "/estimate", input);
console.log(est.hold_credits, est.min_credits, est.model, est.warnings);
raw, _ := os.ReadFile("input.json") // add "os" to the imports
var input map[string]any
json.Unmarshal(raw, &input)
est, _ := api("POST", "/estimate", input, nil)
fmt.Println(est["hold_credits"], est["min_credits"], est["model"])
Map<String, Object> input = M.readValue(new java.io.File("input.json"), Map.class);
JsonNode est = api("POST", "/estimate", input, null);
System.out.println(est.get("hold_credits") + " " + est.get("min_credits") + " " + est.get("model"));
input = JSON.parse(File.read("input.json"))
est = api("POST", "/estimate", input)
puts [est["hold_credits"], est["min_credits"], est["model"]].inspect
$input = json_decode(file_get_contents("input.json"), true);
$est = api("POST", "/estimate", $input);
echo $est["hold_credits"], " ", $est["min_credits"], " ", $est["model"], "\n";
var input = JsonNode.Parse(File.ReadAllText("input.json"))!;
var est = await Api.Call("POST", "/estimate", input);
Console.WriteLine($"{est!["hold_credits"]} {est["min_credits"]} {est["model"]}");
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
import hashlib, time
key = "limit-ladder:" + inp["task"] + ":" + hashlib.sha256(json.dumps(inp).encode()).hexdigest()[:16] + ":a1"
job = api("POST", "/run", inp, {"Idempotency-Key": key})
while True:
j = api("GET", "/jobs/" + job["job_id"])
if j["status"] in ("succeeded", "failed", "cancelled"):
break
time.sleep(2)
reply = json.loads(j["output"]["output"]) # the reply as one JSON object
print(j["charged_credits"], j.get("truncated"), reply["task"])
if reply["task"] == "rollout":
print(reply["override"]["shape"])
for x in reply["phases"]:
print(x["phase"], x["action"])
else:
print(reply["algorithm"]["rung"])
for t in reply["tiers"]:
print(t["tier"], t["sustained"], t["burst"])
import { createHash } from "node:crypto";
const key = `limit-ladder:${input.task}:${createHash("sha256").update(JSON.stringify(input)).digest("hex").slice(0, 16)}:a1`;
const job = await api("POST", "/run", input, { "Idempotency-Key": key });
let j;
do { await new Promise((r) => setTimeout(r, 2000)); j = await api("GET", "/jobs/" + job.job_id); }
while (!["succeeded", "failed", "cancelled"].includes(j.status));
const reply = JSON.parse(j.output.output);
console.log(j.charged_credits, j.truncated, reply.task);
if (reply.task === "rollout") { console.log(reply.override.shape); for (const x of reply.phases) console.log(x.phase, x.action); }
else { console.log(reply.algorithm.rung); for (const t of reply.tiers) console.log(t.tier, t.sustained, t.burst); }
sum := sha256.Sum256(raw) // add "crypto/sha256", "encoding/hex", "time" to the imports
key := "limit-ladder:" + input["task"].(string) + ":" + hex.EncodeToString(sum[:8]) + ":a1"
job, _ := api("POST", "/run", input, map[string]string{"Idempotency-Key": key})
var j map[string]any
for {
time.Sleep(2 * time.Second)
j, _ = api("GET", "/jobs/"+job["job_id"].(string), nil, nil)
if s := j["status"]; s == "succeeded" || s == "failed" || s == "cancelled" { break }
}
var reply map[string]any
json.Unmarshal([]byte(j["output"].(map[string]any)["output"].(string)), &reply)
fmt.Println(j["charged_credits"], j["truncated"], reply["task"])
key := "tiers"
if reply["task"] == "rollout" {
key = "phases"
}
ls, _ := reply[key].([]any)
for _, x := range ls {
xx := x.(map[string]any)
fmt.Println(xx["tier"], xx["sustained"], xx["phase"], xx["action"])
}
var digest = java.security.MessageDigest.getInstance("SHA-256").digest(M.writeValueAsBytes(input));
String key = "limit-ladder:" + input.get("task") + ":" + HexFormat.of().formatHex(digest, 0, 8) + ":a1";
JsonNode job = api("POST", "/run", input, Map.of("Idempotency-Key", key)), j;
do { Thread.sleep(2000); j = api("GET", "/jobs/" + job.get("job_id").asText(), null, null); }
while (!Set.of("succeeded", "failed", "cancelled").contains(j.get("status").asText()));
JsonNode reply = M.readTree(j.get("output").get("output").asText());
System.out.println(j.get("charged_credits") + " " + reply.path("task").asText());
if (reply.path("task").asText().equals("rollout")) {
for (JsonNode x : reply.path("phases")) System.out.println(x.path("phase").asText() + " " + x.path("action").asText());
} else {
for (JsonNode x : reply.path("tiers")) System.out.println(x.path("tier").asText() + " " + x.path("sustained").asText());
}
require "digest"
key = "limit-ladder:#{input["task"]}:#{Digest::SHA256.hexdigest(JSON.generate(input))[0, 16]}:a1"
job = api("POST", "/run", input, { "Idempotency-Key" => key })
j = nil
loop do
sleep 2
j = api("GET", "/jobs/#{job["job_id"]}")
break if %w[succeeded failed cancelled].include?(j["status"])
end
reply = JSON.parse(j["output"]["output"])
puts [j["charged_credits"], j["truncated"], reply["task"]].inspect
if reply["task"] == "rollout"
reply["phases"].each { |x| puts "#{x["phase"]} #{x["action"]}" }
else
reply["tiers"].each { |x| puts "#{x["tier"]} #{x["sustained"]}" }
end
$key = "limit-ladder:" . $input["task"] . ":" . substr(hash("sha256", json_encode($input)), 0, 16) . ":a1";
$job = api("POST", "/run", $input, ["Idempotency-Key" => $key]);
do { sleep(2); $j = api("GET", "/jobs/" . $job["job_id"]); }
while (!in_array($j["status"], ["succeeded", "failed", "cancelled"]));
$reply = json_decode($j["output"]["output"], true);
echo $j["charged_credits"], " ", $reply["task"], "\n";
if ($reply["task"] === "rollout") { foreach ($reply["phases"] as $x) echo $x["phase"], " ", $x["action"], "\n"; }
else { foreach ($reply["tiers"] as $x) echo $x["tier"], " ", $x["sustained"], "\n"; }
var digest = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input.ToJsonString()));
var key = $"limit-ladder:{input["task"]}:{Convert.ToHexString(digest, 0, 8).ToLowerInvariant()}:a1";
var job = await Api.Call("POST", "/run", input, new() { ["Idempotency-Key"] = key });
JsonNode j;
do { await Task.Delay(2000); j = (await Api.Call("GET", "/jobs/" + job!["job_id"]))!; }
while (!new[] { "succeeded", "failed", "cancelled" }.Contains(j["status"]!.GetValue<string>()));
var reply = JsonNode.Parse(j["output"]!["output"]!.GetValue<string>())!;
var task = (string?)reply["task"];
Console.WriteLine($"{j["charged_credits"]} {task}");
if (task == "rollout") { foreach (var x in reply["phases"]!.AsArray()) Console.WriteLine($"{x!["phase"]} {x["action"]}"); }
else { foreach (var x in reply["tiers"]!.AsArray()) Console.WriteLine($"{x!["tier"]} {x["sustained"]}"); }
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
rev = json.load(open("input.json")) # the same run input as input.json: {"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
req = urllib.request.Request(BASE + "/run-stream", method="POST", data=json.dumps(rev).encode(),
headers={"Content-Type": "application/json", "Authorization": "Bearer " + TOKEN,
"Idempotency-Key": "limit-ladder:" + rev["task"] + ":" + hashlib.sha256(json.dumps(rev).encode()).hexdigest()[:16] + ":a1"})
with urllib.request.urlopen(req) as r:
for line in r:
line = line.decode().strip()
if line.startswith("data:"):
print(line[5:].strip()[:200])
const rev = JSON.parse(readFileSync("input.json", "utf8"));
const r = await fetch(BASE + "/run-stream", { method: "POST", body: JSON.stringify(rev), headers: {
"Content-Type": "application/json", Authorization: "Bearer " + TOKEN,
"Idempotency-Key": `limit-ladder:${rev.task}:${createHash("sha256").update(JSON.stringify(rev)).digest("hex").slice(0, 16)}:a1` } });
const dec = new TextDecoder();
for await (const chunk of r.body) for (const line of dec.decode(chunk).split("\n"))
if (line.startsWith("data:")) console.log(line.slice(5).trim().slice(0, 200));
rraw, _ := os.ReadFile("input.json") // add "bufio", "strings" to the imports
rs := sha256.Sum256(rraw)
req, _ := http.NewRequest("POST", base+"/run-stream", bytes.NewReader(rraw))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Idempotency-Key", "limit-ladder:"+input["task"].(string)+":"+hex.EncodeToString(rs[:8])+":a1")
res, _ := http.DefaultClient.Do(req)
sc := bufio.NewScanner(res.Body)
for sc.Scan() { if l := sc.Text(); strings.HasPrefix(l, "data:") { fmt.Println(l[5:]) } }
String rev = java.nio.file.Files.readString(java.nio.file.Path.of("input.json"));
var rd = java.security.MessageDigest.getInstance("SHA-256").digest(rev.getBytes());
var sreq = HttpRequest.newBuilder(URI.create(BASE + "/run-stream")).header("Content-Type", "application/json")
.header("Authorization", "Bearer " + token).header("Idempotency-Key", "limit-ladder:" + input.get("task") + ":" + HexFormat.of().formatHex(rd, 0, 8) + ":a1")
.POST(HttpRequest.BodyPublishers.ofString(rev)).build();
C.send(sreq, HttpResponse.BodyHandlers.ofLines()).body().filter(l -> l.startsWith("data:")).forEach(System.out::println);
rev = File.read("input.json")
uri = URI(BASE + "/run-stream")
req = Net::HTTP::Post.new(uri, "Content-Type" => "application/json", "Authorization" => "Bearer #{TOKEN}",
"Idempotency-Key" => "limit-ladder:#{JSON.parse(rev)["task"]}:#{Digest::SHA256.hexdigest(rev)[0, 16]}:a1")
req.body = rev
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |h|
h.request(req) { |res| res.read_body { |chunk| chunk.each_line { |l| puts l if l.start_with?("data:") } } }
end
$rev = file_get_contents("input.json");
$ch = curl_init(BASE . "/run-stream");
curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_POSTFIELDS => $rev, CURLOPT_HTTPHEADER => [
"Content-Type: application/json", "Authorization: Bearer $TOKEN",
"Idempotency-Key: limit-ladder:" . json_decode($rev, true)["task"] . ":" . substr(hash("sha256", $rev), 0, 16) . ":a1"],
CURLOPT_WRITEFUNCTION => function ($ch, $chunk) { foreach (explode("\n", $chunk) as $l) if (str_starts_with($l, "data:")) echo $l, "\n"; return strlen($chunk); }]);
curl_exec($ch);
var rev = File.ReadAllText("input.json");
var rd = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(rev));
var sreq = new HttpRequestMessage(HttpMethod.Post, Api.Base + "/run-stream") { Content = new StringContent(rev, System.Text.Encoding.UTF8, "application/json") };
sreq.Headers.Add("Authorization", "Bearer " + Api.Token);
sreq.Headers.Add("Idempotency-Key", $"limit-ladder:{input["task"]}:{Convert.ToHexString(rd, 0, 8).ToLowerInvariant()}:a1");
using var http = new HttpClient();
using var res = await http.SendAsync(sreq, HttpCompletionOption.ResponseHeadersRead);
using var rdr = new StreamReader(await res.Content.ReadAsStreamAsync());
for (string? l; (l = await rdr.ReadLineAsync()) != null;) if (l.StartsWith("data:")) Console.WriteLine(l);