Usage metering and billing
Count usage per tenant from the audit ledger and sessions, produce signed usage reports with a FOCUS cost export, verify them, and export pseudonymised reports for an air-gapped true-up.
flowchart LR L[(Audit ledger<br/>calls · tokens · approvals)] --> B[maqpna usage buckets<br/>hourly, per tenant] K[AgentSessions<br/>sandbox time] --> B B --> R[maqpna usage report<br/>signed report.json<br/>report.focus.csv] R --> V[maqpna usage verify<br/>signature + totals + ledger head] R --> X[maqpna usage export<br/>pseudonymised, for true-up] L --> F[maqpna costs focus<br/>per-call FOCUS export]
Goal#
Measure what each tenant used, turn it into a report your customer (or MAQPNA) can check independently, and feed it into billing or FinOps tools.
Usage is metered units for billing:
| Unit | Source | SKU in reports |
|---|---|---|
| Sandbox time | AgentSession status (readyAt to outcome.finishedAt, or now while running), and finished sessions recorded by the gateway in the ledger |
sandbox-vcpu-hours |
| Governed calls | Allowed tool calls in the audit ledger. Denied calls are recorded but not billed | governed-calls |
| Model tokens | Model calls through the gateway's model route | model-tokens |
| Approvals | Calls held for approval | approvals |
Prerequisites#
- The
maqpnaCLI and either a gateway with an access token (auditororadmin) or a copy of the ledger file. - For sandbox time and per-tenant grouping: cluster access (
AgentSessionandTenantobjects). Without it, pass--no-sessions: you get calls, tokens and approvals only, and no tenant mapping. - A signing key you hold. Reports are signed with the audit-checkpoint key, so the same public key (
/v1/audit/jwks) verifies checkpoints and reports.
Steps#
1. Look at hourly buckets#
Offline, from a ledger file (a local MAQPNA writes it to .maqpna/audit.jsonl):
maqpna usage buckets --ledger .maqpna/audit.jsonl --no-sessions
Output from a local run:
HOUR TENANT NAMESPACE KIND SKU QUANTITY UNIT
2026-10-03 03:00 - dev model stub 52 Tokens
2026-10-03 03:00 - dev tool echo 4 Requests
2026-10-03 03:00 - team-a tool echo 1 Requests
Against a gateway and cluster, for one tenant over the last day:
maqpna usage buckets --from 24h --tenant acme
--from takes a duration (24h) or an RFC 3339 time; --to defaults to now. --format csv writes hour,tenant,namespace,kind,sku,unit,quantity; -o json gives the buckets as JSON. Usage outside any tenant shows - as the tenant.
2. Choose the signing key#
--key takes a key URI: a file path or file:///abs/path (Ed25519 PKCS#8 PEM), pkcs11:… or kms://<provider>/<key-id>. The release binaries include only the file backend; pkcs11: and kms:// need a backend built into your binary. In production, use the audit-checkpoint key from the Secret named in audit.checkpointSigning.secretName (key audit-signing.pem). To try it locally, generate one:
maqpna keygen -out keys --name checkpoint
private key: keys/checkpoint.key
public key: keys/checkpoint.pub
kid: 9FHvKLplpeVjzWvxxFWPh1ehLcgi_pTY8-7Q7coiPjE
3. Produce a signed report#
maqpna usage report --period 2026-10 --key keys/checkpoint.key \
--ledger .maqpna/audit.jsonl --no-sessions --installation-id lab-fra-1 --out reports
Output from a local run:
! Node high-water mark unavailable: kubeconfig: invalid configuration: no configuration has been provided, try setting KUBERNETES_MASTER environment variable
Usage report 2026-10-01 to 2026-11-01 (UTC) [partial: period not over]
installation lab-fra-1, licence none
ledger head seq 7 hash 81dff220de43203a25b8270b39c4ad65bc6a1eafb00e46d521a4a8b87c8328c8
billable nodes (high-water): unavailable
TENANT VCPU-HOURS SANDBOX-HOURS GOVERNED-CALLS MODEL-TOKENS APPROVALS LIST USD
(no tenant) 0.0000 0.0000 5 52 0 0.00
TOTAL 0.0000 0.0000 5 52 0 0.00
signed with kid 9FHvKLplpeVjzWvxxFWPh1ehLcgi_pTY8-7Q7coiPjE; wrote reports/report.json, reports/report.focus.csv, reports/report.summary.txt
The warning appears because there was no cluster: the billable-node mark comes from the ConfigMap maqpna-usage-nodes (see licensing).
The three files:
| File | Contents |
|---|---|
report.json |
Report maqpna.com/usage-report/v1: period, partial (period not over yet), installationId, ledgerHead (seq and hash), nodes, cpu, totals, per-tenant totals and the hourly buckets, signed with an Ed25519 detached JWS over the canonical JSON (signature.jws) |
report.focus.csv |
One FOCUS 1.2 row (FOCUS is the FinOps cost-export format) per tenant and SKU: list price × quantity, with x_MaqpnaInstallationId, x_MaqpnaLicenseId and x_MaqpnaLedgerHeadSeq |
report.summary.txt |
The table above |
Options:
--period YYYY-MMfor a calendar month (UTC), or--fromand--to(RFC 3339 orYYYY-MM-DD, end exclusive).--tenant acmefor one tenant's report.--installation-id(orMAQPNA_INSTALLATION_ID); the default is the UID of thekube-systemnamespace.--license FILEputs your licence ID in the report.--default-vcpu(default 1) is used for sandbox time without CPU data; the report is then flaggedestimated.
4. Price it with your own price list#
By default the FOCUS export uses the Operator edition list prices. Pass a price file to bill at your own prices. SKUs you leave out keep the defaults:
# prices.yaml
currency: EUR
skus:
governed-calls: {unitPrice: 0.05, per: 1000}
sandbox-vcpu-hours: {unitPrice: 0.015, per: 1}
maqpna usage report --period 2026-10 --key keys/checkpoint.key --ledger .maqpna/audit.jsonl \
--no-sessions --installation-id lab-fra-1 --price-file prices.yaml --out reports-eur
The summary's last column becomes LIST EUR, and the FOCUS rows carry BillingCurrency EUR. Valid SKUs are sandbox-vcpu-hours, governed-calls, model-tokens and approvals; an unknown SKU or a negative price is an error.
5. Verify a report#
Anyone with the public key can check a report offline:
maqpna usage verify reports/report.json --pubkey keys/checkpoint.pub --ledger .maqpna/audit.jsonl
OK: reports/report.json: signature valid (kid 9FHvKLplpeVjzWvxxFWPh1ehLcgi_pTY8-7Q7coiPjE), totals match 3 buckets
period 2026-10-01T00:00:00Z to 2026-11-01T00:00:00Z, installation lab-fra-1: 0.0000 vCPU-hours, 5 governed calls, 52 model tokens, 0 approvals
ledger head seq 7 matches .maqpna/audit.jsonl
verify checks the signature, recomputes the totals from the buckets and, with --ledger, checks the report's ledger head against a ledger copy. Use --jwks with the gateway's /v1/audit/jwks (a URL or a file) instead of --pubkey.
A report changed after signing (here governedCalls edited from 5 to 2) fails with exit status 3:
TAMPERED: t.json: usage report: invalid signature: kid "9FHvKLplpeVjzWvxxFWPh1ehLcgi_pTY8-7Q7coiPjE"
6. Export for an air-gapped true-up#
For a licence true-up without any network connection, export the signed report with tenant, namespace and SKU names pseudonymised. Nothing is sent; you hand over the file:
maqpna usage export --period 2026-10 --key keys/checkpoint.key --ledger .maqpna/audit.jsonl \
--no-sessions --installation-id lab-fra-1 --out usage-2026-10.json
! Node high-water mark unavailable: kubeconfig: invalid configuration: no configuration has been provided, try setting KUBERNETES_MASTER environment variable
signed with kid 9FHvKLplpeVjzWvxxFWPh1ehLcgi_pTY8-7Q7coiPjE; wrote usage-2026-10.json
tenant, namespace and SKU names are pseudonymized; nothing was sent (upload the file for the true-up)
The file has "pseudonymized": true and verifies with maqpna usage verify. export writes the signed JSON only, so it does not take --price-file. Sending the report online is not built.
7. Export per-call costs (FOCUS)#
For FinOps tools, maqpna costs focus exports costs per call and charge period, with agent, session and user tags:
maqpna costs focus --ledger .maqpna/audit.jsonl --out costs.csv
maqpna costs focus --gateway "$MAQPNA_GATEWAY_URL" --since 2026-09-01T00:00:00Z --format json --out costs.json
--period sets the charge-period granularity (default 1 h). Running totals and windows are in maqpna costs summary:
maqpna costs summary --scope agent --window month
month window 2026-10 (2026-10-01T00:00:00Z to 2026-11-01T00:00:00Z, UTC)
SCOPE KEY COST USD TOOL CALLS MODEL CALLS IN TOKENS OUT TOKENS
agent dev/coder 0 4 1 8 44
agent team-a/reviewer 0 1 0 0 0
Costs are what you configured in the gateway's pricing; budgets are covered in budgets and cost limits.
Verify#
maqpna usage verify report.json --pubkey PUBprintsOKand exits 0.- The report's
ledgerHeadmatchesmaqpna audit verifyon the same ledger (see audit ledger and evidence). - The FOCUS CSV imports into your FinOps tool with currency and SKU columns filled.
Troubleshooting#
| Symptom | Cause | Fix |
|---|---|---|
sessions need cluster access (or --no-sessions) |
No kubeconfig and no --no-sessions |
Configure the cluster context, or add --no-sessions |
! Node high-water mark unavailable |
No cluster, or the operator has not written maqpna-usage-nodes yet |
Run with cluster access; the report is still signed |
--tenant reports zero |
--no-sessions skips the Tenant objects, so no namespace maps to a tenant |
Run with cluster access |
price file: unknown SKU |
A key in skus is not one of the four SKUs |
Use sandbox-vcpu-hours, governed-calls, model-tokens or approvals |
TAMPERED: … invalid signature |
The report was changed, or you used the wrong public key | Compare the kid with your key's; re-run the report |
Report flagged estimated |
Sandbox time without CPU data used --default-vcpu |
Set CPU requests on the agents' sandboxes |
Next steps#
- Multi-tenant setup for service providers: per-tenant reports.
- Licensing: node counting and licence limits.
- Audit ledger and evidence: the ledger behind every report.
- Command reference:
maqpna usage report,maqpna usage verify,maqpna usage export,maqpna costs focus.