# AgentBill > AgentBill is an SDK and HTTP API that shows what each AI agent, client and job costs, in dollars at public list price, and puts a spend ceiling on one agent task. Before expensive work your code calls preflight, which atomically reserves an estimate against a ceiling identified by a task_ref you choose. A job counts dollars (each call priced from the tokens your provider reported; a call that cannot be priced is charged its reservation, never $0), tokens, which the SDK's wrap() around your OpenAI, Anthropic or Gemini client reports by default, or integer units you pass. When the reservation would cross that ceiling it refuses, answering approved: false (the plain SDK client raises a typed error; a wrapped client returns a typed Refusal), after which your own code decides what happens next. Every process, machine, provider and agent that passes the same task_ref draws on the same ceiling. After the call, record settles what it used: your number or, through wrap(), the token counts your provider reported on the response, which the server prices as an estimate at public list price (list price, your invoice may differ). A monthly report per customer_id, by agent and model, carries the same figures. AgentBill never reads your provider bill and never converts units you define into money. Python and Node SDKs, and a remote MCP server at https://agentbill.dev/mcp for Claude, ChatGPT, Cursor, Codex and other MCP clients. Free tier: 1,000 preflight calls a month. The unit of enforcement is the task: not the calendar month, not the project, not the API key, not the agent. One job, a batch or a run or a customer request, gets one task_ref and one task_ceiling, and the ceiling travels with that string, so a job spread across three workers and two providers still consults one number. Integration is two calls. preflight before the work, which reserves and returns a decision, and record after it, which settles what the run used or releases the reservation if it failed. Three calls carry the whole product: preflight() decides, record() settles, get_task() reads the live burn-down. For model calls, wrap() around the client makes the first two for you, from the usage your provider reports. **AgentBill refuses. Your code decides what happens next**: return a partial result, retry with a smaller estimate, drop to a cheaper model, escalate to a human, or stop. There is no kill switch on our side, because there is no proxy on our side. A job counts one of two things, fixed when it opens. In a unit job (the default) a unit is an integer you define and pass: with 1 unit = 1 cent, a job worth $5 to you is task_ceiling=500, and AgentBill compares those units to the ceiling and never converts them into money. In a token job, which is what wrap() opens and records into, the numbers are the tokens your provider reported, the ceiling is in tokens, and GET /tasks/:task_ref shows beside them an estimate at public list price (list price, your invoice may differ). ## Install ``` pip install agentbill-sdk npm install agentbill ``` Get a key at https://agentbill.dev/register: sign up with Google, GitHub or an email link, and the console's start screen makes the key and shows it once. Free, no card. ## Quick start, Python ```python from agentbill import AgentBillClient, TaskCeilingExceededError client = AgentBillClient(api_key="agb_your_key") # job-142 already has its ceiling, set in the console at agentbill.dev/app or # with PUT /tasks/job-142/ceiling. The call names the job and what this one # call is worth, and nothing about the budget. client.preflight(agent_id="researcher", task_ref="job-142", estimated_units=12) # ... your provider call goes here ... # Settle, or the units stay held until the reservation expires. client.record(agent_id="researcher", task_ref="job-142", units=12) ``` Every later call in the run passes task_ref and nothing else about the budget. It does not need to know the ceiling or what the calls before it spent: ```python try: # A different agent, the same job, the same ceiling. client.preflight(agent_id="writer", task_ref="job-142", estimated_units=250) except TaskCeilingExceededError as e: # e.task_ref, e.task_ceiling, e.task_used_units, e.task_remaining_units handle_it(e) ``` Or let the decorator do both. It records on return and, on an exception, records success=False, which releases the reservation without spending it: ```python # task_ceiling here opens the job from code, the alternate to the console; # once the job exists it is not applied. @client.gate(agent_id="researcher", task_ref="job-142", task_ceiling=500, estimated_units=12) def run_agent(topic: str) -> str: return call_your_llm(topic) ``` ## Quick start, Node ```typescript import { preflight, record, TaskCeilingExceededError } from 'agentbill' // Reads AGENTBILL_API_KEY from the environment. There is no client object. // job-142 already has its ceiling, set in the console or with PUT /tasks/job-142/ceiling. await preflight({ agentId: 'researcher', taskRef: 'job-142', estimatedUnits: 12 }) // ... your provider call goes here ... await record({ agentId: 'researcher', taskRef: 'job-142', units: 12 }) ``` Python names everything in snake_case and takes api_key as an argument; Node names everything in camelCase and reads AGENTBILL_API_KEY only. There is no gate decorator in the Node SDK: the explicit pair above is the Node integration path. ## Automatic metering with wrap() Wrap your model client once and the SDK makes both calls around every call it measures: preflight on the job in tokens before it, record with the usage your provider reported after it. ```python from openai import OpenAI from agentbill import wrap, Refusal # Reads AGENTBILL_API_KEY. task_ceiling opens the job, counted in tokens. llm = wrap(OpenAI(), task_ref="tokens-1", agent_id="researcher", task_ceiling=50_000) reply = llm.chat.completions.create(model="gpt-4o-mini", max_tokens=300, messages=[{"role": "user", "content": "Hello"}]) if isinstance(reply, Refusal): print(reply) # the call that would have passed the ceiling was not sent; nothing raised else: print(reply.choices[0].message.content) ``` ```typescript import OpenAI from 'openai' import { wrap, isRefusal } from 'agentbill' const llm = wrap(new OpenAI(), { taskRef: 'tokens-1', agentId: 'researcher', taskCeiling: 50_000 }) const reply = await llm.chat.completions.create({ model: 'gpt-4o-mini', max_tokens: 300, messages: [{ role: 'user', content: 'Hello' }] }) if (isRefusal(reply)) console.log(String(reply)) // the call was not sent; nothing thrown ``` A refusal is a value, not an exception: the measured call returns a Refusal (approved False, reason, task_ref, asked, used, ceiling, remaining, upgrade_url on a quota refusal, and answer, the preflight answer whole). It has no choices, content, candidates or usage, and bool() of it is False. A refused streaming call returns the same Refusal, and iterating it yields nothing. The one stream that can be refused after it started, a Gemini automatic-function-calling stream whose later round is refused, ends after the earlier round's chunks and sets its .refusal. Exceptions out of a wrapped call are failures: the provider's own error, or from AgentBill a network error, a 401 or a 5xx. - Measured: OpenAI chat.completions.create and responses.create, Anthropic messages.create, and google-genai generate_content and generate_content_stream (Python also aio.models), sync, async and streamed. Nothing else a wrapped client does is measured, and neither is an unwrapped client. - The estimate preflight reserves needs no number from you: the job's running average per call in your process (default_estimate, 2,000, before the first), never more than the average prompt plus the call's own max_tokens. The prompt is not counted before the call, and no request is added. - After the call: idempotency_key is the provider's response id, the reservation is settled whole, and metadata carries provider, model, the tokens by type, duration_ms and step. Missing usage is recorded as missing, never as 0. - Each measured call is one preflight, so it uses one preflight of the account's monthly quota. - A refusal is returned as a Refusal before the provider call is sent, never raised. Your code decides. preflight() on the plain client still raises TaskCeilingExceededError. - GET /tasks/:task_ref then breaks the job down by model and by step, with tokens and an estimate at public list price. List price, your invoice may differ. ## Remote MCP server https://agentbill.dev/mcp, Streamable HTTP, stateless (every message is its own POST; GET and DELETE answer 405). Two ways in: - OAuth 2.1, for Claude's custom connector, ChatGPT and any client that follows the MCP authorization spec. With no credentials the endpoint answers 401 with WWW-Authenticate: Bearer resource_metadata="https://agentbill.dev/.well-known/oauth-protected-resource/mcp". Authorization server metadata is at https://agentbill.dev/.well-known/oauth-authorization-server: dynamic client registration or a client ID metadata document, PKCE S256 only, tokens bound to the resource https://agentbill.dev/mcp, rotating refresh tokens. Scopes: agentbill:read (the read tools) and agentbill:meter (preflight and record_event). - Authorization: Bearer with an AgentBill API key, for Codex, Cursor, Antigravity, VS Code and Claude Code with --header. The key's limits apply. Tools: preflight (same arguments and refusal sentences as the local server; approved: false is a result, not an error), record_event (adds task_ref, reservation_id, idempotency_key and success, so it settles a job's reservation), task_status, top_jobs (units, or an estimate at public list price), recent_refusals. No tool creates or shows an API key, changes a plan or touches billing. Setup per client: https://agentbill.dev/integrations/mcp ## Local MCP server (stdio) ``` uvx agentbill-mcp ``` ```json { "mcpServers": { "agentbill": { "command": "uvx", "args": ["agentbill-mcp"], "env": { "AGENTBILL_API_KEY": "agb_..." } } } } ``` Two tools. preflight takes agent_id, customer_id, estimated_units, ceiling, task_ref, task_ceiling and idempotency_key. record_event takes agent_id, units, customer_id and metadata, and has no task_ref parameter, so a setup on the local server alone can open a task budget with preflight but cannot settle its reservation; the sweeper reclaims it when it expires. Settle from the Python or Node SDK, or use the remote server, whose record_event takes task_ref and reservation_id. ## How a task budget behaves - A job is opened with its ceiling either by the first preflight carrying a task_ref with a task_ceiling, or by PUT /tasks/:task_ref/ceiling (the console's task budgets view makes the same write). A preflight for a job that does not exist, sent without a ceiling, is 422 task_ceiling_required. - Once the job exists, a task_ceiling from code is not applied, so a retry cannot raise the ceiling it was meant to respect. The last save through the endpoint or the console is the ceiling in force. An approved answer and a task_ceiling_exceeded refusal carry the ceiling that decided them as task_ceiling; the other refusals are decided before the job's row is consulted. - The row is unique on (account_id, task_ref), which is why processes, machines, providers and agent_ids converge on one ceiling by sending one string. - The check and the reservation are one conditional UPDATE, so two calls arriving together cannot both be approved against the last of the budget. - An approved preflight holds a reservation with an expiry, 60 minutes by default and returned as reservation_expires_at. record settles it, record(success=False) releases it, and a sweeper running every five minutes reclaims it if neither arrives, so a crashed run cannot hold a budget hostage. Note the direction: an abandoned reservation makes the ceiling tighter, never looser. - idempotency_key makes a retried preflight safe: same key, same decision, one reservation. - Recording reports reality, including spend past the ceiling. POST /events moves used_units and surfaces the overage as task_exceeded: true. A task ceiling is only ever enforced by preflight. ## The refusal contract One rule: when YOUR spend rule refuses, the SDK raises. When AGENTBILL'S OWN monthly quota refuses, the SDK returns a result carrying approved: false and an upgrade_url, so our billing state cannot crash your agent. Every one of these is HTTP 200 with approved: false; none of them is an error status. | reason | Python | Node | |---|---|---| | task_ceiling_exceeded | raises TaskCeilingExceededError | throws TaskCeilingExceededError | | budget_exhausted (that customer's balance) | raises BudgetExhaustedError | throws BudgetExhaustedError | | ceiling_exceeded (this one call's estimate) | raises CeilingExceededError | throws CeilingExceededError | | free_tier_exceeded (plan is free) | returned, with .upgrade_url | returned, with .upgradeUrl | | plan_limit_exceeded (any paid plan) | returned, with .upgrade_url | returned, with .upgradeUrl | Under wrap() nothing in that table is raised: a measured call returns a Refusal for every reason, the quota included (once it is spent no ceiling can be checked, and on_quota="send", Node onQuota: 'send', sends the call unchecked instead). Branch on it: Python isinstance(r, Refusal), Node isRefusal(r). Exceptions out of a wrapped call are failures only (network, 401, 5xx, the provider's own error). preflight() and record() on the plain client are not changed by this: preflight() still raises TaskCeilingExceededError on a ceiling refusal and returns on the quota. Two shapes that are errors rather than refusals: a task_ref preflight has never seen, arriving without a task_ceiling, is 422 task_ceiling_required (Python raises TaskCeilingRequiredError, Node throws AgentBillError); a retried preflight whose idempotency_key is still being decided is 409 preflight_in_progress (Python raises PreflightInProgressError, Node throws AgentBillError). TaskCeilingExceededError carries task_ref, task_ceiling, task_used_units and task_remaining_units, so the handler can choose between degrading, queueing and stopping without a second call. FreeTierExceededError and PlanLimitExceededError are still importable from the Python package for compatibility, and nothing has raised them since 0.6.0. One exception to the rule above, and it is in our code rather than in the docs. @client.gate calls preflight() internally, so the three typed spend errors still propagate out of the decorated function unchanged. What gate changes is the other half: the two quota reasons preflight() would have RETURNED become a bare Exception instead. Use the explicit preflight/record pair where a quota refusal has to stay non-fatal. preflight is not the only endpoint that can refuse, though it is the only one that enforces a task ceiling. POST /events answers 402 budget_exhausted when the customer's own limit_units would be crossed, and writes no event row in that case. ## What AgentBill is not - **Not a proxy or a gateway.** No base URL to change, no traffic routed through us, no provider credentials held by us. Your model calls stay direct. AgentBill sees one HTTPS call, made by your code, to our API. - **Not a reader of your provider bill.** No invoice ingestion and no access to your provider account. wrap() reads the usage on the response your own process already received and sends the token counts, the model and the step, never the prompt or the answer. The one dollar field in the API, list_price_usd_estimate on GET /tasks/:task_ref, is an estimate at public list price from a dated price table named in price_versions: list price, your invoice may differ, and a call it cannot price shows no list price, never $0. The ceiling is exactly as tight as the estimate preflight reserves: yours, or the job's running average under wrap(). - **Not a per-agent budget.** agent_id is an attribution label. It is stored on tasks, steps and refusals and can be filtered on, and nothing is capped by it. Two agents sharing a task_ref share one ceiling. - **Metering is automatic only for wrapped calls.** wrap() measures the calls a wrapped OpenAI, Anthropic or Gemini client makes through chat.completions.create, responses.create, messages.create, generate_content and generate_content_stream. Nothing else is counted unless your code calls preflight or record: an unwrapped client, another method, a tool call, a GPU run or a vector search counts against a task ceiling if you record it with the same task_ref, and is invisible if you do not. (record_step is a third call your code can make, and it feeds the anomaly baseline only: it takes no task_ref and moves no budget.) A task ceiling is enforced only by preflight calls carrying that task_ref, which wrap() makes before each call it measures; POST /events with that task_ref moves the task's used_units but never refuses on it, and cannot create a task. - **Not billing for your end customers.** AgentBill does not charge your users, hold their cards or issue them credit. Stripe Connect is not shipped. The per-customer limit_units balance is an internal ceiling you set for your own accounting, not an invoice. (AgentBill's own subscription runs on Polar: that is us charging you, not you charging anyone.) - **Not observability.** No traces, no spans, no prompt capture. GET /tasks/:task_ref breaks a job down by model and by step (calls, tokens, a list-price estimate), and GET /decisions stores spend decisions, not conversations. - **Not a process supervisor.** Nothing here can terminate a run. preflight answers and the SDK raises, or under wrap() returns a Refusal; the except, catch or if block is what ends the job. ## Pricing - Free: $0/month, 1,000 preflight calls per month. - Builder: $29/month, 50,000 preflight calls per month. - Team: $99/month, 500,000 preflight calls per month. - Scale: $299/month, 2,000,000 preflight calls per month. The quota counts POST /preflight calls, and the counter is incremented inside the same transaction that reserves, so a refused call rolls it back and burns no quota; a call refused on the per-call ceiling never reaches the database at all. The period rolls on the first preflight after the calendar month turns. Crossing the quota answers approved: false with free_tier_exceeded or plan_limit_exceeded and an upgrade_url, and does not raise, so running out of our quota degrades your spend control rather than crashing your agent. No feature is gated by plan; the tiers sell headroom. Billing runs on Polar. ## Docs - [Documentation index](https://agentbill.dev/docs): The SDK quick start, the core concepts, and the HTTP reference. - [Task budgets, a hard cost ceiling per agent job](https://agentbill.dev/docs/task-budgets): How one task_ref carries one ceiling across every call in a job. - [How to cap what one agent run can spend](https://agentbill.dev/docs/limit-cost-per-agent-run): The preflight-and-record pair applied to a single run. - [Every setup failure, and its fix](https://agentbill.dev/docs/first-run): The errors between installing the SDK and its refusal of a call, each with its fix. - [Questions](https://agentbill.dev/faq): What the product does and does not do, answered against the source. ## Integrations - [Integrations](https://agentbill.dev/integrations): What AgentBill publishes and where to install it; framework rows are guides that use the plain SDK, not packages. - [OpenClaw plugin](https://agentbill.dev/integrations/openclaw): One ceiling per OpenClaw session: the plugin asks before every tool call, and before every model turn on the embedded and CLI runners. OpenClaw does not run or send what the ceiling refused; on the Codex and Copilot harnesses only tool calls are asked. - [LangChain](https://agentbill.dev/integrations/langchain): A wrap_model_call middleware that asks before each model call, with the task_ref taken from the job. - [OpenAI Agents SDK](https://agentbill.dev/integrations/openai-agents-sdk): A RunHooks class that asks in on_llm_start, before each model request Runner.run makes. - [CrewAI](https://agentbill.dev/integrations/crewai): PRE_MODEL_CALL and POST_MODEL_CALL hooks; the refusal is raised as HookAborted because CrewAI runs hooks fail-open. - [Claude Code](https://agentbill.dev/integrations/claude-code): Its built-in OpenTelemetry export pointed at https://agentbill.dev/otel with two files in a client's project; every API request is recorded under that client with its model, tokens and price at list. Records after the fact; no ceiling refuses a Claude Code request. - [Connect over MCP](https://agentbill.dev/integrations/mcp): The remote MCP server at https://agentbill.dev/mcp: Streamable HTTP, OAuth or an API key as a Bearer header, with setup for Claude, ChatGPT, Claude Code, Codex, Cursor, Antigravity and VS Code. ## Packages - [agentbill-sdk on PyPI](https://pypi.org/project/agentbill-sdk/): Python SDK 0.8.0. AgentBillClient with preflight, record, gate, get_task, checkpoint, record_step, and wrap() for model clients. - [agentbill on npm](https://www.npmjs.com/package/agentbill): Node SDK 0.6.0, ESM. Exports preflight, record, getTask, meter and wrap. - [Remote MCP server](https://agentbill.dev/mcp): Streamable HTTP, stateless. Sign in with OAuth 2.1 (dynamic client registration or a client ID metadata document, PKCE S256), or send Authorization: Bearer with an API key. Tools: preflight, record_event, task_status, top_jobs, recent_refusals. - [agentbill-mcp on PyPI](https://pypi.org/project/agentbill-mcp/): the local stdio MCP server 0.3.1, exposing preflight and record_event to an agent host with the key in its environment. - [@agentbill/openclaw on ClawHub](https://clawhub.ai/agentbill/plugins/openclaw): OpenClaw plugin 0.3.0, one ceiling per session. Install with openclaw plugins install clawhub:@agentbill/openclaw. - [Source repository](https://github.com/marketinglior-pixel/agentbill): The API, this site, and both SDKs. MIT. ## Optional - [Pricing](https://agentbill.dev/pricing): The four plans and the monthly preflight-call limit each includes. - [Get an API key](https://agentbill.dev/register): Sign up with Google, GitHub or an email link; the console makes one agb_ key and shows it once. Free plan, no card. - [Why monthly caps don't protect you from one bad LLM run](https://agentbill.dev/blog/monthly-caps-wont-save-you): Why a calendar month is the wrong unit for a single run. - [How preflight avoids double-billing under concurrent load](https://agentbill.dev/blog/how-preflight-avoids-double-billing): Reservations, idempotency keys and concurrent settlement. - [About](https://agentbill.dev/about): Who builds this, and what it deliberately is not. - [Status](https://agentbill.dev/status): Service status. - [כמה כל לקוח עולה לך](https://agentbill.dev/he/cost-per-client): Hebrew, for n8n and Make operators: working out what one client costs to run. - [Terms](https://agentbill.dev/terms): Terms of service. - [Privacy](https://agentbill.dev/privacy): Privacy policy. - [Security](https://agentbill.dev/security): How to report a vulnerability, what the SDKs send, and how to revoke a leaked key. - [Console](https://agentbill.dev/app): Your tasks, refusals and keys. Sample data at /app?demo=1, no account needed. - [llms-full.txt](https://agentbill.dev/llms-full.txt): The same product with the whole HTTP contract, the reservation lifecycle and a worked cross-process example.