> ## Documentation Index
> Fetch the complete documentation index at: https://docs.stigg.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Gating calls

> Check a customer's credits or usage limit before an LLM call leaves your process, and turn denied access into an upgrade or a credit purchase.

Metering tells you what a customer spent. Gating stops them from spending what they do not have. With gating on, every call through a wrapped client is checked against Stigg **before** the request reaches the provider, so an out-of-credits customer costs you nothing.

## Turning gating on

Set `entitlement` in the `stiggContext` to what the call is checked against: a credit currency or a metered feature, not both.

```typescript theme={null}
const anthropic = stigg.wrap(new Anthropic());

await anthropic.messages.create({
  model,
  max_tokens: 1024,
  messages,
  stiggContext: {
    customerId: 'customer-acme',
    entitlement: { currencyId: 'credits' },   // or { featureId: 'feature-ai-tokens' }
  },
});
```

A credit balance can be checked and drawn without a subscription, so the quickest way to start is a credit currency, a customer with a credit grant, and a `customerId` on the call. You can model plans and pricing once metering works.

Without `entitlement`, the wrapper only meters: every call goes through and its usage is reported to Stigg, but nothing is checked and access is never denied. If you wrap a client with a customer but no `entitlement` as its default, the SDK logs this once, so it is never a surprise.

