Docs menu
Budget & quota (402)
Enforces a per-organization token/cost limit: on the write path, if the limit is exceeded, a new run/stream/workflow request gets 402 (resume is unaffected); limits are managed from Studio.
What it's for / when to use it#
If you're serving the same agent to multiple organizations, one organization's uncontrolled token/cost consumption shouldn't affect the others or blow up your bill. Budget/quota enforces this on the write path (new run/stream/workflow requests): if the organization's usage (incrementally counted from the journal via recordRunUsage) exceeds a defined limit (tokens or USD), the request is rejected with 402. Resuming pending work (e.g. a pending tool approval or a suspended workflow) is unaffected by this gate — otherwise no pending work in an over-limit organization could ever finish. Limits live in the journal (__budget__:*) — they can be changed live from Studio, with no need to redeploy the host; host config (RestApiOptions.budgets) is only a fallback.
Setup / import#
No separate subpackage is needed; the gate is built into @gnldev/server's createRestApi, and the primary primitives come from @gnldev/durable:
import { createGnl } from '@gnldev/durable';
import { createRestApi } from '@gnldev/server';
// To enforce it directly (for non-HTTP paths):
import { checkBudget, assertBudget, getOrgUsage, readBudget, isBudgetExceeded, BudgetExceededError } from '@gnldev/durable';Step-by-step usage#
The most common usage: pass createRestApi a budgets.default fallback limit along with org — this limit applies if there isn't already a specific __budget__ in the journal:
// org: every organization gets an isolated journal (exactly-once is inherited); budgets: the fallback limit —
// the __budget__ document written from Studio OVERRIDES it and returns 402 to the organization that exceeds it.
const DEFAULT_TOKEN_LIMIT = Number(process.env.GNL_DEFAULT_TOKEN_LIMIT ?? 100);
app.route('/api', createRestApi(config, {
title: 'SWAPI Pro',
auth,
org: {},
budgets: { default: { tokenLimit: DEFAULT_TOKEN_LIMIT } },
}));
// call again with a different runId -> 402 once 100 tokens are exceededFor different per-organization limits, use perOrg (the effective limit is always resolved in the order journal > perOrg[id] > default):
budgets: {
default: { tokenLimit: 100_000 },
perOrg: { 'acme-corp': { usdLimit: 25 } },
}On overage, the client gets a 402 with a JSON body:
{ "error": "budget/quota exceeded — the new run was refused", "usage": { "runs": 3, "tokens": 142, "costUsd": 0 }, "limit": { "tokenLimit": 100 } }There's a ready-made /usage endpoint to query remaining quota/usage (it uses the same scope resolution: the organization header or the bound identity):
GET /usage
Headers: x-gnl-org: acme-corp
→ { "org": "acme-corp", "usage": { "runs": 3, "tokens": 142, "costUsd": 0 }, "limit": { "tokenLimit": 100 }, "exceeded": true }@gnldev/server does NOT automatically enforce quota on non-HTTP paths (@gnldev/queue worker, @gnldev/scheduler cron, @gnldev/a2a, or embedding runDurable/createGnl directly) — you need to call assertBudget yourself at your own pre-run checkpoint:
import { assertBudget, BudgetExceededError } from '@gnldev/durable';
try {
await assertBudget(reader, { orgId, fallback: { tokenLimit: 100_000 } });
// ... start the run here
} catch (e) {
if (e instanceof BudgetExceededError) {
// build your own 402 / refusal response from e.check.usage and e.check.limit
}
throw e;
}API reference#
RestApiOptions.budgetscreateRestApi's budget fallback option: { default?, perOrg? } — the journal's __budget__ overrides this.
checkBudgetReads the effective limit (journal > fallback) and compares it against usage; returns { exceeded, usage, limit } (async).
assertBudgetCalls checkBudget and throws BudgetExceededError on overage — called on non-host paths before a run starts.
getOrgUsageReturns an organization's (or the shared root scope's) total runs/tokens/costUsd; uses the incremental __usage__ counter (O(1)).
readBudgetReads the budget document from the journal: __budget__:<orgId> first, falling back to __budget__:default.
isBudgetExceededA pure function: compares OrgUsage against BudgetLimit and returns true/false.
BudgetLimitA limit definition with usdLimit? / tokenLimit? fields.
BudgetExceededErrorThrown by assertBudget on overage; carries usage+limit in its check (BudgetCheck) field.
recordRunUsageAdds a run's cost to the __usage__ counter WHEN IT COMPLETES (idempotent — the same runId is never counted twice).
createBoundedUsageCacheProduces a bounded (LRU) UsageCostCache — prevents the cache from growing unbounded in multi-organization setups.
runId (a pending tool approval, or continuing a suspended workflow) already has a trace in the journal, so it passes through the gate automatically — pending work in an over-limit organization is never locked out. If the journal doesn't support listRuns, quota can't be enforced (fail-open) and the host logs a warning to the console.cost-observability page to learn how usage is computed (tokens/USD, broken down per model), the multi-organization page to understand the basis of organization isolation, and the studio-inspector page to edit limits from Studio.