3.5 KiB
metering — the one way to meter usage to commerce
github.com/hanzoai/commerce/metering is the single, reusable hook every Hanzo
product uses to charge for usage. It is the DRY replacement for the balance-check
and usage-record logic that was copy-pasted across the LLM/cloud path
(ai/routers/filter_balance.go, ai/controllers/openai_api.go,
gateway/auth_middleware.go). Any product — search, functions, storage, a CLI —
imports this package and gets the proven, fail-closed billing gate plus usage
recording, against commerce, the single billing source of truth.
What it does
Two operations, matching the proven cloud/gateway path:
| Op | Commerce endpoint | Purpose |
|---|---|---|
Authorize |
GET /v1/billing/balance, then GET /v1/billing/alerts/authorize |
Pre-request balance + spend-cap gate. Fail-closed, always. |
Record |
POST /v1/billing/usage |
Post-request usage write. Every served call writes a row, $0 included. |
Auth is the commerce service token (admin-scoped S2S):
Authorization: Bearer ${COMMERCE_SERVICE_TOKEN}
X-Org-Id: <tenant org slug>
The token is a secret and must come from KMS (the operator wires it from a
KMS-backed secret into COMMERCE_SERVICE_TOKEN). This package never reads it
from disk.
Use it (middleware — the common case)
meter, _ := metering.FromEnv() // COMMERCE_URL + COMMERCE_SERVICE_TOKEN (KMS) + COMMERCE_SERVICE_ORG
mux.Use(meter.Middleware(metering.MiddlewareConfig{
Provider: "search",
Price: func(r *http.Request, status int, in metering.AuthInput) int64 {
if status >= 200 && status < 300 { return 5 } // 5¢ per successful request
return 0 // counted, not charged
},
Skip: func(r *http.Request) bool { return r.URL.Path == "/healthz" },
}))
Middleware is plain func(http.Handler) http.Handler, so it composes with the
standard library, gorilla/mux (.Use), chi, and anything that speaks
http.Handler — no per-framework variants.
It reads the caller identity from the gateway-minted X-User-Id / X-Org-Id
headers (the trust boundary). The gateway strips client-supplied copies on
ingress, so these are safe to trust downstream.
Use it (imperative — per-unit pricing)
in := metering.IdentityFromGatewayHeaders(r)
if err := meter.Authorize(ctx, in); err != nil {
// ErrInsufficientBalance / ErrSpendCapExceeded -> 402 ; other -> 503
}
// ... do work, measure cost ...
meter.Record(ctx, metering.Usage{User: in.User, Org: in.Org, AmountCents: cents, Provider: "functions"})
Configuration (env, operator-wired)
| Var | Default | Notes |
|---|---|---|
COMMERCE_URL |
http://commerce.hanzo.svc.cluster.local:8001 |
Commerce base (no /v1 suffix). |
COMMERCE_SERVICE_TOKEN |
— | Admin-scoped S2S token. KMS-sourced. |
COMMERCE_SERVICE_ORG |
hanzo |
Default tenant org (X-Org-Id). |
No variable turns the gate off, opens it on error, or routes it to a sandbox ledger.
Fail-closed contract (aligned with the gateway)
Authorize returns:
nil→ allow.ErrInsufficientBalance→ out of funds → 402.ErrSpendCapExceeded→ funded, over a scope's cap → 402.- any other error → balance or cap unknown → deny → 503.
New refuses an empty COMMERCE_URL, and a nil *Client refuses every call
with ErrNotConfigured (503): nothing is served unchecked. A Middleware with
no Price refuses every metered request.
This is the same balance source and the same status mapping the gateway uses — no divergent logic.