MAQPNADocs

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 maqpna CLI and either a gateway with an access token (auditor or admin) or a copy of the ledger file.
  • For sandbox time and per-tenant grouping: cluster access (AgentSession and Tenant objects). 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.
Signed usage report.cast

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-MM for a calendar month (UTC), or --from and --to (RFC 3339 or YYYY-MM-DD, end exclusive).
  • --tenant acme for one tenant's report.
  • --installation-id (or MAQPNA_INSTALLATION_ID); the default is the UID of the kube-system namespace.
  • --license FILE puts your licence ID in the report.
  • --default-vcpu (default 1) is used for sandbox time without CPU data; the report is then flagged estimated.

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 PUB prints OK and exits 0.
  • The report's ledgerHead matches maqpna audit verify on 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#