<Note>
  The check and the reporting are set up separately, so make sure they point at the same balance. `entitlement` sets which balance the check looks at, for example your `credits` currency. The SDK reports token metrics by default, and those only reduce the balance if their features are mapped to the currency in its **Credit consumption** settings in Stigg. If they are not, the balance never goes down and every check passes. Alternatively, convert usage into credits in your code with [weighted credits](/documentation/ai-agents/llm-wrappers/metering#weighted-credits), which deducts from the balance directly. If you check a metered feature instead, the metric you report must feed that feature's meter. See [The credit balance does not go down](/documentation/ai-agents/llm-wrappers/metering#the-credit-balance-does-not-go-down).
</Note>

## Handling denied access

When access is denied, the call is not sent to the provider. A wrapped call has to return the vendor's own response type, so denied access is raised as an `AccessDeniedError`. `stigg.denial(err)` turns it back into a decision you can act on, and returns `undefined` for any other error:

```typescript theme={null}
try {
  const reply = await anthropic.messages.create({
    model,
    max_tokens: 1024,
    messages,
    stiggContext: { customerId: 'customer-acme', entitlement: { currencyId: 'credits' } },
  });
  return reply;
} catch (err) {
  const decision = stigg.denial(err);
  if (!decision) throw err; // a real failure, not denied access

  console.log(decision.summary);
  // hasAccess=false remaining=0 deniedBy=customer-acme reason=InsufficientCredits remedy=top-up source=rest

  return showUpsell(decision.message, decision.remedy);
  // "Out of credits for customer-acme. Top up the balance to continue."
}
```

To check access without making a call, use a wrapped client's `stigg.check()`, for example `anthropic.stigg.check()`, or, with no client, `stigg.check(context)`. Both return the decision rather than throwing. For example, check that an agent has enough credits before it starts a multi-step task, so it does not stop halfway. In a user-facing feature, you can also use it to disable a button before the user clicks it.

```typescript theme={null}
// Ask for the credits the whole task is expected to use, not just one unit.
const decision = await stigg.check(
  { customerId: 'customer-acme', entitlement: { currencyId: 'credits' } },
  estimatedTaskCredits,
);
if (!decision.hasAccess) return pauseAgent(decision.message);
```

### The decision object

`stigg.check()` returns a decision object, and `stigg.denial(err)` returns the same object for a call where access was denied. Its type is [`AccessDecision`](/api-and-sdks/references/sdk/backend/rest/agent-sdk#accessdecision). It has these fields:

| Field | Description |
| - | - |
| `hasAccess` | Whether access is granted and the call may proceed. The same as `hasAccess` in an entitlement check. |
| `deniedBy` | Which entity denied access: an agent, a team, or the customer. See [Agent governance](/documentation/ai-agents/agent-governance). |
| `reason` | Why access was denied, for example `InsufficientCredits` or `BudgetExceeded`. The equivalent of `accessDeniedReason` in an entitlement check. |
| `remedy` | What the caller can do: `top-up` (buy more credits, see [top-ups](/documentation/modeling-your-pricing-in-stigg/credits/self-service-top-up)), `raise-budget`, `subscribe` or `retry`. |
| `remaining` | The tightest remaining budget across the customer and every level above the agent. |
| `usageLimit`, `currentUsage` | The limit and usage of the binding constraint. |
| `source` | Who answered: `rest`, `node-sdk`, `cache`, `policy` (the check could not complete and your [fallback strategy](#fallback-strategy) decided) or `unenforced` (no `entitlement` was set). |
| `message` | A sentence you can show the user, matched to the remedy. |
| `summary` | A one-line description for logs. |

### Remedies are product decisions

Different reasons for denied access call for different responses, and the SDK keeps them apart all the way to `message`:

| Remedy | Typical cause | What to offer |
| - | - | - |
| `top-up` | The customer's credit balance is empty | An offer to buy more credits, with a [top-up](/documentation/modeling-your-pricing-in-stigg/credits/self-service-top-up) or a larger credit pack |
| `raise-budget` | An admin-set budget for an agent or team is spent | Ask the admin to raise it, or wait for the reset |
| `subscribe` | No active subscription includes the feature | An upgrade |
| `retry` | The check could not complete, for example because of a network error, and your `checkErrorAction` is `deny` | Retry the call with increasing delays, for example after 1, 2 and then 4 seconds |

## Gating actions that are not model calls

Wrapped clients check and report model calls for you. Other actions can cost you money too, such as an agent's calls to a paid web-search API, retrievals, or whole workflow steps. Check and report those yourself with `stigg.check()` and `stigg.report()`, using the same context as your model calls, so they count against the same customer, agent and team budgets:

```typescript theme={null}
const ctx = {
  customerId: 'customer-acme',
  entitlement: { featureId: 'feature-web-searches' },
  agentId: 'drafter',          // optional: attribute it to an agent, see Agent governance
  attributionKey: 'agentId',
  ingestMode: 'sync' as const, // report straight to the feature, no meter needed
};

const decision = await stigg.check(ctx);
if (decision.hasAccess) {
  await runWebSearch();
  await stigg.report(ctx, { 'feature-web-searches': 1 });
}
```

To gate the steps of an agent run in a framework such as LangGraph, see [Agent orchestration](/documentation/ai-agents/agent-orchestration).

## Caching and accuracy

The Agent SDK does not keep a cache of its own. How a check is answered depends on whether it names an agent:

* **A check that names an agent, or other [dimensions](/documentation/ai-agents/agent-governance#fine-grained-attribution-with-dimensions), is never cached.** Only Governance can work out the agent's budgets and every level above it, so each of these checks goes to Stigg. The decision always reflects the latest usage, and two servers can never both approve the same budget.
* **A check for a customer only** is answered from the Node server SDK's [entitlement cache](/documentation/high-availability-and-scale/local-caching-and-fallback-strategy), so it does not add a network round trip.

That cache stays accurate on its own:

* Usage you report is written back into it, so the next check already includes it.
* It is updated in real time when an entitlement changes, for example a plan change, a cancelled subscription or a credit purchase.
* Usage resets are applied when the period ends.
* It can be shared by all your servers through [Redis](/documentation/high-availability-and-scale/persistent-caching), instead of each server keeping its own.

When you wrap a client with both a customer and an entitlement, the SDK loads that customer's entitlements into the cache in the background, so the first call does not wait for it. Denied access is never cached: as soon as access is granted again, the very next call goes through.

To configure the cache, for example to use Redis, pass the Node server SDK's cache settings as `cache`:

```typescript theme={null}
const stigg = new StiggAgentSDK(process.env.STIGG_SERVER_API_KEY, {
  cache: { redis: { host: 'redis', port: 6379, environmentPrefix: 'prod' } },
});
```

If your application already uses the Node server SDK, pass that client as `client` instead, so both share one cache. See the [SDK reference](/api-and-sdks/references/sdk/backend/rest/agent-sdk#stiggagentsdk).

### Requested usage

Like an [entitlement check](/guides/quick-start-guides/gating-access-to-features#1-check), each check is made against a **requested usage**: the amount the call is about to consume. By default the requested usage is 1, which asks "is there anything left?". The real usage is reported after the call, so the call that uses up a balance is allowed to finish.

So a balance or budget can end up above its limit by the cost of the last call. With agents, one call can be a large share of a budget: a long generation can cost thousands of credits. In `async` mode the overshoot can be larger, because usage is counted only after Stigg processes the events, and calls made in the meantime are checked against the earlier balance. This adds up when one turn makes several calls in a row, such as an orchestrator and its sub-agents. To keep usage close to a hard limit, use the `sync` [ingest mode](/documentation/ai-agents/llm-wrappers/metering#ingest-modes), so each call is counted before the next check, together with an estimate of each call's usage, described below.

To check against an estimate of the call's usage instead, add an `estimate` to the recipe. The SDK includes one for Anthropic, `estimateFromMaxTokens(tokensPerUnit)`, one for Gemini, `estimateFromMaxOutputTokens(tokensPerUnit)`, and one for [TypeSafe](/documentation/ai-agents/llm-wrappers/providers/typesafe#requested-usage), `estimateFromState(tokensPerUnit)`. For example, the Anthropic one sets the requested usage from the call's `max_tokens`, the longest response it could produce:

```typescript theme={null}
import { anthropicRecipe, estimateFromMaxTokens } from '@stigg/agent-sdk-js';

const recipe = {
  ...anthropicRecipe,
  methods: anthropicRecipe.methods.map((m) => ({ ...m, estimate: estimateFromMaxTokens(1_000) })),
};
const anthropic = stigg.wrap(new Anthropic(), { recipe });
```

The OpenRouter and Bedrock recipes take an `estimate` option instead, a function that receives the call's parameters and returns the requested usage, for example `openrouterRecipe({ estimate: (params) => params.max_tokens ?? 1 })`.

A requested usage that is too high denies calls the customer could have afforded, so start with the default and add an estimate only where going over the limit matters.

## Fallback strategy

If a check cannot complete, for example because of a network error, the SDK falls back to the behavior you choose with `checkErrorAction`, the same idea as the [fallback strategy](/documentation/high-availability-and-scale/local-caching-and-fallback-strategy#fallback-strategy) in Stigg's other SDKs:

* **`allow`** (default): calls go through without a check, so your users are never blocked. Usage from that time is not reported, so it is not counted against any balance.
* **`deny`**: access is denied with `remedy: 'retry'`. Use this when enforcing a hard budget matters more than letting every call through.

You can set it for the whole SDK, for one wrapped client, or for a single call. A more specific setting takes precedence. For example, with a client for the OpenRouter gateway:

<CodeGroup>
  ```typescript Whole SDK theme={null}
  const stigg = new StiggAgentSDK(process.env.STIGG_SERVER_API_KEY, { checkErrorAction: 'deny' });
  ```

  ```typescript One client theme={null}
  const router = stigg.wrap(
    new OpenAI({ baseURL: 'https://openrouter.ai/api/v1', apiKey: process.env.OPENROUTER_API_KEY }),
    { stiggContext: { checkErrorAction: 'deny' } },   // the default for this client
  );
  ```

  ```typescript One call theme={null}
  await router.chat.completions.create({
    model,
    messages,
    stiggContext: {
      customerId: 'customer-acme',
      entitlement: { currencyId: 'credits' },
      checkErrorAction: 'allow',   // for this call only
    },
  });
  ```
</CodeGroup>

## Observe before you enforce

Gating a production AI feature is a big change, so roll it out in stages. Set everything up once at the start, so turning on enforcement later is a configuration change in Stigg, not a code change:

1. **Grant the entitlement with a soft limit.** In Stigg, add the metered feature or credit currency to the customer's plan, and set it to a soft limit: for a metered feature, turn off [limit enforcement](/documentation/modeling-your-pricing-in-stigg/plans/assigning-features-plans#limit-enforcement); for credits, turn off the [hard limit](/documentation/modeling-your-pricing-in-stigg/credits/credits-core-concepts#credit-wallet-soft-limit). Usage is only metered for customers who are granted an entitlement, so always grant one, even before you enforce anything.
2. **Set the same `entitlement` in your `stiggContext`.** Every call is checked and its usage is metered, but because the limit is soft, access is never denied when usage passes it.
3. **Register agents with `budget: { entitlement, limit: null }`.** Per-agent usage is tracked and visible, but agent budgets never block. See [Agent governance](/documentation/ai-agents/agent-governance#observe-only-rollout).
4. **Turn on enforcement in Stigg** once the numbers look right: switch the entitlement to a hard limit, and set real per-agent limits. Your wrapped clients do not change.


## Related topics

- [LLM and gateway wrappers](/documentation/ai-agents/llm-wrappers/overview.md)
- [Anthropic](/documentation/ai-agents/llm-wrappers/providers/anthropic.md)
- [Agent governance](/documentation/ai-agents/agent-governance.md)
- [Gateways](/documentation/ai-agents/llm-wrappers/gateways.md)
- [Agent orchestration](/documentation/ai-agents/agent-orchestration.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.