GNL
Docs menu
Enterprise@gnldev/server

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 exceeded

For 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:

402 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):

Studio REST
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#

typeRestApiOptions.budgets

createRestApi's budget fallback option: { default?, perOrg? } — the journal's __budget__ overrides this.

fncheckBudget

Reads the effective limit (journal > fallback) and compares it against usage; returns { exceeded, usage, limit } (async).

fnassertBudget

Calls checkBudget and throws BudgetExceededError on overage — called on non-host paths before a run starts.

fngetOrgUsage

Returns an organization's (or the shared root scope's) total runs/tokens/costUsd; uses the incremental __usage__ counter (O(1)).

fnreadBudget

Reads the budget document from the journal: __budget__:<orgId> first, falling back to __budget__:default.

fnisBudgetExceeded

A pure function: compares OrgUsage against BudgetLimit and returns true/false.

typeBudgetLimit

A limit definition with usdLimit? / tokenLimit? fields.

classBudgetExceededError

Thrown by assertBudget on overage; carries usage+limit in its check (BudgetCheck) field.

fnrecordRunUsage

Adds a run's cost to the __usage__ counter WHEN IT COMPLETES (idempotent — the same runId is never counted twice).

fncreateBoundedUsageCache

Produces a bounded (LRU) UsageCostCache — prevents the cache from growing unbounded in multi-organization setups.

Enterprise
This feature is in the ee tier — using it in production requires a license. If there's no limit (neither in the journal nor as a fallback), usage isn't computed at all: a setup without a budget runs with no extra cost.
Note
The budget gate only blocks new work requests: a resume request with the same 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.
Related
See the 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.