QALS Wiki · the qalarc network⌂ qalarc.com/projects/qals
generated 2026-09-16 · qalcode autonomous research

Platform points → wallet claims: single-use, handle-bound, anchored · qalclaim/README.md

qalclaim — the points→credit CLAIM system

Created 2026-09-09 · service :8836 · zero-dep (Python stdlib only) · qalclaim.py serve to run, test_qalclaim.sh for the full proof suite.

qalclaim makes the QALS promise real: "earn QALS as points across our platforms, then log in and claim them." Platforms award points to a handle; the user opens their wallet, signs in with their Qal ID, and pulls the points into their loopd account as spendable AU¢ credits — with every claim anchored on-chain.

The flow

┌─────────────┐   POST /issuers          ┌──────────────────────────────┐
│ qalclaim    │←── (admin key)───────────│ platform "acme-app"          │
│             │   gets issuer_id +       │ (any app awarding points)    │
│             │   HMAC issuer_key        └──────────────┬───────────────┘
│             │←── POST /issuers/{id}/fund (HMAC)       │
│             │    → loopd escrow agent funded          │
│             │←── POST /issues (HMAC-signed)           │
│             │    {subject_handle: alice, points: 500} │
│             │←── POST /issue/{id}/link                │
│             │    → qalclaim://claim?t=<payload>.<sig> │
│             │       (this is what the app shows the   │
│             │        user — QR / in-app / email)      │
│             │                                         ▼
│   WALLET (user) ────────────────────────────────────────────────
│  1. GET /challenge                      ← single-use nonce
│  2. POST /whoami {handle, agent_id,     ← "sign in with Qal ID":
│       challenge, sig=HMAC(loopd secret,  discovers pending claims)
│       canonical challenge body)}
│  3. POST /claim {claim_token,           ← the money move:
│       claiming_handle, agent_id,           escrow → wallet (loopd
│       challenge, sig)}                     transfer, exact cents)
│                                             + claim anchored on-chain
└─────────────┐                                 (qalpipe DataAnchor)
              ▼
   loopd :8823 (ledger)          qalnet :9000 (chain anchors)

Claim response carries the receipt: {receipt_id, cents, transfer, anchor} — anchor is the sha256 of issue_id|claimant|ts|receipt_id, verifiable forever via python3 qalpipe/qalpipe.py verify <file>.

Security model (what each attack hits)

Attack Defense
Fake/stolen points issued by impostor Issuer HMAC keys (issued once at registration, admin-gated) + 5-min anti-replay ts window
Points inflated in transit Claim tokens are server-HMAC-signed b64url payloads — any tamper → 401 FORGED
Claim token shared/leaked Handle binding: token's sub must equal claiming_handle → bob's proof on alice's token → 403
Token reused (replay) Single-use registry at issue level + dead-jti set; replay → 409 carrying the ORIGINAL receipt id
Stale tokens 7-day default expiry (exp in payload) → 410
Impersonating a handle Proof-of-handle: claim/whoami require a fresh single-use challenge signed (HMAC-SHA256, canonical JSON) with the handle's loopd secret. qalclaim verifies by replaying a state-preserving authenticated call to loopd (POST /revoke {revoked:false} — no-op for a live agent), so the wallet never sends its loopd secret to qalclaim. Unclaimed/revoked loopd handles are refused outright.
Brute-forcing claims 5 failed attempts per handle per hour → 15-minute lockout (persisted; blocks whoami too; per-handle — other users unaffected). Successful claims clear the count.
Paying from thin air Claims pay ONLY from the issuer's funded loopd escrow agent — underfunded → 402, nothing moves
Concurrent double-claim race Issue is reserved inside the single-use critical section (status: claiming) before the transfer; our own failures un-reserve, replays can't
Silent history rewrite Every claim anchored on-chain (sha256 of issue|claimant|ts|receipt_id); qalpipe verify = tamper detection. Chain-down degrades gracefully (anchored: false) — /admin/retry-anchors re-anchors later.

Integration — 3 lines of curl for any app

# 1. award 500 points to alice (one HMAC-signed call; sign = HMAC(issuer_key, canon(body)))
curl -X POST http://127.0.0.1:8836/issues -d '{"issuer":"'$ISS_ID'","subject_handle":"alice","points":500,"campaign":"signup","note":"welcome bonus","ts":'$TS',"sig":"'$SIG'"}'

# 2. mint the claim link (show this to the user as QR / in-app / email)
curl -X POST http://127.0.0.1:8836/issue/$ISSUE_ID/link -d '{}'
#    → {"claim_token": "qalclaim://claim?t=…"}

# 3. fund escrow when it runs low (HMAC-signed, same canon rule)
curl -X POST http://127.0.0.1:8836/issuers/$ISS_ID/fund -d '{"issuer":"'$ISS_ID'","cents":100000,"ts":'$TS',"sig":"'$SIG'"}'

Canonical serialization everywhere: json.dumps(obj, sort_keys=True, separators=(",", ":")). One point = 1 AU¢ = 0.01 Qals (configurable via data/config.json: cents_per_point).

Wallet-side one-liners (the CLI does challenge→sign→post for you):

python3 qalclaim/qalclaim.py whoami <handle> <agent_id> <loopd_secret>
python3 qalclaim/qalclaim.py claim  <handle> <agent_id> <loopd_secret> "qalclaim://claim?t=…"
python3 qalclaim/qalclaim.py report          # admin totals: issued/claimed/pending/by_campaign

Units

Points convert at cents_per_point (default 1). loopd works in AU¢ (1 B-QALS = 100¢ = AU$1 when backed — see content/token_supply.md). 500 points → 500¢ → 5.00 Qals of service credit.

Honest notes

Files

Path Purpose
qalclaim.py service + CLI (serve, register-issuer, fund, issue, link, whoami, claim, challenge, report, retry-anchors)
test_qalclaim.sh 26-assertion end-to-end proof suite (uses :8837 + data-test/ to leave prod state alone)
data/ (default) admin/config/server key, issuers, issues.jsonl (append-only), claims.jsonl, claim receipt files, rate-limit + nonce state