Skip to content

Monitor balance and usage

Your balance is held on chain and debited request by request. It is a cliff, not a slope: while there are funds every request is served, and at zero every request returns 402. Nothing warns you on the way down unless you build the warning. This page covers the account endpoints, an alert you can put on a schedule, and what to do when the 402 arrives anyway.

  • An account with a funded balance. See Get 0G and fund your account.
  • An mk- management key with the account:read scope, exported as ZG_MANAGEMENT_KEY.

This is the first thing that trips people up, so it comes before the code.

/v1/account/* accepts only an mk- management key carrying the account:read scope. An sk- inference key returns 403 insufficient_scope, always — not because the scope is missing but because inference credentials cannot read account state at all. Two credentials, two jobs:

Prefix Used for Reaches /v1/account/*
sk- Inference requests, billed against your balance No — 403 insufficient_scope
mk- with account:read Reading balance and usage Yes

Keep them in separate environment variables. ZG_API_KEY goes to the runtime that serves traffic; ZG_MANAGEMENT_KEY goes only to the job that reads your finances. Grant the key account:read and nothing more — a balance alert has no reason to be able to create keys. See API keys for creating and scoping both kinds.

Terminal window
# Spendable balance. ZG_MANAGEMENT_KEY holds an mk- key with account:read.
curl "$ZG_BASE_URL/account/balance" \
--max-time 30 \
--fail-with-body \
--silent \
-H "Authorization: Bearer $ZG_MANAGEMENT_KEY"
Terminal window
# Usage over a window. Omit end_date to leave the window open.
curl "$ZG_BASE_URL/account/usage/stats?start_date=2026-07-01&end_date=2026-07-20" \
--max-time 30 \
--fail-with-body \
--silent \
-H "Authorization: Bearer $ZG_MANAGEMENT_KEY"

GET /v1/account/funds is a third endpoint on the same scope. Where balance answers “how much can I spend”, funds shows where that money sits:

{
"address": "0xcc539270e65f88c2b3798c4e827610cf1fb89574",
"currency": "0g",
"total": "97149123997100000000",
"router": {
"deposit": "0",
"credit": "97149123997100000000",
"pending_charge": "0",
"subtotal": "97149123997100000000"
},
"payment_layer": {
"balance": "0",
"shared_across_products": true,
"queried_at": "2026-07-20T08:24:55Z"
}
}

Live response, 2026-07-20. router is the balance this API spends from, split into what you deposited directly and any credit. payment_layer reports a balance held at the account level that is shared across 0G products rather than reserved for inference. Both are reported so that a zero router balance next to a non-zero payment_layer balance is diagnosable rather than mysterious.

usage/stats returns account totals — requests, tokens, and cost — and, as of 2026-07-20, no breakdown by trust tier. If you need to show how much traffic ran sealed, record it yourself: every response carries the provider address in x_0g_trace, and the tier a provider can serve is in the catalog’s verifiability field. Logging provider per request (see Track what each request costs) gives you evidence from the response rather than an assertion about your own configuration.

Prompts and completions are never stored, so anything you cannot reconstruct from your own logs is not recoverable later. See Trust modes for what each tier guarantees.

Poll on a schedule — every few minutes is plenty. This is not an inference endpoint and it does not belong in the request path.

#!/bin/sh
# Balance alert. Exits non-zero when the balance is at or below the threshold,
# so cron or a monitoring agent can page on it.
set -eu
: "${ZG_MANAGEMENT_KEY:?ZG_MANAGEMENT_KEY is not set}"
BASE_URL="${ZG_BASE_URL:-https://router-api.0g.ai/v1}"
THRESHOLD="${ZG_BALANCE_THRESHOLD:?set a threshold in the same unit the API returns}"
response=$(curl "$BASE_URL/account/balance" \
--max-time 30 \
--fail-with-body \
--silent \
-H "Authorization: Bearer $ZG_MANAGEMENT_KEY")
balance=$(printf '%s' "$response" | jq -r '.total_balance')
# Balances are returned as strings and can exceed shell integer range,
# so compare as floating point rather than with test -lt.
if awk -v b="$balance" -v t="$THRESHOLD" 'BEGIN { exit !(b <= t) }'; then
printf 'router balance %s at or below threshold %s\n' "$balance" "$THRESHOLD" >&2
exit 1
fi

The field to read is balance (total_balance carries the same value), and like every amount in this API it is an integer string in the smallest unit of 0G — 1018 per 0G. A threshold of “one 0G” is therefore 1000000000000000000, not 1.

Two notes on picking the threshold:

  • Alert on runway, not on a fixed number. Divide the current balance by recent burn from usage/stats, and page when the remaining time drops below however long it takes a human to fund the account — hours, not minutes. A fixed number stops meaning anything the moment traffic doubles.
  • Alert separately on zero. A traffic spike can outrun any runway estimate mid-incident.
{
"error": {
"message": "Insufficient balance to process request",
"type": "payment_error",
"code": "insufficient_balance"
},
"request_id": "<REQUEST_ID>"
}

The envelope is verified against the live API; the 402 body itself is reproduced from the error contract rather than triggered against a funded account.

Do not retry it. No amount of backoff creates funds, and a retry loop converts a funding problem into a funding problem plus a rate-limit problem. Fail the request, page whoever can fund the account, and deposit — see Get 0G and fund your account. Funds are spendable within a few seconds of the deposit transaction confirming.

Everything above is visible at pc.0g.ai if you would rather look than poll:

Dashboard tab Shows
Overview Current balance, spend and request count for the period, active keys, recent requests
Activity Full request history by model, time, and cost
Billing Current balance and the funding entry

The Console is the right surface for a spot check or for the price of a model. The endpoints are the right surface for anything that needs to page someone at 3am.

GET /v1/account/balance:

{
"address": "0xcc539270e65f88c2b3798c4e827610cf1fb89574",
"currency": "0g",
"deposit_balance": "0",
"credit_balance": "97163900867100000000",
"pending_charge": "0",
"total_balance": "97163900867100000000",
"balance": "97163900867100000000"
}

GET /v1/account/usage/stats:

{
"total_requests": 63,
"total_tokens": 1438636,
"total_cost": "2850876002900000000",
"prompt_tokens": 1420254,
"completion_tokens": 18382,
"currency": "0g"
}

Live responses, 2026-07-20. total_cost is an integer string in the smallest unit of 0G: 2850876002900000000 is 2.8509 0G.

Status code Cause Fix
401 missing_authorization No Authorization header Send Bearer mk-...
401 invalid_api_key The management key is wrong or was revoked Issue a replacement in the Console
403 insufficient_scope An sk- key was used, or the mk- key lacks account:read Use an mk- key with account:read
402 insufficient_balance Balance exhausted on an inference call Fund the account; do not retry
429 rate_limit_exceeded Polling too aggressively Honour Retry-After; poll on a schedule, not per request

Full list with retry guidance: Error codes.