Appearance
Usage
Usage is what this host writes down about the work it did, and what a pool has been charged for it. A record is written once and never edited, and it is read back through a scheme of its own - usage: - rather than a command, so a client uses the resource calls it already has. What it is not is an enforcement: nothing here refuses anything, and the rows that decide who may use which agent, model and computer are POLICY.md.
Terms, one line each:
| Term | |
|---|---|
| a usage record | One thing that was spent - a model call, or a stretch of computer time |
| a pool | The name a record is charged to: a person, a team or a project |
| the owner | Who the work belongs to, as a typed reference - user:<id>, team:<name>, project:<team>:<project> or root:<host> |
| the store | Where the records are kept and what a pool has been charged |
| the charged cost | What the pools are charged, in US dollars |
| the provider's cost | What the provider or the harness reported, kept beside the charged one |
| the split | A cost reported in two parts, what the tokens sent cost and what the tokens received cost |
What a record is
Two kinds share one base and one port. A model call is kind: 'model', written by the proxy for a call through /v1 (source: 'proxy') or by the agent meter for a turn a session's own harness ran (source: 'agent'). Computer time is kind: 'computer', a stretch a machine was up, written against the machine's owner.
Every record carries when it happened, what wrote it, who it belongs to and where it ran: the owner, the team and the project it is charged under, the session, the chat and the turn, the agent provider, and the computer when there was one. The base is the same for both kinds, so a listing by agent or by computer is a filter and never a join.
A model record carries the model's <maker>/<name>, the provider it ran on, and the tokens in three parts: input, output, and cache split into what was read from and written to the provider's cache. A pool's tokens is all three added together, because those are the tokens that were billed, and the three counts are kept apart as well so a report can say what was sent and what came back.
The two costs
cost is what the pools are charged and providerCost is what the provider or the harness reported, and they are two fields because they are two different things. A cost carries from, which says which of the two worked it out: price when this host computed it from a price list, harness when the agent or the provider reported it.
A call the proxy priced is charged that price and keeps the provider's figure beside it. An agent's harness calls its model itself, so the figure it reported is both what the pools are charged and what the provider reported. A cost of 0 on a turn that counted no token is a harness saying it spent nothing, which is no cost at all: it is left off rather than written as 0, because a record of 0 there would charge a pool for a call that never happened.
A harness that priced what was sent and what was received apart says so beside the amount, and those two parts are the split. They are kept as input and output on the cost, and a pool's inputUsd and outputUsd add them over the records that have them - a measure nothing was charged in is absent rather than zero, so a total over records that never split says nothing about the split rather than saying it was nothing.
Pools
A record names the pools it is charged to, and pools on the record is the whole of it: the owner, team:<name>, and project:<team>:<project>. Each is named only when the record has it, so work with no owner and no scope is charged to no pool rather than to one whose name says nothing. The name is opaque to the store; what a pool means belongs to the policy plan.
The store keeps a live total per pool per day, rebuilt when it starts up, because a limit is checked before a call and debited as usage arrives - an answer read off the month's file would be a total from before the last few seconds. A day is the granularity, which is exact for the ranges a limit asks about and counts a partly covered day whole.
What a pool reads
Through the usage: scheme, one leaf under the pool.
| URI | What it answers |
|---|---|
usage:// | The pools a record was charged to that this reader may read, as directory entries |
usage://<pool> | That pool as { pool, kind, name, day, week, month }, the last three each a total |
usage://<pool>/day, /week, /month | One of those periods on its own |
usage://<pool>/range?from=&until= | One total between two days |
usage://<pool>/records?from=&until= | The records charged to it, newest first, at most the newest 200 |
usage://groups?by=&from=&until= | The records between two days summed by user, team and project, one row per set of key values |
A listing is the same for every reader: the pools the store has records for, limited to the pools the reader may read. Root and a member see the same row for the same pool, and a pool nothing was charged to is listed for neither, so a team or project that spent nothing has no row until its first record.
A pool name holds colons, so it is one encoded path segment: usage://project%3Abackend%3Asearch is the pool project:backend:search and not an authority called project. A record naming no instant is kept nowhere and said out loud, which is the only answer that is not a charge in the wrong place. A request for a leaf that is not one of the five is -32008.
A pool's kind and name
kind is the pool key's prefix: user, team, project or root. name is what a person reads the pool by: the user's id, the team's title, <team title> / <project title>, or the host of a root: pool. Each title falls back to its id, so project:backend:ahpapp is Backend / ahpapp when the team has the title Backend and the project has none. The titles come from the people directory, and a reader needs no team or project grant to get them: only the title goes out, and only for a pool the reader may read.
A range
range, records and groups take from and until, each a day as 2026-10-01 or an ISO 8601 instant, and compare them at the day as the totals do. A bound left out is the first day of the current month in the configured zone, and now. A bound that is not a date is -32602, rather than a range of every record or of none. range and records over the same from and until agree: the records are the ones that total summed.
Groups
usage://groups sums the records of a range by the keys in by, a comma-separated list of user, team and project. It answers one row per distinct set of key values:
json
[
{
"keys": { "team": "team:backend", "project": "project:backend:ahpapp" },
"names": { "team": "Backend", "project": "Backend / ahpapp" },
"total": { "usd": 5, "calls": 2 }
}
]Each key is spelled as the pool key the record is charged under: user is the owner, which is root:<host> for a root connection's work, team is team:<team> and project is project:<team>:<project>. A key the records had none of is left out of keys, so team work grouped by team,project is a row with a team and no project. Each name is the pool's name as above. A by naming another key is -32602, and no by at all is one row for the whole range.
A record counts once in a group, however many pools it is charged to, where a pool row counts it once under each of its pools. A record counts when the reader may read at least one pool it is charged to, so a member grouping by user sees their team's spending split by person. The store reads the month files for this, which is slower than a pool total over a long range, and a store plugin answers the same call.
The three periods are ranges in the configured zone rather than in UTC, so a week starts on the deployment's Monday 00:00 and not the evening before - see the timezone key below. until is always now, so the day behind the current hour is a partly covered one and is counted whole. The records a period's total summed are the records that period's records leaf lists, because both compare the range at the day.
Reading usage by command
Each read above is also an x- command a client sends on its connection, which answers the same value as JSON rather than as text in a data string. The usage: scheme stays, and both answer from the same functions. Usage has no channel: nothing is sent when a record is written.
| Command | Params | Answers what this read answers |
|---|---|---|
x-ahpd/usage/pools | none | usage://, as [{ pool, kind, name }] |
x-ahpd/usage/totals | pool | usage://<pool>, as { pool, kind, name, day, week, month } |
x-ahpd/usage/range | pool, from?, until? | usage://<pool>/range |
x-ahpd/usage/records | pool, from?, until? | usage://<pool>/records |
x-ahpd/usage/groups | by?, from?, until? | usage://groups |
pool is the pool key as written, user:ana, not encoded. from and until are strings with the defaults and the refusals of a range. by is a list, ["user", "team"], rather than a comma-separated string, and a key that is not one of the three is refused with the sentence the read gives. A param of the wrong type is -32602.
json
{ "id": 7, "method": "x-ahpd/usage/groups", "params": { "by": ["user"] } }json
{ "id": 7, "result": [{ "keys": { "user": "user:ana" }, "names": { "user": "ana" }, "total": { "calls": 2, "providerUsd": 4, "usd": 4 } }] }Each command needs usage:read, and lets in a reader without it by the rules under Grants: pools and groups let in every signed-in reader and answer only what that reader may read, and totals, range and records let a reader in for a pool they may read. Another person's pool is -32009, <id> may not usage:read here, as the read is. The five are listed on the handshake under _meta['ahpd.methods'].
Configuration keys
Both keys are in the daemon's configuration file, under usage. Neither has a flag, because both are properties of a deployment rather than of one run.
| Key | Values | Default | What changes |
|---|---|---|---|
usage.per | turn or report | turn | turn holds the running sum a harness reports and writes one record when the turn ends. report writes one record for every usage report, each holding what that report added since the one before it, so a long turn is billed while it is still running |
usage.timezone | an IANA zone name, as America/Sao_Paulo | the system's own zone | The zone a day begins in and a week starts in. A week is Monday 00:00 there, not the system's own |
Both modes bill the same work, and a wrong value is refused by name at startup: usage.per must be one of turn or report, and a timezone this host cannot read is one line at start and the system's own zone.
Where the records are kept is not a key: the daemon writes them into a usage folder beside its own configuration, and a host built from the SDK is handed its own store. Everything else about the daemon's keys is HOST.md.
Commands
The ahpd usage commands are server/USAGE.md.
Grants
Reading a pool is usage:get on the usage subject, which like every scheme's carries the resource operations under its own name - get, list, resolve, watch in the read group - and usage:read is the group that holds all four (RESOURCES.md). There is no write half, because a record is never edited and nothing is made, and this provider implements get, list and resolve: a usage:watch is a grant a role may hold and an operation nothing here answers.
What a person may see is not only the grant. Their own user:<id> pool, and the team: and project: pools of the teams and projects they belong to, are readable with no grant at all, because a client showing somebody their own spending has to be able to. Any membership in a team reads team:<team>, so a member reads the team pool their own work is charged to. team:* reads every project:<team>:<project> and team:<project> reads that one, and a bare team reads no project pool. The grouped read at usage://groups needs no grant either, as the listing needs none, and it counts only the records the reader may read. Every other pool needs usage:read. That is the provider's own authorize rather than a rule in the host, which is how a scheme opens part of itself without a host change. Where the pools come from is USERS.md.
See also
| RESOURCES.md | What a scheme is, and the grants a resource method asks for |
| POLICY.md | The limits a pool's total is checked against |
| PROXY.md | The calls through /v1 that write the proxy records, and the prices they are charged |
| SESSIONS.md | The turns the agent records belong to |
| USERS.md | The people, teams and projects a pool is named after |
| HOST.md | The daemon's keys, usage beside the rest |