Redeem QALS for services: SKUs, fees, dust, anchored receipts · qalredeem/README.md
qalredeem — the QALS Redemption System
Created: 2026-09-09 · Service: 127.0.0.1:8835 · Zero-dependency (Python stdlib only)
The operational half of the AU$1 story: customers turn their loopd credit balance into real qalarc services — services only, no cash-out, by policy (see content/token_supply.md §Cash-out policy and 03_qals_architecture/QALS_BACKING_DESIGN.md §1–2).
python3 qalredeem/qalredeem.py serve # listens 127.0.0.1:8835
bash qalredeem/test_qalredeem.sh # 57 end-to-end proofs (all green)
1. The flow
wallet/agent qalredeem :8835 loopd :8823
────────────── ──────────────────── ─────────────────
GET /catalog ────────────► pick sku, qty
POST /redeem ────────────► verify X-Sig ─────────────────────────► hold(gross) ~115% pre-auth
{agent_id,sku,qty, (HMAC of loopd secret) │ 402 if balance/cap exceeded
ref,ts,nonce,secret} nonce replay guard ▼
provision() ◄── HOOK POINT settle(service, round5)
(v1: instant stub) │ doof dust → Church ledger
│ ▼
├─────────────────────────────────► transfer service value
│ (+release sweep) → qalredeem-ops
├─ fee>0 ─────────────────────────► transfer redemption fee
│ → qalredeem-fee (fee ledger)
│ record → data/redemptions.jsonl (append-only)
│ record → qalpipe anchor (sha256 on-chain, DataAnchor object)
▼
200 {redemption_id, receipt_id, anchored_tx, fulfilled:true, split…}
Every redemption is recorded in data/redemptions.jsonl, written as a per-redemption JSON file under data/anchors/<redemption_id>.json, and anchored on the qalnet devnet chain via qalpipe — the anchored hash is sha256(sku|qty|cents|receipts|ts). Verify any record any time:
python3 qalpipe/qalpipe.py verify qalredeem/data/anchors/rdm_XXXXXXXXXX.json # → PASS
2. Where every cent goes (fee + dust accounting)
For gross = qty × price_cents_per_unit:
| Slice | Formula | Lands in | Proof trail |
|---|---|---|---|
| Service value | settled = gross − fee, rounded down to 5c |
qalredeem-ops agent balance (qalarc operating revenue — Reserve → Operations in the backing design) |
loopd settle receipt (kind service_redemption) + ops transfer receipt |
| Redemption fee | fee = ⌊gross × fee_bps / 10000⌋ (per-SKU, default 0) |
qalredeem-fee agent balance (the redemption-fee ledger) |
loopd transfer receipt, memo redemption fee <sku> (<bps>bps) |
| Church dust | dust = (gross − fee) mod 5 |
Church of the Doof NFP via loopd's doof-rounding (loopd/data/nfp.json) |
dust_cents_to_nfp on the settle receipt |
Identity (the whole model in one line): service_settled + fee + dust == gross, enforced in code before a record is written.
Worked example — priority-consult-hour (1003c, fee_bps 50), qty 1:
gross 1003c ─┬─ service 995c (998 rounded down: 3c dust → Church) → ops agent
├─ fee 5c (⌊1003×50/10000⌋) → fee ledger
└─ dust 3c (doof rounding, the Giving) → Church of the Doof
customer debited exactly 1000c; every cent of the 1003 accounted.
Balance-debit exactness: loopd (paper) credits the hold release back to the customer's balance, so qalredeem sweeps the release into the ops transfer — net customer debit is exactly gross − dust. In the Move phase, holds debit balance at hold-time and the sweep disappears.
3. Policy (services-only, machine-readable)
GET /policy returns the standing policy for auditors and apps:
- services_only: true — QALS credit is acquired for AUD and consumed by qalarc services. It is interchangeable inside the ecosystem, never converted back to fiat in Phase 1–2 (closed-loop prepaid carve-out per the AU Token Law Playbook; VASP/licensed phase may revisit).
- redemption_fee — the standing terminology for any fee on redemption paths (never "withdrawal fee", never "cash-out fee"). Per-SKU
fee_bps, default 0, capped at 5000. Demo SKU:priority-consult-hour(50bps). - doof_rounding — settle rounds to the 5c; doof dust is donated to the Church of the Doof (doof.ing NFP) by default, opt-out. Complete audit trail via loopd's nfp ledger + on-chain records.
- refusal — redeeming beyond balance fails at the loopd hold → HTTP 402 with balance/cap/required info. No credit, no overdraft, no cash conversion.
- units — loopd counts AUD cents; 1 QALS = 100c = 1,000,000 doofs; 1 cent = 10,000 doofs.
4. API
| Route | Auth | What |
|---|---|---|
GET /health |
– | service + loopd + catalog status |
GET /catalog |
– | active catalog (seeded + admin-edited) |
GET /policy |
– | standing policy JSON |
GET /report |
– | {redemptions_today, cents_redeemed, service_settled_cents, fees_collected_cents, dust_to_church_cents, by_sku} |
GET /redemptions?agent_id= |
– | ledger query (last 100), or GET /redemption/<id> |
POST /catalog |
admin X-Sig = HMAC(admin_key, raw body) |
upsert {sku:{…}} or {skus:[…]} |
POST /redeem |
customer X-Sig = HMAC(loopd_secret, canonical payload) |
the redemption flow |
Admin key: env QALREDEEM_ADMIN_KEY wins; otherwise generated once, stored data/admin_key.txt (0600), printed once. CLI: python3 qalredeem/qalredeem.py add-sku <sku> <name> <kind> <price_c> [fee_bps] [desc].
POST /redeem contract (paper phase): body {agent_id, sku, qty, ref, ts, nonce, secret} where secret is the customer's 64-hex loopd api secret, and X-Sig = HMAC-SHA256(loopd_secret, canonical_json({agent_id,sku,qty,ref,ts,nonce})) with canonical_json = json.dumps(payload, sort_keys=True, separators=(",",":")). Verifying the signature proves the caller owns the very loopd credential qalredeem will spend against; ts must be within ±5 min and nonce single-use. On localhost (loopd's own paper model, no TLS) the secret-in-body is equivalent to session auth — upgrade path: delegated session keys / RFC 9421 once loopd grows signature verification.
CLI: serve · catalog · policy · report · redemptions [agent_id] · add-sku … · redeem <agent_id> <sku> <qty> <secret|'-' for $QALREDEEM_SECRET> [ref] [nonce] · admin-key [--show]
5. App integration (wallet "Redeem" screen)
GET /catalog→ render cards (name,price_cents_per_unit, kind icon, fee badge iffee_bps>0). Prices are cents; display as0.02 Qals(= 2c).- User picks sku + qty → show the split preview: value / fee / (likely 0 dust).
POST /redeemwith the device's loopd agent credentials (qalchat handles / seedkit-bound agents work as-is) → spinner ("provisioning…").- On
200: show receipt + anchored tx link (anchored_tx, object on qalnet explorer). On402: "insufficient balance — top up first" (top-up = the credit-sale path,/topupat loopd → Reserve mint when live).
6. Upgrade path
| Now (paper) | Next | Later (Phase 2+) |
|---|---|---|
provision() instant stub |
real hooks per kind: llm token grants, GPU scheduler seconds, render queue, storage quota, consult booking — fail before settle, hold auto-expires | metered fulfilment with ComputeReceipts (07_compute_marketplace/) |
| loopd ledger = the "chain" | records map 1:1 to CreditAccount/Hold/DataAnchor objects (loopd is the genesis importer) | on-chain burn: redemption burns B-QALS in qal_reserve (reserve.mint_against_aud counterpart), Move-enforced |
| sha256 anchor via qalpipe on devnet | same pipeline, mainnet anchor objects | dispute recompute: expected output hash anchored with the redemption |
| secret-in-body localhost auth | delegated session keys | RFC 9421 / DID-bound signatures |
| redemption fee → fee agent | fees → FloorVault per backing design §3 | licensed-phase cash option: AU$1 − 0.5% redemption fee (terminology already correct) |
7. Operations
# qalredeem restart
pkill -f qalredeem.py; cd qalredeem && setsid nohup python3 qalredeem.py serve > /tmp/qalredeem.log 2>&1 &
# loopd restart (if :8823 down)
pkill -f loopd.py; cd loopd && nohup python3 loopd.py &
# chain (:9000) — see scripts/gen_devnet.sh; qalpipe needs qalpipe/pkg_id.txt
# (restored 2026-09-09 to 0xe3953c8e…74873 after a devnet regen left it blank)
Data (all under qalredeem/data/): catalog.json (seeded, admin-editable) · redemptions.jsonl (append-only) · anchors/<id>.json (anchored records) · keys.json (qalredeem's own ops/fee loopd agents) · admin_key.txt.
Related: loopd/loopd.py (payment loop) · qalpipe/qalpipe.py (anchoring) · 03_qals_architecture/QALS_BACKING_DESIGN.md · content/token_supply.md. Not financial advice.