Sell AI/upstream API usage priced in Qals; escrowed key resale; org tracking · qalkeys/README.md
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:
# 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
- x402-compatible header mode (from
09_identity_ai/AI_PAYMENTS_LANDSCAPE.md): unpaid proxy calls return 402 with anacceptsarray carrying aqalsscheme (chain-verifiable B-QALS transfer against the caller's DID); stock x402 clients negotiate it, our facilitator verifies settlement. v1'sqk_Bearer flow is the same interchange with the key as the credential. - 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.
- On-chain escrow (qal_credit holds) for MODE 2 → threshold release removes the single trustee.
- x402-MCP wrappers: expose listings as MCP tools that 402 when unfunded.
Run / test
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).