# US-039 — Caller Credit granted once per account; chat requires account keys

Status: planned
Priority: Critical (blocks friends-test inference — every request 402s)
Stage: Designed

## Context

`DEFAULT_STARTING_CREDIT = 0.0` and nothing can raise it: no CLI flag, no admin
credit endpoint. The only funding path is the on-chain USDT deposit watcher —
so on a fresh public tracker every inference request returns
`402 insufficient balance`, including keys created through the working
account flow (`/v1/auth/register` → `/v1/account/keys`, also in the dashboard).

Separately, the billing gate accepts **any** bearer string (only revoked keys
are rejected), so wiring starter credit into `BillingLedger.ensure_client`
would let anyone mint unlimited free credit by inventing keys.

Decision (grilled 2026-07-06): **Caller Credit is granted once per account**,
when the account creates its first API key; chat requests on an
accounts-enabled tracker must present a real active `sk-mesh-` key.
Rejected: credit-any-bearer (free-inference farming against volunteers' GPUs
on the open internet) and manual-admin-credit-only (operator toil, no
self-serve testing).

## Design

1. **`BillingLedger.grant_caller_credit(api_key, account_id, amount) -> bool`**
   Appends a credit event with the deterministic id
   `caller-credit-{account_id}` and note `caller-credit`. Already-seen id →
   no-op returning False. The deterministic id makes the once-per-account rule
   idempotent locally *and* across hive gossip (event dedupe is by id), so a
   second key, a retried request, or a replicated event can never double-grant.
2. **`AccountStore.is_active_key(api_key) -> bool`.** The chat billing gate
   (`_handle_proxy_chat`) rejects keys that are not active account keys with
   `401 invalid_api_key` — but only when an accounts store is configured, so
   embedded/test trackers without accounts keep today's any-key behavior.
3. **`--starting-credit N`** (USDT) on `meshnet-tracker start`. Default is the
   single constant `DEFAULT_CALLER_CREDIT_USDT = 1.0` in `server.py`
   (devnet-friendly alpha, revised 2026-07-06; set 0 on mainnet). Threaded
   through `TrackerServer` → `_TrackerHTTPServer`; registration and
   `_handle_account_key_create` call `grant_caller_credit` when billing is on
   and the amount is positive. The ledger's own `_starting_credit` stays 0 —
   implicit per-key grants in `ensure_client`/`charge_request` remain dead
   code paths.
4. **Stack variable `STARTING_CREDIT`** in
   `deploy/portainer/meshnet-tracker-nobuild-stack.yml` (default 1).
5. **Docs.** QUICKSTART gains "Get an API key" (register/login/create key via
   dashboard or curl, what 402 means); DEPLOY_PORTAINER's stale "any key is
   accepted and starts with the configured starter credit" paragraph replaced.

## Acceptance criteria

- Fresh account → first key → key has `--starting-credit` balance; chat succeeds
- Second key on the same account → no additional credit
- Revoke-and-recreate keys → still no additional credit (deterministic event id)
- Random bearer string on an accounts-enabled tracker → 401, never 402/free work
- Tracker without accounts store: gate behavior unchanged
- `--starting-credit 0` disables the grant entirely (mainnet posture)
- `python -m pytest` passes from repo root
