Python Decorator Billing — Charge Per API Call in 10 Lines
You have a function that does real work. It calls a model, runs a query, or transforms data, and someone else wants to call it. The question is how you meter and charge for those calls without writing a billing service. This is a tutorial on Python decorator billing: attaching a price to any function with a single decorator, then reading the usage data the decorator records for you. The pattern is built into nano-empire-tollbooth, a live package on PyPI under an MIT license that runs on Python 3.9 and up.
The goal here is operator-level mechanics. We cover the decorator, sync and async functions, reading usage with get_usage(), the JSONL ledger the package writes, the difference between paper and live mode, and how per-agent caps work. The starting point for the full walkthrough is the quickstart, and you can validate flows in the simulator before any money is involved.
The decorator: one line to charge per API call
Most metered billing setups in Python ask you to stand up a separate system. You wire a payment provider, store entitlement state, write middleware that checks every request, and reconcile usage on a schedule. The function you actually wrote becomes a small part of a much larger billing project. The decorator approach collapses that. You declare the price where the function is defined, and the wrapper handles metering on every call.
Install the package from PyPI:
terminalpip install nano-empire-tollbooth
Here is a complete, runnable example. Import the decorator, set a price in USD, and call the function as you normally would. Every call is metered.
pythonfrom nano_empire_tollbooth import monetize
@monetize(price_usd=0.01)
def summarize(text: str) -> str:
"""Summarize a document. Charged per call."""
sentences = text.split(". ")
return ". ".join(sentences[:3]) + "."
print(summarize("First point. Second point. Third point. Fourth point."))
The @monetize decorator wraps summarize in a tollbooth. The function signature does not change, so callers and your own code keep working exactly as before. What changed is that each invocation now flows through a metering and settlement step before the result returns. There is no second service to deploy and no database to provision. The price lives next to the logic it prices.
Sync and async, plus per-agent caps
The same decorator handles coroutines. If your function is async, decorate it the same way and the wrapper awaits correctly. This matters because most agent tooling and modern API handlers are async.
pythonfrom nano_empire_tollbooth import monetize
@monetize(price_usd=0.05)
async def translate(text: str, lang: str) -> str:
"""Translate text. Charged per call."""
return await my_async_llm(text, lang)
The decorator also accepts a source_agent and target_agent, which lets you attribute calls to a specific caller. That attribution is what makes per-agent daily caps work. You configure a cap once, and the tollbooth rejects calls from an agent that has exceeded its daily spend rather than running the function. This is a guardrail against a single caller, human or automated, running up usage you did not intend to allow.
pythonfrom nano_empire_tollbooth import monetize, TollboothConfig, create_tollbooth
config = TollboothConfig(
toll_per_message_usd=0.01,
paper_mode=True, # simulate, do not settle
max_daily_toll_per_agent=5.0, # cap each caller at $5/day
)
create_tollbooth(config)
@monetize(price_usd=0.01, source_agent="research-agent")
def score_lead(company: str) -> dict:
"""Score a sales lead. Charged per call."""
return {"company": company, "score": 0.0}
The numbers above are illustrative. The point is that the cap is configuration, not code you write per function. When a caller crosses its daily limit, the charge is recorded as failed and the underlying function does not execute.
Read usage with get_usage and the JSONL ledger
Metering is only useful if you can read it back. The package exposes get_usage(), which returns a dictionary of call counts keyed by function. Call it with no argument for everything, or pass a function to get just that one.
pythonfrom nano_empire_tollbooth import get_usage
summarize("First. Second. Third. Fourth.")
summarize("Another. Set. Of. Sentences.")
print(get_usage()) # {'__main__.summarize': 2}
print(get_usage(summarize)) # {'__main__.summarize': 2}
Counts are the live view. The durable record is the JSONL ledger, an append-only file with one JSON object per charge. Each line carries the task id, the source and target agent, the amount in USD, the settlement status, and a timestamp. Because it is line-delimited JSON, you read it with the standard library and no extra dependency.
pythonimport json
with open("logs/toll_ledger.jsonl", encoding="utf-8") as f:
for line in f:
record = json.loads(line)
print(record["task_id"], record["amount_usd"], record["status"])
The ledger path is configurable through TollboothConfig(ledger_path=...) if you want it somewhere specific. The append-only format means the file doubles as an audit trail. You can replay it, sum it, or feed it into whatever reporting you already run. Nothing about reading your own usage requires calling out to a service.
Paper mode versus live mode
Every monetized function starts in paper mode. Paper mode runs the full path, metering, ledger writes, and cap checks, but settles nothing. The first 100 calls are free in paper mode, which gives you and your callers room to integrate and verify behavior before any real charge exists. This is informational and not financial advice. Switching modes does not change your function signature or your callers' integration.
You flip to live with one config field, or with an environment variable so you do not touch code between environments.
pythonfrom nano_empire_tollbooth import TollboothConfig
# Live: settle real charges instead of simulating
config = TollboothConfig(paper_mode=False)
terminalexport TOLLBOOTH_PAPER_MODE=false
In live mode the tollbooth can lock a charge in escrow, release it when the call succeeds, and refund it when the call fails, so a failed call does not bill the caller. Live settlement is part of Tollbooth Pro at $19/mo, which also adds unlimited metered calls and cloud sync for the ledger. The x402 hook lets agents pay per call over the HTTP 402 Payment Required flow, and x402 is optional rather than required. If you prefer to settle through your own books, the escrow and ledger still operate without it.
For a side-by-side look at how this compares to a traditional provider, see Stripe vs Nano Empire. For the why behind machine-paid calls, read why AI agents need payment rails.
Putting it together
The full loop is short. Decorate the function, set a price, run it in paper mode, read get_usage() and the ledger to confirm the records look right, then set paper_mode=False when you are ready to settle. Because the decorator wraps both sync and async functions and the configuration carries the caps and ledger path, the surface you maintain stays small.
- Install:
pip install nano-empire-tollbooth - Decorate: add
@monetize(price_usd=...)above your function - Meter: call the function, then read
get_usage() - Inspect: open the JSONL ledger and confirm the charge records
- Go live: set
paper_mode=Falsewhen the records check out
If you want a deeper background read on this pattern, the companion piece on how to monetize a Python function covers the same package from the product angle, and the Nano Empire AI home page links the rest. Decorator billing keeps the billing concern where it belongs: one line, next to the code it prices.
Charge per API call without building a billing service.
Install from PyPI, decorate a function, and read your usage in minutes. Paper mode is free to start, no credit card required.