Platform points → claim link → wallet credit, single-use and anchored · content/guides/claims-and-points.md
Claims & Points — turn platform points into wallet credit
Earn points on any participating site, pull them into your wallet as spendable credit. The full loop, verified.
✓ verified 2026-09-12 — qalclaim/test_qalclaim.sh ALL 28 ASSERTIONS GREEN against live loopd + chain: issuer registers → funds escrow → issues 500 points → mints claim link → user signs in (challenge + proof) → claims → credit lands exactly (+500¢) → anchored → replay/expiry/wrong-handle/forgery all refused. qalclaim :8836 live.
What you need
- A wallet identity with its loopd handle (getting-started).
- A claim link from a participating platform:
qalclaim://claim?t=<payload>.<sig>— delivered as QR, in-app, or email. - Nothing else. No account on the platform beyond the one that awarded the points.
The flow in one picture
platform "acme-app" qalclaim :8836 your wallet
────────────────── ────────────── ───────────
registers as issuer (admin-gated) → issuer_id + HMAC key
funds an escrow agent → the ONLY money claims ever pay from
awards points to your handle ────────────────────────────────► 500 points, pending
mints the claim link ──────────► single-use, handle-bound, 7-day expiry
1. sign in: challenge
2. /whoami: "you have 500"
3. claim → escrow pays YOU
credit lands EXACTLY (+500¢)
receipt anchored on-chain
One point = 1 AU¢ = 0.01 Qals (configurable per platform). 500 points → 5.00 Qals of service credit.
Steps (the user side)
1. See what's waiting
The wallet's sign-in runs challenge → proof → whoami in one CLI line:
python3 qalclaim/qalclaim.py whoami <handle> <agent_id> <loopd_secret>
# → pending claims listed with their campaigns
2. Claim the link
python3 qalclaim/qalclaim.py claim <handle> <agent_id> <loopd_secret> "qalclaim://claim?t=…"
What success looks like: a receipt in your hand —
{"receipt_id": "…", "cents": 500, "transfer": "r_…", "anchor": "…"}
balance shows the extra 500¢; the claim record's sha256 is anchored on Qalnet (tamper-evident forever — anyone can re-verify).
3. Spend it like any credit
Chat payments, compute, locked content, redemption desk — claimed points are ordinary credit (F14: credit stays credit; spending retires it).
Steps (the platform side — 3 lines of curl)
# 1. award 500 points to alice (HMAC-signed; 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 as QR / in-app / email)
curl -X POST http://127.0.0.1:8836/issue/$ISSUE_ID/link -d '{}'
# 3. fund escrow when it runs low
curl -X POST http://127.0.0.1:8836/issuers/$ISS_ID/fund -d '{"issuer":"'$ISS_ID'","cents":100000,"ts":'$TS',"sig":"'$SIG'"}'
Points cannot pay out beyond funded escrow (402 — nothing moves), so points are never promises.
Why a stolen link is worthless
| Attack | What happens |
|---|---|
| Link shared/leaked | Handle-bound: only the awarded handle can claim — bob's proof on alice's token → 403 |
| Token reused | Single-use registry → 409 carrying the original receipt id (the thief learns nothing useful) |
| Token stale | 7-day expiry → 410 |
| Token tampered | Server HMAC signature breaks → 401 FORGED |
| Impersonating a handle | Fresh single-use challenge signed with the handle's loopd secret; the wallet never sends its secret to qalclaim |
| Brute force | 5 failures/hour → 15-minute lockout (per-handle, persists) |
| History rewritten | Every claim anchored; python3 qalpipe/qalpipe.py verify <receipt file> → PASS |
Honest scope (from the qalclaim README): proof-of-handle v1 proves this device owns this payment handle, not this human is who they say — human-grade login is the passkey/WebAuthn upgrade, and the swap changes nothing else in the flow (digital-identity-login is that story).
Troubleshooting
| Symptom | Fix |
|---|---|
| 403 on claim | You're claiming from a different handle than the link was issued to — links are handle-bound on purpose. |
| 409 with someone else's receipt id | Already claimed (single-use). The receipt id proves it paid out. |
| 410 | Link expired (7 days) — ask the platform to re-mint. |
| 429 / locked out | 5 failed attempts — wait 15 minutes; successful claims clear the count. |
| 402 | The issuer's escrow is empty — that's the platform's problem to fund, not yours; you were never at risk. |
anchored: false in the receipt |
Chain briefly down — the claim still paid; anchors retry via /admin/retry-anchors. |
Companions: qalclaim reference · release & sell playbook · how it works §7.