MAQPNADocs

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#

  1. When a session ends with an outcome and a readyAt, the operator posts {namespace, session, agent, user, tier, readyAt, finishedAt} to the gateway's POST /v1/usage/sandbox (Helm sets --usage-report-url to the gateway; usageReporting.enabled is true by default). It authenticates with a projected ServiceAccount token (audience maqpna-sandbox-usage), re-read on every call.
  2. The gateway validates the token with a TokenReview and accepts only the configured reporters (the operator's ServiceAccount).
  3. It meters the sandbox time (RecordSandbox, priced per tier from sandboxPerSecondUSD), so session cost and budgets include it.
  4. It appends a ledger record with decision: usage, reason: sandbox_usage and ext kind, tier, sandboxSeconds, readyAt, finishedAt (and forkRoot for forks). Reports are idempotent per session.
  5. On a 2xx the operator sets status.outcome.usageReport: Recorded. It retries failures every 30 seconds and, after one hour, marks the report Lost with a UsageReportFailed event. 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)
  1. maqpna usage report (--period YYYY-MM | --from DATE --to DATE) --key KEYURI reads the ledger and the cluster and builds a report with schema maqpna.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[] and buckets[].
  2. 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 its signature. The key is a pkg/keys URI you hold.
  3. It writes report.json, report.focus.csv (FOCUS 1.2, the FinOps cost-export format, list price × quantity, SKUs sandbox-vcpu-hours, governed-calls, model-tokens, approvals) and prints a summary.
  4. maqpna usage verify checks the signature (--pubkey or --jwks) and recomputes every total from the buckets, so an edited total is caught even with a valid signature on the original. With --ledger FILE it also checks that the ledger head in the report matches.
  5. maqpna usage export writes 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).

  1. 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 distinct nodeName values.
  2. It records the day and month high-water marks in the ConfigMap maqpna-usage-nodes in the gateway namespace (keys day.YYYY-MM-DD, month.YYYY-MM, updatedAt; 62 days and 25 months kept).
  3. It exports maqpna_billable_nodes{period=current|day|month}.
  4. Usage reports and maqpna license status read the month's high-water mark. The count is reported, never enforced: exceeding the licence's node limit raises the alert MaqpnaLicenseNodeLimitExceeded and 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)