# qalkeys — the API-token ↔ Qals value exchange

**One service, three modes, port :8841.** Any HTTP API becomes sellable for
Qals; pre-paid third-party keys change hands under escrow; companies track
agent/team usage per cost centre — all settled through loopd holds, anchored
on-chain, zero-dependency (Python stdlib only).

```
qalkeys.py        the service (:8841)
qk.py             minimal CLI (list, buy, usage, call, keys, buykey, reveal, org-*)
mock_upstream.py  local OpenAI-style mock upstream for tests
test_qalkeys.sh   66-assertion end-to-end proof (loopd :8823 + chain :9000)
```

## MODE 1 — Metered gateway (the flagship)

**Sell any upstream API for Qals.** A seller registers a listing (HMAC
proof-of-ownership via loopd's revoke-oracle, issuer-style per qalclaim);
buyers mint a `qk_` key and call the metered proxy:

```bash
# seller (agent_id + secret = their loopd credentials)
curl -X POST :8841/listings -H "X-Sig: $SIG" -d '{
  "seller_handle": "apiseller", "agent_id": "ag_…", "secret": "…",
  "name": "GPT-5 gateway",
  "upstream_url": "https://api.openai.com/v1/chat/completions",
  "price_per_call_cents": 3, "price_per_1k_tokens_cents": 2,
  "upstream_auth": "sk-seller-NEVER-SHARED" }'

# buyer: mint a key (shown ONCE), fund the prepaid payer agent, call
curl -X POST :8841/buy -d '{"listing":"ls_…","buyer_handle":"myagent"}'
curl -X POST :8823/topup -d '{"agent_id":"<payer_agent_id>","cents":2000}'
curl -X POST :8841/v1/proxy/ls_… -H "Authorization: Bearer qk_…" -d '{…}'
```

The gateway forwards with the **seller's** upstream credentials (encrypted at
rest, never exposed), meters tokens from the upstream response's
`usage.total_tokens`, and charges via **loopd holds**: one hold covers a
batch (10 calls or 25c accrued, whichever first), settled at actual; the
seller is paid **exactly** the metered cents (settle + residual-sink, see
accounting note in `qalkeys.py`). Every 100th settle is anchored on-chain
(`QALKEYS_ANCHOR_EVERY`). Per-key daily cap (default **500c**), per-key rate
limit (**10 rps, burst 20** → 429).

**Pricing patterns:** per-call only; per-call + per-1k-tokens
(`floor(tokens×rate/1000)`); meter-only at 0c. Charges cap at the batch
hold's room (header `X-Qalkeys-Charge-Truncated` marks a truncated call).

## MODE 2 — Key escrow resale

**Sell your spare API tokens.** Got a pre-paid third-party key you don't
fully use? List it for a fixed price; the key is encrypted at rest; the buyer
pays into the `qalkeys-escrow` loopd agent, `POST /keys/{id}/reveal` verifies
the payment receipt against the loopd ledger and reveals the key **exactly
once** (second reveal → 409 — enforced in code, not policy). Seller payout is
deferred by a **cooldown** (default 24h); within it the buyer can
`POST /keys/{id}/dispute` → full refund from escrow, listing burned.

## MODE 3 — Company usage tracking

Register an org (`POST /org/register` → `ok_` admin key), mint internal keys
per **team** (`POST /org/keys`, each backed by its own loopd agent = the
cost-centre ledger), meter identically through the same proxy, then
`GET /org/report` → per-team calls/tokens/cents + totals, with the team
agents debited exactly. Bind team keys to your own listing for pure internal
chargeback, or to an external one to track real spend.

## Security model (v1, honest)

- **Buyer never sees seller secrets.** Upstream credentials are sealed
  (HMAC-CTR keystream + HMAC tag, master key 0600) and only ever attached
  server-side. The `qk_` key is the *only* credential a buyer holds; only its
  sha256 is stored.
- **Seller proof:** X-Sig HMAC + loopd revoke-oracle (state-preserving
  no-op) — forged credentials → 401, tested.
- **Caps & limits:** per-key daily cap → 402; token bucket → 429; hold
  refused → 402 with top-up instructions.
- **One-time reveal:** enforced server-side (409 on second reveal); escrow
  payout + dispute windows; every trade anchored.
- **Escrow honesty note:** we CAN'T revoke a seller's upstream access after a
  resale — a seller could keep using the key. Mitigations: cooldown +
  dispute-refund from escrow, one-time reveal, on-chain trade proof, burned
  status. Beyond that it's seller-trust; the fix is seller-deposited
  collateral (upgrade path).
- **Operator-trusted:** qalkeys is the escrow trustee and holds the payer
  sub-agent secrets (paper phase, same model as qalbazaar/qalclaim).

**Paper-phase accounting note:** loopd (paper) holds never debit balance and
settle credits the release back, so an unconsumed batch residual is swept to
the `qalkeys-residual` sink agent — this keeps the payer's net debit EXACTLY
the metered cents and the seller's income EXACTLY the metered cents. In the
Move phase, holds debit at hold-time and the sink disappears (same note as
qalredeem's sweep).

## Upgrade path

1. **x402-compatible header mode** (from `09_identity_ai/AI_PAYMENTS_LANDSCAPE.md`):
   unpaid proxy calls return **402** with an `accepts` array carrying a
   `qals` scheme (chain-verifiable B-QALS transfer against the caller's DID);
   stock x402 clients negotiate it, our facilitator verifies settlement.
   v1's `qk_` Bearer flow is the same interchange with the key as the
   credential.
2. **Delegated holds:** loopd macaroon-style attenuated credentials (L402
   lineage) let a buyer derive a capped sub-credential — kills the prepaid
   sub-agent and the residual sink.
3. **On-chain escrow** (qal_credit holds) for MODE 2 → threshold release
   removes the single trustee.
4. **x402-MCP wrappers:** expose listings as MCP tools that 402 when unfunded.

## Run / test

```bash
python3 qalkeys/qalkeys.py          # :8841 (loopd must be on :8823)
bash qalkeys/test_qalkeys.sh        # 66 checks, self-starts service + mock
python3 qalkeys/qk.py list|buy|usage|call|keys|buykey|reveal|org-*
```

Env: `QALKEYS_ANCHOR_EVERY` (100) · `QALKEYS_KEY_COOLDOWN_MS` (24h) ·
`QALKEYS_BATCH_CALLS` (10) · `QALKEYS_BATCH_VALUE_CENTS` (25) ·
`QALKEYS_RATE_RPS` (10) · `QALKEYS_RATE_BURST` (20) ·
`QALKEYS_DEFAULT_KEY_CAP_CENTS` (500) · `QALKEYS_PORT` (8841)

Files live in `qalkeys/data/` (archived, never deleted, by the test).
