Usage metering and node counting#
MAQPNA meters usage: the units a service provider bills its tenants for, and the units a licence is sized by. There are four kinds: sandbox seconds, governed calls, model tokens and approval requests. All of them come from evidence MAQPNA already keeps, mostly the audit ledger, so usage is tamper-evident and survives the garbage collection of finished sessions. Usage is separate from spend (the cost that budgets limit, see budget enforcement).
flowchart LR
subgraph Sources
OP["Operator: session ends<br/>readyAt to finishedAt"]
GW["Gateway: tool, model,<br/>approval decisions"]
end
OP -- "POST /v1/usage/sandbox<br/>projected SA token" --> GW
GW --> L[("Audit ledger<br/>decision usage, allow, deny,<br/>require_approval")]
L --> B["maqpna usage buckets<br/>hourly, per tenant"]
B --> R["maqpna usage report<br/>signed JSON + FOCUS CSV"]
N["Operator leader: billable nodes<br/>every minute"] --> CM[("ConfigMap maqpna-usage-nodes<br/>day and month high-water")]
CM --> R
R --> V["maqpna usage verify<br/>signature + totals"]
1. Sandbox time#
- When a session ends with an outcome and a
readyAt, the operator posts{namespace, session, agent, user, tier, readyAt, finishedAt}to the gateway'sPOST /v1/usage/sandbox(Helm sets--usage-report-urlto the gateway;usageReporting.enabledis true by default). It authenticates with a projected ServiceAccount token (audiencemaqpna-sandbox-usage), re-read on every call. - The gateway validates the token with a TokenReview and accepts only the configured reporters (the operator's ServiceAccount).
- It meters the sandbox time (
RecordSandbox, priced per tier fromsandboxPerSecondUSD), so session cost and budgets include it. - It appends a ledger record with
decision: usage,reason: sandbox_usageandextkind,tier,sandboxSeconds,readyAt,finishedAt(andforkRootfor forks). Reports are idempotent per session. - On a 2xx the operator sets
status.outcome.usageReport: Recorded. It retries failures every 30 seconds and, after one hour, marks the reportLostwith aUsageReportFailedevent. The operator also reports before removing the session's finalizer, so deleting a session does not lose its time.
The operator's report is not signed; its integrity comes from the hash-chained ledger it lands in. Sessions that never became ready are not billed.
2. Hourly buckets#
maqpna usage buckets aggregates usage per hour, per tenant, with counts only (no user or session identities):
| Kind | SKU | Unit | Source |
|---|---|---|---|
sandbox |
Trust tier | Seconds | usage ledger records, or AgentSession status for sessions not yet in the ledger |
tool |
MCP server | Requests | Governed tool-call records |
model |
Model route | Tokens | Model-call records |
approval |
Rule | Requests | Approval requests |
Namespaces map to tenants through Tenant.spec.namespaces; usage outside any tenant has an empty tenant. Records come from a ledger file (--ledger FILE, offline) or the gateway's /v1/audit/records (--gateway URL). Sessions found in the ledger are not counted twice.
3. Signed usage reports#
A service provider bills its customers, and trues up with its MAQPNA licence, with a signed usage report:
sequenceDiagram
autonumber
participant SP as Service provider
participant CLI as maqpna usage report
participant GW as Gateway or ledger file
participant K as Kubernetes
participant C as Customer or auditor
SP->>CLI: --period 2027-01 --key KEYURI [--tenant T]
CLI->>GW: ledger records for the period
CLI->>K: AgentSessions, Tenants, maqpna-usage-nodes
CLI->>CLI: buckets, totals, ledger head, node high-water,<br/>vCPU-hours (estimated when no CPU data)
CLI->>CLI: payload = RFC 8785 JCS of the report without signature<br/>detached JWS, EdDSA, typ maqpna-usage-report+jws
CLI-->>SP: report.json + report.focus.csv + summary
SP->>C: report.json
C->>CLI: maqpna usage verify report.json --pubkey PEM [--ledger FILE]
CLI->>CLI: verify signature, recompute totals from buckets,<br/>optionally match the ledger head
CLI-->>C: OK or TAMPERED (exit 3)
maqpna usage report (--period YYYY-MM | --from DATE --to DATE) --key KEYURIreads the ledger and the cluster and builds a report with schemamaqpna.com/usage-report/v1:periodStart,periodEnd,generatedAt,partial(the period is not over),installationId,licenseId,tenant,ledgerHead {seq, hash},nodes {count, available},cpu,totals {sandboxSeconds, sandboxVcpuHours, governedCalls, modelTokens, approvals},tenants[]andbuckets[].- It signs the report with a detached compact JWS (RFC 7515 Appendix F):
alg: EdDSA,typ: maqpna-usage-report+jws, over the RFC 8785 canonical form of the report without itssignature. The key is apkg/keysURI you hold. - It writes
report.json,report.focus.csv(FOCUS 1.2, the FinOps cost-export format, list price × quantity, SKUssandbox-vcpu-hours,governed-calls,model-tokens,approvals) and prints a summary. maqpna usage verifychecks the signature (--pubkeyor--jwks) and recomputes every total from the buckets, so an edited total is caught even with a valid signature on the original. With--ledger FILEit also checks that the ledger head in the report matches.maqpna usage exportwrites a signed, pseudonymised report (tenant, namespace and SKU names hashed) for an air-gapped true-up with MAQPNA. Nothing is sent: you upload the file yourself.
4. Node counting#
A billable node is a node running at least one pod labelled maqpna.com/session (a sandbox).
- The operator's leader replica samples every
--node-count-interval(default 1 minute; 0 disables): it lists session pods (uncached), keeps scheduled, unfinished ones and counts distinctnodeNamevalues. - It records the day and month high-water marks in the ConfigMap
maqpna-usage-nodesin the gateway namespace (keysday.YYYY-MM-DD,month.YYYY-MM,updatedAt; 62 days and 25 months kept). - It exports
maqpna_billable_nodes{period=current|day|month}. - Usage reports and
maqpna license statusread the month's high-water mark. The count is reported, never enforced: exceeding the licence's node limit raises the alertMaqpnaLicenseNodeLimitExceededand nothing else.
What you see#
Formats from cmd/maqpna/usage.go and usage_report.go (values illustrative):
$ maqpna usage buckets --from 24h
HOUR TENANT NAMESPACE KIND SKU QUANTITY UNIT
2026-10-02 14:00 acme acme-prod sandbox tier-1-microvm 5412.7 Seconds
2026-10-02 14:00 acme acme-prod tool github 318 Requests
2026-10-02 14:00 acme acme-prod model acme-prod.coder 184220 Tokens
2026-10-02 14:00 acme acme-prod approval prod-guard/k8s-delete 2 Requests
$ maqpna usage verify report.json --pubkey usage.pub.pem
OK: report.json: signature valid (kid 6sQ9h1...), totals match 2184 buckets
period 2027-01-01 to 2027-02-01, installation inst-7f3a, 1840.2500 vCPU-hours, 412870 governed calls, 98123400 model tokens, 312 approvals
maqpna usage report prints signed with kid <kid>; wrote report.json, report.focus.csv and a table with the columns TENANT VCPU-HOURS SANDBOX-HOURS GOVERNED-CALLS MODEL-TOKENS APPROVALS LIST <currency>. A tampered report prints TAMPERED: report.json: <cause> and exits 3. See maqpna usage buckets, maqpna usage report and maqpna usage verify.
Failure modes#
| Failure | Effect |
|---|---|
| Gateway unreachable when a session ends | The operator retries every 30 seconds for an hour, then marks the report Lost; maqpna usage buckets still counts the session from its status while it exists |
maqpna-usage-nodes unreadable |
The report carries nodes {count: 0, available: false} and says so |
| No CPU data for sandboxes | vCPU-hours are marked ESTIMATED, sized at --default-vcpu (1) |