StiggLangChainCallbackHandler governs a LangChain or LangGraph run. Add it to the run’s callbacks, and LangChain passes it to every agent in the run, including agents started by other agents. Each model call and paid tool is then checked before it runs, and its usage is reported as the agent that made it. For how Stigg governs a team of agents and what it’s used for, see Agent frameworks.
Key terms
Quick start
You need a LangChain or LangGraph app, a Stigg server API key, and a credit type in Stigg, herecredits, that your customers have.
1
Install the SDK
@langchain/core 1.x is an optional peer dependency. The handler uses the copy your app already has.2
Create the Stigg client
Create it once, when your server starts:
3
Create the handler
Create the handler once too, and tell it what to check each step against:
4
Pass the handler and the customer on every run
Your agent needs no changes. Add the handler to The customer changes per request, so it goes on the run, not on the handler. Take it from the logged-in user on your server. See Naming the customer safely. A process that serves one customer can set a default in the handler’s
callbacks, and the customer the run is for to metadata:stiggContext: { customerId } instead. A run with neither is denied before its first step.5
Check that it works
Run the agent once. Then confirm in Stigg that the customer’s
credits balance went down. If it didn’t, the token counts the handler reports aren’t converted into credits yet. See The credit balance does not go down.To see each decision while you test, add a hook to the handler:
A step that fails reports no usage.
Naming the customer safely
The customer ID decides who pays for a run. If it comes from the user’s request, a user can send another customer’s ID and charge their usage to that customer. Always set it on your server, from the logged-in user.- In your own server, set
metadata.stiggContext.customerIdwhere you callinvoke, from your session or auth layer. - On LangGraph Platform, the client sends the run’s
metadata, so the client could name any customer. SetstiggContextin your custom auth handler from the authenticated identity, and overwrite any value the client sent. The client can also name the run, so also setrootAgenton the handler. - With
defaultPlanId, every new customer ID a run names becomes a customer on that plan. Make the default plan a free one, so a made-up ID never starts a paid subscription.
invoke, the run’s stiggContext can set only customerId and correlationId. Any other field throws, because a client can write that metadata. Set entitlements, mappers and agent IDs on the handler or under agents.
Configurations
Each configuration below adds to the one before it.Metering only
entitlement, nothing is checked or denied. Every model call is reported, and the agents run as they would without Stigg. By default each call reports LangChain’s own counters: input_tokens, output_tokens and total_tokens. Create meters for those names in Stigg, or convert them with a usageMapper.
One budget for the whole team
usageMapperconverts a model call’s token counts into what is reported, here credits. See Usage mappers.ingestMode: 'sync'reports each call before the next check, so fast agents can’t run far past the limit. See Ingest modes.
A budget per agent
To give each agent its own budget, register the agents in Stigg, then tell the handler which LangChain agent is which Stigg agent. Step 1. Register the agents. Each agent is a Stigg entity of typeagent. Create a parent agent whose budget caps the whole team, then one agent per team member with its own budget under that parent. Run this once per customer, for example when the customer signs up:
agents, each key is an agent’s name in LangChain, and its agentId is the Stigg agent registered in step 1:
attributionKeyis the key youragententity type uses to identify an agent, hereagentId. It’s set once on the entity type in Stigg. See Entity types.rootAgentnames the agent that the top-level run always counts as. See the rules below.
- Agents are matched by name. A key in
agentsmust be thenamegiven tocreateAgentorcreateReactAgent, or the node’s name in a hand-built LangGraph graph. - A step belongs to the nearest named agent above it. If the lead starts the researcher, the researcher’s steps count against the researcher, even when the lead passes its own config along. The
lc_agent_namein a run’s metadata never decides which agent a step counts against, because a caller can set it. - Name every agent. An agent with no name, or with a name
agentsdoesn’t list, counts as the nearest listed agent above it, usually the one that started it. - Set
rootAgent. Whoever callsinvokecan name the top-level run, so withoutrootAgentthe caller could pick whose budget pays. WithrootAgent: 'lead', the top-level run always counts as the lead. - An agent’s settings are merged over the shared ones, field by field. An agent that sets only
agentIdstill uses the sharedentitlementandusageMapper. Each step is checked once and reported once. - An agent missing from
agentsuses the shared settings alone, so its spend counts against the customer, not an agent, unless the shared settings name an agent. The SDK logs a warning once. idempotencyKeyis not allowed in any of these settings, because every step would share it and only the first would be reported. The SDK keys each step by its own ID.
A hand-built LangGraph graph
In a hand-builtStateGraph, each node is an agent. Use the same handler, with agents keyed by node name:
ToolNode, counts against the agent whose model asked for it, not to the tools node.
Tools
Model calls are always checked and reported. A tool is only checked and reported whentools lists it, because most tools, such as a calculator or a lookup in your own database, cost nothing.
usageprices a tool call. It receives{ name, input, output }, with the tool’s arguments and result, and returns amounts by feature or credit ID. A model call’susageMapperdoes the same job from token counts. Ifusagethrows, the error goes toonErrorand the run continues.- Avoid counting usage twice. A common pattern wraps an agent in a tool, such as
tool(() => researcher.invoke(...)), so the lead can call it. The researcher’s model calls are already reported as the researcher, so reporting the tool too would count the same work twice. If'*'would cover that tool, set it tofalse. - Per agent.
agents.<name>.toolsis merged over the sharedtools, name by name. An agent can add its own paid tool, or set a shared one tofalse.'*': falseonly opts the agent out of the catch-all. - Attribution. A tool counts against the agent whose model asked for it.
- Packaged tools. Add
'*'so packaged tools such asTavilySearchare always covered, including after the graph resumes. The handler names these tools from the model’s request, so their own entries apply too. - Each tool call is reported once, keyed by the model’s tool-call ID. A retried or resumed call is checked again but reported under the same key.
- Entries are validated when the handler is built. An entry with neither an entitlement nor a
usageis rejected, and so is{ currencyId, usage }. Write that as{ entitlement: { currencyId }, usage }.
When access is denied
What your code can do depends on which step was denied:
Stigg reports a denial by throwing an
AccessDeniedError from inside the step, and your code decides what to do with it. LangChain often wraps that error in its own, so use stigg.denial(err) to find it. It returns the denial, with a message you can show, or undefined for any other error.
In this example the researcher runs as a tool of the lead. When the researcher is out of budget, a cheaper generalist agent, with no paid search, answers instead:
state.refused ? 'generalist' : 'writer'.
If Stigg can’t be reached to check, the SDK follows checkErrorAction, which lets the step run by default. See Fallback strategy. Reporting usage never throws into the run.
Observability
- Hooks.
hooks: { onDecision, onIngest }are called for every check and every report.context.agentandcontext.toolsay which agent and tool the step belonged to. - Tracing. Stigg emits OpenTelemetry spans, so each check and each report appears in any tracing tool that reads OpenTelemetry, next to the agents’ own steps. A check shows up as a
stigg.checkspan and a report as astigg.ingestspan. A span is one timed entry in a trace. A denied step’s span is marked as an error. This needs@opentelemetry/api, an optional peer dependency. - Flushing. Call
await stigg.flush()before the process exits, so no report is lost.
STIGG_TRACER_NAME, to the filter. For example, in Langfuse:
Best practices
- Use chat models for exact metering. Chat models report usage for every call. For LangChain’s text-completion models, avoid streaming, and expect a batch of prompts to report its whole usage on the first prompt. A call with no usage goes to
onErrorinstead of being reported as zero. - Set the customer on your server. The customer ID decides who pays for a run. Take it from the logged-in user, never from the request, or a user can charge their usage to another customer. See Naming the customer safely.
- Keep parallel spend within budget. Each check reads the balance at that moment, so steps that start together can each pass. Keep limits a little below the true cap, or reserve credits for the whole task. See Caching and accuracy.
- Choose small credit units. Agent budgets count whole units and round each call up, so define your credit type in small units, such as thousandths of a dollar, to keep rounding negligible. See Usage governance.
- Roll out without denials. Start with metering only, or register agents with no limit, then add limits once usage looks right. See Observe-only rollout.