AgentBill
One spend ceiling per job, enforced before the call goes out.
Your provider's cap is bound to a project, to an organization over a calendar month, or to one
session on that vendor's own harness. This one is bound to job-142.
The problem
You built an AI agent. It does something valuable. You charge $99/month flat.
- A 3-second run costs you $0.80. You made $98.20.
- A 45-minute recursive loop costs you $140. You lost $41.
A monthly cap does not catch that, because a loop that burns $140 over one weekend is too small to move a monthly number, and a monthly number low enough to catch it takes every agent you run down with it until the 1st.
The fix: name the job, give it a ceiling, then ask before each call
Give the job a name and a ceiling in the console at agentbill.dev/app,
or with PUT /tasks/:task_ref/ceiling. Then every call names the job and says what this one call
is worth, and nothing about the budget:
from agentbill import AgentBillClient, TaskCeilingExceededError
client = AgentBillClient(api_key="agb_your_key")
try:
# job-142 already has its ceiling. Ask, run, settle.
client.preflight(agent_id="researcher", task_ref="job-142", estimated_units=12)
result = call_your_llm("quarterly report")
client.record(agent_id="researcher", task_ref="job-142", units=12)
except TaskCeilingExceededError as refused:
# approved: false. Your code decides what the job does next.
print(refused)
That's it. On every call AgentBill now:
- Reserves the units before your provider call goes out, in the same statement that checks them, so ten parallel calls cannot all be approved for the last 8 units
- Refuses with
TaskCeilingExceededErroronce the job's total would cross its ceiling - Settles what the run actually used on
record();record(..., success=False)releases the reservation without billing, and thegatedecorator further down does both for you
task_ref is the whole idea: a second agent, a different tool and a retried step all pass the same
one, and they are all checked against a single ceiling. agent_id is a label for attribution and
carries no budget of its own.
Install
pip install agentbill-sdk
npm install agentbill
Quick start (5 minutes)
1. Get an API key
AGENTBILL_API_KEY=your_key_here
2. Put the ceiling on the job
Two parameters do the work. task_ref is your name for this run, and every call that passes it is
checked against the same ceiling. The ceiling itself is set in units you define, in the console or
with PUT /tasks/:task_ref/ceiling before the run starts; passing task_ceiling on the first
preflight of a new run opens the job from code instead, the alternate. A task_ceiling on a later
preflight is not applied, so a retry cannot raise the ceiling it was meant to respect.
from agentbill import AgentBillClient
client = AgentBillClient(api_key="agb_your_key")
# Explicit: preflight before, record after. job-142 already has its ceiling.
client.preflight(agent_id="researcher", task_ref="job-142", estimated_units=12)
result = call_your_llm("quarterly report")
client.record(agent_id="researcher", task_ref="job-142", units=12)
# Or let the decorator do both. On an exception it settles with success=False,
# which releases the reservation instead of billing it. 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)
Every later call in the same 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:
# A different agent, a different tool, the same run and the same ceiling.
client.preflight(agent_id="writer", task_ref="job-142", estimated_units=40)
3. Handle the refusal
The exception carries the numbers, so the handler can say what happened without a second call.
from agentbill import TaskCeilingExceededError
try:
result = run_agent("quarterly report")
except TaskCeilingExceededError as e:
# Your code decides: retry later, degrade, alert.
alert_ops(f"run {e.task_ref} hit its ceiling of {e.task_ceiling} units")
One rule, identical in the Node SDK: it raises when your spend rule refused the call, and returns a result when AgentBill's own billing did.
| Refusal | What you get |
|---|---|
ceiling_exceeded |
raises CeilingExceededError |
task_ceiling_exceeded |
raises TaskCeilingExceededError |
budget_exhausted |
raises BudgetExhaustedError |
free_tier_exceeded |
returns approved=False with .upgrade_url |
plan_limit_exceeded |
returns approved=False with .upgrade_url |
The last two mean our quota ran out, not that your budget did. AgentBill running out of quota must never crash your agent, so those come back as a result you can degrade on rather than an exception that takes the process down.
check = client.preflight("researcher", estimated_units=5)
if not check.approved:
# only free_tier_exceeded / plan_limit_exceeded reach here; the rest raised
alert_ops(f"AgentBill quota: {check.reason}", check.upgrade_url)
Changed in 0.6.0.
free_tier_exceededandplan_limit_exceededused to raiseFreeTierExceededError/PlanLimitExceededError. They now return a result. Both classes are still exported so your imports keep working, but nothing raises them any more. If you were catching them, checkresult.approvedinstead. The other three are unchanged.
A wrong key is neither a refusal nor a quota state. The server answers 401 and every call in the
SDK raises AuthenticationError, which carries the server's error (unauthorized,
key_revoked or key_expired) and its message, verbatim, followed by where the key came from
and how to get back in. It subclasses AgentBillError.
Added in 0.6.3. Until then a 401 surfaced as a bare
requestsHTTPError: 401 Client Errorwith the server's sentence dropped, and a key with a non-ASCII character in it died as aUnicodeEncodeErrorinsidehttp.client. The client now rejects the second at construction, before any request is made.
4. Watch the console
Open https://agentbill.dev/app and paste your API key:
- Live task burn-down: each job's ceiling, what it has spent, and what is still reserved
- Every refusal, with the reason your code got back
- Per-customer balances, key health, and this month's plan quota
- What refuses a call on your account today, in the order preflight checks it
There is no signup wall on the sample: https://agentbill.dev/app?demo=1 is the same page with
invented data.
Task budgets: "this job gets 500 units"
The same mechanism as the Quick start, with the two pieces that section left out: what the refusal carries, and how to read a job's burn-down while it runs. The ceiling is set on the first preflight; every later call reserves against the same budget, and the call that would cross it is refused before the money is spent.
from agentbill import AgentBillClient, TaskCeilingExceededError
client = AgentBillClient(api_key="agb_...")
# The alternate to the console: open the job from code, with task_ceiling on its first call
client.preflight("researcher", estimated_units=2,
task_ref="job-42", task_ceiling=50)
# ... run your LLM / tool call, then record what actually happened
client.record("researcher", units=2, task_ref="job-42")
# Every later call just names the task
try:
client.preflight("researcher", estimated_units=10, task_ref="job-42")
except TaskCeilingExceededError as e:
print(f"job-42 is done: {e.task_used_units}/{e.task_ceiling} units spent")
# Live burn-down
status = client.get_task("job-42")
print(status.used_units, "/", status.ceiling_units)
Retries and abandoned runs
A reservation is placed by preflight() and released by record(). Two things can go wrong between them, and both are handled explicitly.
A retried preflight. Without an idempotency key, retrying a timed-out check reserves the budget a second time, so the mechanism meant to prevent waste is the one consuming it. Pass a key that is stable across retries:
from agentbill import AgentBillClient, PreflightInProgressError
client = AgentBillClient(api_key="agb_...")
try:
check = client.preflight(
"researcher", estimated_units=12,
task_ref="job-142", task_ceiling=500,
idempotency_key="job-142:summarize", # stable across retries
)
except PreflightInProgressError:
... # the original is still being decided. Not a refusal, nothing reserved.
Same key, same decision, one reservation.
A run that never comes back. If the process crashes between preflight() and record(), the units stay reserved: nothing else can spend them, and the remaining budget reads smaller than it is, until a sweeper reclaims them once the reservation passes its TTL, returned on every approved check as check.reservation_expires_at.
Note the direction. An abandoned reservation makes the ceiling tighter, never looser. The gate does not open by accident.
Settle every run, including the ones that fail. record(..., success=False) releases the reservation without billing, and the gate decorator does it for you.
A reservation bigger than the call. A record that does not name its reservation settles the oldest reservations of that customer and task_ref by the units you pass, and no more: reserve 70,000 and record 8,000, and the other 62,000 stay held until the reservation expires. Every approved check carries check.reservation_id. Settle with check.record(units=actual), which carries the agent, customer, task_ref and reservation for you, or pass reservation_id=check.reservation_id to client.record(...): that reservation closes whole, the actual is what the job spent, and the rest is released at once. gate does it for you. Settling the same reservation twice releases it once.
Since 0.7.0.
reservation_id,PreflightResult.record(), andrecord()'sidempotency_key,reservation_id,metadataandusage_missingarguments are in 0.7.0 and later, not in 0.6.5 or earlier. They also need an AgentBill API that returnsreservation_idon preflight; against one that does not,check.reservation_idisNoneand records settle as before.
Automatic metering with wrap()
Since 0.7.0.
wrap()is in 0.7.0 and later, not in 0.6.5 or earlier. It needs an AgentBill API that returnsreservation_idand acceptsunit: "token".
Wrap your model client once. Every call it makes through chat.completions.create and responses.create (OpenAI), messages.create (Anthropic) or models.generate_content and generate_content_stream (google-genai, aio included), sync or async, streamed or not, is measured from the usage the provider returned: a preflight on the job in tokens before the call, a record of the reported tokens after it. You pass no estimate and record no number.
from openai import OpenAI
from agentbill import wrap, Refusal
# Reads AGENTBILL_API_KEY. The job is counted in tokens; task_ceiling opens it.
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": "Summarize the quarter in one line."}],
)
if isinstance(reply, Refusal):
# The call that would have passed the ceiling was not sent. Nothing is
# raised: reply.reason, reply.used, reply.ceiling, reply.remaining.
print(reply)
else:
print(reply.choices[0].message.content)
-
A refusal is returned, never raised. The measured call's value is a
Refusal(approved=False,reason,task_ref,asked,used,ceiling,remaining,upgrade_urlon a quota refusal, andanswer, the preflight answer whole);bool()of it isFalse, and it has nochoices,content,candidatesorusage, so it cannot be read as a provider response. A refused streaming call returns the sameRefusal, which iterates to nothing (forandasync forrun zero times, and awithblock is a no-op); 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. An exception out of a wrapped call is a failure: the provider's own error, or from AgentBillAuthenticationError(401),requests.HTTPError(any other HTTP error, a 422task_unit_mismatchincluded) orrequests.ConnectionError.preflight()andrecord()on the plain client are not changed by this:preflight()still raisesTaskCeilingExceededErroron a ceiling refusal and returns on the quota. -
The estimate needs no number from you. Before the job's first measured call in this process it is
default_estimate(2,000); after, the job's running average per call, never more than the average prompt plus the call's ownmax_tokens(ormax_completion_tokens,max_output_tokens). The prompt is not counted before the call, and no request is added. A call that uses more than it reserved is still recorded at what it used, so one call can take the job past its ceiling, by at most that call for each caller running at the same moment; the next preflight is refused. That bound holds while preflight checks the ceiling; see the quota below. -
After the call the record carries
idempotency_key= the provider's response id (a random key on a-compatibleendpoint, whose ids need not be unique), the preflight'sreservation_id, and metadata: provider, model, tokens by type (input,cache_read,cache_write,output,reasoning),duration_ms, and thestepyou named. No prompt, no answer. For another step of the same job,wrap(llm, step="review"). -
Tokens by type: OpenAI's
cached_tokensandcache_write_tokensarecache_readandcache_write; with Gemini's automatic function calling (a Python function intools), each round the SDK sends is its own measured call, because the response it returns carries only the last round's usage. -
Missing usage is recorded as missing, never as 0. A provider error releases the reservation. A record that fails after the provider answered warns and still returns the answer. A stream is recorded when it ends, is closed, or a
forloop over it stops. -
Each measured call is one preflight, so it uses one preflight of your account's monthly quota. Once that quota is spent no ceiling can be checked, so by default (
on_quota="refuse") the wrapped call returns aRefusalwith reasonfree_tier_exceededorplan_limit_exceededand.upgrade_url, and is not sent.wrap(..., on_quota="send")sends it unchecked and records it, with a warning once per job, and nothing bounds the job until the quota resets or you upgrade. -
Only wrapped calls are measured. Another method, an unwrapped client, a tool call or a GPU run counts only if your code records it with the same
task_ref. A client pointed at another host is recorded asopenai-compatible(oranthropic-compatible) and gets no list price. -
The job is counted in tokens. One opened in units answers 422
task_unit_mismatch, and the call is not sent. -
What it cost:
client.get_task("tokens-1").breakdownhas the job by model and by step, with tokens andlist_price_usd_estimate, an estimate at public list price from a dated price table. List price, your invoice may differ; a model with no list price is counted inunpriced_calls, never as $0.
Since 0.8.0: a ceiling in dollars.
wrap(client, task_ref="job-9", agent_id="writer", task_ceiling_usd=0.50)opens the job with a ceiling in dollars at public list price (unit"usd"), andunit="usd"meters a dollar job opened in the console. Before each measured call the server reserves the job's recent median call, or $0.10 before its first priced call, so a job whose whole ceiling is under $0.10 needsestimated_usdfrom the plain client. Each record is charged the list price of the tokens the provider reported; a call it cannot price is charged its reservation, never $0. A refusal on such a job hasunit == "usd"and says dollars. On the plain client,preflight(..., task_ceiling_usd=..., estimated_usd=...), and the result carriestask_unit,estimated_usd,task_remaining_usdandestimate_source. Needs an AgentBill API from 2026-09-25 or later.
Node.js
import { preflight, record, TaskCeilingExceededError } from 'agentbill'
// Reads AGENTBILL_API_KEY. job-142 gets 500 units across every call
// that passes it, however many that turns out to be.
try {
await preflight({ agentId: 'researcher', taskRef: 'job-142',
taskCeiling: 500, estimatedUnits: 12 })
const result = await callLLM('quarterly report')
await record({ agentId: 'researcher', taskRef: 'job-142', units: 12 })
} catch (e) {
if (e instanceof TaskCeilingExceededError) {
// e.taskRef, e.taskCeiling, e.taskUsedUnits
handleRefusal(e.taskRef)
}
}
Units for outcomes, not tokens
Most usage meters count events. They have no concept of "did the task actually succeed?"
AgentBill does. The unit count is a function of the result. You decide what success means:
# Support agent, count units only when the ticket is resolved
@meter(
event="ticket_resolved",
customer_id_from="customer_id",
units=lambda result: 5 if result["resolved"] else 0,
)
async def resolve_ticket(customer_id: str, ticket_id: str) -> dict:
resolution = await run_support_agent(ticket_id)
return resolution # {"resolved": True, "summary": "..."}
# Coding agent, count units only when tests pass
@meter(
event="code_generated",
customer_id_from="customer_id",
units=lambda result: 10 if result["tests_passed"] else 0,
)
async def generate_code(customer_id: str, spec: str) -> dict:
code = await run_coding_agent(spec)
passed = run_tests(code)
return {"code": code, "tests_passed": passed}
# Research agent, count by volume processed
@meter(
event="research_completed",
customer_id_from="customer_id",
units=lambda result: result["pages_processed"],
)
async def research(customer_id: str, topic: str) -> dict:
return await run_research_agent(topic)
# returns {"summary": "...", "pages_processed": 14}
If the units resolve to 0, no event is recorded and nothing is drawn from that customer's balance.
Why AgentBill?
1. The ceiling is bound to a job, not to a month
A usage meter records after the fact, so the number arrives with the invoice. Provider spend caps are consulted before the call, and they are real. What they are bound to is a project, or an organization over a calendar month, or one session on that vendor's own harness. A 3-hour research loop spread across two providers and a scraping tool is none of those.
AgentBill's ceiling is bound to a task_ref you choose. If the units already used plus this call's
estimate would cross it, preflight answers approved: false and the SDK raises before your provider
call goes out. Your code decides what the job does next.
Usage meter: run → record → the number arrives with the invoice
Monthly cap: run → run → run → ... → dark until the 1st
AgentBill: consult this job's ceiling → approved: false → your code decides → settle
This matters when a single agent run costs $0.80 on a good day and $43 on a bad one.
2. Lives inside your function
Nothing to emit from your infrastructure. AgentBill is a decorator: it wraps your function directly and handles the preflight, the reservation, the settle, idempotency and error handling.
And there is no proxy in your request path. Your provider call is still yours; AgentBill answers a separate allow/deny question next to it.
Current scope: what AgentBill solves today
AgentBill is designed for atomic, short-running agent tasks: functions that complete in a single execution and return a deterministic result.
Works well for:
- Research runs, report generation, document processing
- Support ticket resolution (single attempt)
- Code generation with test validation
- Any agent function that runs once and returns a clear result
Not yet supported:
- Multi-signal outcomes, tasks where success is determined by multiple events over time (e.g., a ticket that gets reopened 3 days later)
- Long-running workflows, agents that run for hours or days across multiple steps
- Outcome invalidation, billing reversal when a previously "successful" result is later undone
These are real problems. They require a different architecture: event sourcing, state machines, reversal logic. If you're building at that level of complexity, AgentBill's current version isn't the right tool yet.
For atomic tasks, it is one decorator.
How it works
Your agent code
│
▼
client.preflight(task_ref=...) then client.record(...) ← or the gate decorator
│
├─ POST /preflight → reserve the units in the same statement that checks them
│ │
│ ├─ over the task ceiling → raise TaskCeilingExceededError, nothing reserved
│ ├─ over the per-call ceiling → raise CeilingExceededError
│ ├─ customer out of budget → raise BudgetExhaustedError
│ └─ our own quota gone → return approved=False with .upgrade_url, never raise
│
├─ Run your function (LLM or tool call happens here)
│
├─ [succeeded] POST /events → settle the reservation with what it really used
│
└─ [raised] POST /events success=False → release the reservation, bill nothing
Nothing is billed for a run that raised. And note which way an abandoned run fails: the units stay reserved until the sweeper reclaims them, so the ceiling gets tighter, never looser.
API reference
client.preflight(...) and @client.gate(...)
| Option | Type | Default | Description |
|---|---|---|---|
agent_id |
str |
required | A label for attribution, not a budget. Nothing is capped by it. |
task_ref |
str |
none | Your name for this run. Every call passing it shares one ceiling. |
task_ceiling |
int |
none | The run's total, in units you define. Opens a new task_ref; required then unless the job was opened first from the console or PUT /tasks/:task_ref/ceiling. Not applied once the job exists. |
estimated_units |
int |
1 |
What this one call is worth. This is the amount reserved. |
customer_id |
str |
"default" |
Your internal customer identifier. Carries its own balance. |
idempotency_key |
str |
none | Stable across retries: same key, same decision, one reservation. |
ceiling |
int |
none | Set on AgentBillClient(...), not per call. Refuses any single call whose estimated_units exceed it. |
Base URL. AGENTBILL_BASE_URL (or AgentBillClient(base_url=...)) must be https, or plain http to localhost, 127.0.0.1 or [::1], since 0.8.0. Any other value raises AgentBillError before a request is sent, because every request carries your API key. approved is read as approved only when the server's answer is JSON true.
client.record(...) and check.record(...)
check.record(...) takes units, success, idempotency_key, usage_missing and metadata, and reads agent_id, customer_id, task_ref and reservation_id from the preflight that made check.
| Option | Type | Default | Description |
|---|---|---|---|
agent_id |
str |
required | The same attribution label you passed to preflight. |
units |
int |
1 |
What the call actually used. 0 is allowed: a call that ran and cost nothing records 0. |
customer_id |
str |
"default" |
The customer the preflight named. |
task_ref |
str |
none | The job the preflight named. Without it the units settle against no job. |
reservation_id |
str |
none | check.reservation_id. Closes that reservation whole and releases what it held beyond units. Without it the oldest reservations are settled by units only. |
success |
bool |
True |
False releases the reservation and bills nothing. |
idempotency_key |
str |
a fresh random key | Same key, one event: a retried record is ignored as a duplicate. Pass something stable, such as your provider's response id. |
usage_missing |
bool |
False |
Your provider reported no usage for this call. Not read as 0: the call is charged at least the reservation the record settles, the one reservation_id names or, without it, the oldest open reservation of this customer and task_ref. With no reservation open, units is recorded as sent. Either way the job counts it in usage_missing_calls. |
metadata |
dict |
none | Stored on the event, never counted. |
client.preflight(...) also takes unit, "unit" (yours, the default) or "token", with a task_ref: it is read when the call opens the job and checked on a job that exists, and a different unit is a 422. client.get_task(...) returns it as status.unit, beside status.usage_missing_calls.
@meter(event, options)
Separate tool, for outcome-based metering rather than enforcement: it records what a run was
worth after the fact, with units as a function of the result.
| Option | Type | Default | Description |
|---|---|---|---|
event |
str |
required | Event label, shown in the console |
customer_id |
str |
none | Fixed customer identifier |
customer_id_from |
str |
none | Name of a function parameter to read customer_id from |
units |
int | callable |
1 |
Units per call, or a function (result) -> int returning 0 to skip billing |
task_ref |
str |
none | Attributes the event to a task budget: a job opened in the console, by PUT /tasks/:task_ref/ceiling, or by a first client.preflight(task_ref=..., task_ceiling=...) |
metadata |
dict |
none | Static key-value pairs attached to every event |
preflight=Trueon@meteris not the task ceiling. It callsGET /budgetand refuses only if that customer's balance is already exhausted: no estimate, no per-call ceiling, no task ceiling, and no reservation, so it gives you none of the concurrency guarantee above. Use@client.gate(...)orclient.preflight(...)for a ceiling. The option is kept for the accounts that already depend on it.
Exceptions
| Exception | When |
|---|---|
TaskCeilingExceededError |
The run's total would cross its task_ceiling. Carries .task_ref, .task_ceiling, .task_used_units, .task_remaining_units |
TaskCeilingRequiredError |
A task_ref preflight has never seen arrived without a task_ceiling |
CeilingExceededError |
estimated_units exceed the per-call ceiling set on the client |
BudgetExhaustedError |
That customer's balance is gone |
PreflightInProgressError |
A preflight with the same idempotency_key is still being decided |
AgentBillError |
Network error or unexpected server response |
preflight() never raises FreeTierExceededError or PlanLimitExceededError: AgentBill's own
quota running out returns approved=False with .upgrade_url instead. Our billing must never crash
your agent. A client made with wrap() raises for no refusal at all: every one, the quota included,
is returned as a Refusal (once the quota is spent no ceiling can be checked), and on_quota="send"
sends the call unchecked instead.
Self-hosting
git clone https://github.com/marketinglior-pixel/agentbill
cd agentbill
cp .env.example .env # add your DATABASE_URL and AGENTBILL_API_KEY
npm install
npm run dev
Requires: Node 20+, PostgreSQL 14+
Roadmap
- Core metering (
POST /events), idempotent peridempotency_key - Cross-call task ceilings (
task_ref+task_ceiling), reserved atomically - Per-call ceiling and per-customer balances
- Outcome-based metering (
units=lambda) - Live console at
/app - Webhooks, alerts at 80% and 100% credit usage
- Multi-signal outcome support
- Team accounts
One ceiling per job, consulted before the call goes out. preflight answers approved: false; your
code decides what happens next.
Release files for agentbill-sdk 0.8.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| agentbill_sdk-0.8.0.tar.gz | 66.0 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| agentbill_sdk-0.8.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 106.7 kB
Release files / agentbill_sdk-0.8.0.tar.gz
| Download URL | agentbill_sdk-0.8.0.tar.gz |
|---|---|
| Size | 66.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
f3c1b2b043a2a7151988b899735842d4d272f297f1542d7f9c04e8e9eb5b0b7c
|
|
BLAKE2b-256 checksum How to use checksums |
9a6d6a62040a484ec365c986721c71585655794c75158c214a8fa2f3f0fc6c14
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.9.6
|
Release files / agentbill_sdk-0.8.0-py3-none-any.whl
| Download URL | agentbill_sdk-0.8.0-py3-none-any.whl |
|---|---|
| Size | 40.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
4a64444eb0fde143e064003c1f80840da1f8b33bf79bcf39fe9d9a56432ae616
|
|
BLAKE2b-256 checksum How to use checksums |
19542869609b3754fb90fae44cc9966d70332a84b8706bd7be9d1a97c31a8b66
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.9.6
|