This file is the single source of truth for what gets built next. It serves exactly one goal: the REVENUE-MODEL USE CASE — a fully functional settlement application, integrable on x402 or similar layers, deployable for ANY state or country. No marketing tasks live here. No item may fake anything: pending obligations stay honestly pending, verify steps assert real behavior, and nothing ships that docs/HONEST_ECONOMICS.md would blush at. The normative target this backlog builds toward is docs/SOVEREIGN_KIT.md — where the two disagree, fix the disagreement in both files before building.
1. Pick the topmost unchecked item. Do not skip ahead; the order encodes dependencies (tests before refactors, adapters before profiles, auth before pilots). Items pre-split into Na/Nb are consumed in order: Na first, checked off on its own. EXCEPTION: items marked ⏭ GATED wait on a founder/partner/legal gate — skip them (do not build) until the gate note is removed; the next non-gated item is yours.
2. Build it fully in one run. Partial work does not get checked off. If an item is genuinely too large for one run, split it in place into Na/Nb sub-items (same priority slot) and complete Na.
3. Verify exactly as the item's VERIFY block says. Minimum bar for every item: npm run typecheck passes, the server boots cleanly (npm run start), and npm run demo completes end-to-end against it. Then run the item-specific assertions. A VERIFY step that needs credentials, live chains, or hardware the run does not have is performed with mocks and the live check is recorded as an open manual-verification note — never skipped silently, never faked.
4. Check it off with date + version, e.g. - [x] … — ✅ 2026-07-03 v0.3.1, and bump the version (patch for hardening, minor for a new capability) in BOTH package.json and the VERSION file — item 1 reconciles them; after that they move together.
5. Append discoveries as new items at the right priority. If the build reveals a bug, a missing seam, or a hardening need, insert it as a new checkbox where it belongs (money-integrity issues go near the top), with its own WHY/WHAT/VERIFY.
6. Standing rules apply to every run: nothing goes public (deploys, repos, posts) without Franky's explicit GO; never pay in anything a named funder didn't deposit; never write a fake tx hash; .env secrets never enter git.
☑ 1. Automated test suite for the core money flows — ✅ 2026-07-02 v0.10.0 (MM4 founder-sessie). Shipped: src/app.ts extracted (index keeps only listen), vitest+supertest, 36 tests across 6 files covering every demo flow (funding gate 402, exact 85/8/4/3 ledger rows, rejected-reopen, double-verdict 409, sybil 409, robot-owner rule, dividend gates/dust/dry-run/idempotent replay, inbox split + withdraw-min + honest-pending, splitAmount exactness incl. 1/7/999_999 micro, paywall fail-closed 503). .env.example already existed (stale WHAT-note); VERSION↔package.json reconciled and scripts/release.ts now bumps BOTH together. Test env pins :memory: DB + no treasury key, so no test can touch a network or fake a settlement.
- WHY: Autonomous routines refactor this rail unattended; a state pilot cannot inherit regressions. The demo proves the flows manually — tests make that proof repeatable and cheap, and every later item's VERIFY leans on it.
- WHAT: Add vitest + supertest dev-deps and npm test. Create test/ covering the flows scripts/demo.ts exercises, against an in-memory/tmp DB (DB_PATH per test): pool funding gate (tasks rejected when pool underfunded → 402), claim/submit/verdict happy path with exact 85/8/4/3 ledger rows, verdict on rejected reopens the task, sybil rejection (identities.person_hash UNIQUE → 409), robot registration refused without ownerHandle, dividend refuses unverified recipients / refuses dust / dry-run math, inbox delivery split + withdraw-min, splitAmount sums exactly to input for adversarial amounts (1, 7, 999_999 micro). Extract the Express app from src/index.ts into src/app.ts (index keeps only listen) so tests can mount it. Housekeeping this run: create the missing .env.example that src/config.ts already tells users to copy, and reconcile the version sources (VERSION says 0.3.0, package.json says 0.1.0 — align package.json to VERSION and keep both bumped together from now on). Files: src/app.ts, src/index.ts, test/*.test.ts, .env.example, package.json.
- VERIFY: npm run typecheck && npm test (all green, ≥15 assertions) && npm run start boots && npm run demo completes; package.json version equals VERSION.
☑ 2. Money-path integrity: transactions, error middleware, funding-tx uniqueness — ✅ 2026-07-02 v0.11.0 (MM4 founder-sessie; NB: MBAir-routines draaien niet meer — alle bouw gebeurt op MM4) Shipped: better-sqlite3-transacties op alle multi-write geldpaden (pool create, task create+reservering, verdict-split incl. vaults/status/reputatie, inbox delivery, inbox withdrawal); JSON-error-middleware (ZodError→400 met issues, malformed JSON→400, SQLITE_CONSTRAINT→409, rest→500 JSON) + wrap() voor async handlers (src/http.ts); funding-tx-uniciteit op pools én dividends (route-checks met vriendelijke 409 "already funded pool #N" + best-effort UNIQUE-indexen als backstop op verse DB's); dividend-replay → 409 (one deposit distributes once; crash-mid-loop blijft hervatbaar omdat de dividends-rij pas ná voltooiing bestaat); claimed_at gewist bij rejection-reopen; kill-switch-gedrag: settle-throw → claim released (compensating delete, alleen zonder tx-hash) → retry betaalt eerlijk; demo/seed genereren verse deposit-hashes per run. Tests: 40 groen (nieuw: moneypath.test.ts met gemockte settle-throw → 500 JSON, nul partiële ledger-rijen, succesvolle retry).
- WHY: Code review findings. A crash mid-verdict currently leaves half-written ledgers; a zod failure returns an HTML 500; the same fundingTx hash can fund unlimited pools — all fatal to the "every cent lands on a public ledger" promise the use case sells to states.
- WHAT: (a) Wrap all multi-write money paths in better-sqlite3 transactions: verdict split (src/routes/marketplace.ts), dividend insert+ledger (src/routes/dividends.ts), inbox delivery (src/routes/inbox.ts), pool create. On-chain settle() stays outside the transaction; DB state records its true outcome. (b) Add a JSON error middleware in src/app.ts mapping ZodError→400 with issue detail, SQLITE_CONSTRAINT→409, else 500 JSON — no HTML errors ever. (c) UNIQUE on pools.funding_tx and dividends.funding_tx (migration in src/db.ts), duplicate → 409 "this deposit already funded pool #N". (d) Clear claimed_at on rejection-reopen (rejection currently nulls claimed_by but leaves claimed_at).
- VERIFY: typecheck + boot + demo; npm test extended with: duplicate fundingTx → 409; malformed body → 400 JSON with error field; kill-switch test that a thrown error inside verdict leaves zero partial ledger rows for that task.
☑ 2b. THE SIMPLE PRODUCT — invisible wallets — ✅ 2026-07-02 v0.8.0 (founder priority 2026-07-02: "fast operational, simple for people")
- WHY: A normal person has no wallet, doesn't understand 0x-addresses, and must only ever see "€3,75 verdiend". The stablecoin is invisible plumbing (founder principle). Without this layer R4M is an engine without a product.
- WHAT: Custodial-lite accounts: users register with email only (users table: email UNIQUE, display_name, created_at; balances = internal ledger rows against the treasury; NO per-user on-chain wallet — on-chain settlement happens only at cash-out). Endpoints: POST /api/users/register {email}, GET /api/users/:id/balance (formatted with the currency export: "€3,75" / "$3.75"), POST /api/users/:id/payout-request {destination} (creates an honest pending payout obligation; on-chain leg uses settle(), bank leg awaits the off-ramp partner). Make workers.payout_address optional when linked to a user account. HARD RULE: no user-facing response string may contain "USDC", "crypto", "wallet", "token", a chain name, or an 0x-address.
- VERIFY: typecheck + boot + demo; curl: email register → balance renders "€0,00"/"$0.00"; payout-request without funds → honest pending; grep all user-facing strings for the forbidden jargon list → zero hits.
☑ 2c. THE SIMPLE PRODUCT — the three-screen consumer app — ✅ 2026-07-02 v0.8.0
- WHY: "Saldo · Verdien · Uitbetalen" — three screens anyone understands. The current app/ PWA is developer-grade (asks for 0x-addresses) and unusable for normal people.
- WHAT: Rebuild app/ as the consumer app: screen 1 Saldo (big €/$ number + plain-language history: "Taak voltooid +€2,13", "Bericht ontvangen +€1,70"); screen 2 Verdien (open tasks in plain language + your paid-inbox address with the one-liner "vreemden en AI betalen om jou te bereiken"); screen 3 Uitbetalen (amount + destination + honest status available/pending). NL strings first, EN second (simple strings file). Keep the developer PWA at /dev.html for the founder. No jargon anywhere.
- VERIFY: typecheck + boot + demo; scripted fetch renders all three screens; jargon-grep zero hits; currency symbol follows config (EURC ⇒ €).
☑ 2d. THE SIMPLE PRODUCT — Inbox402 as the launch use case — ✅ 2026-07-02 v0.8.0 (card-onramp for human senders = follow-up)
- WHY: Fastest self-funding product: "vreemden en AI-agents betalen jou €2 om je te bereiken" — needs no sponsor pool, one-sentence pitch, money-in door already exists (x402). The founder wants ONE product operational fast.
- WHAT: Landing flow in the consumer app: email signup → claim a paid-inbox handle → a shareable "betaal om mij te bereiken" page for senders (plain-language price explanation; x402 path live for agents, card/on-ramp path documented as the next item); received messages readable in the app; earnings flow into the 2b balance.
- VERIFY: typecheck + boot + demo; full curl flow: signup → handle → sender hits paid link → real 402 challenge → (testnet-settled or honest-pending) → message visible → balance increased with € formatting.
• [~] 2e. Passkey login (magic-link login shipped v0.9.0; passkey upgrade pending) (and the seed of R4M Pass) (founder herbevestigd 3/7 nacht: "eenmaal geïdentificeerd moet je niet meer komen aandraven met een of ander e-mailadres" — de keten eID-éénmalig → person_hash → passkey maakt e-mail optioneel: alleen nog meldingskanaal/herstel, nooit je identiteit. Meenemen zodra de launch-track het toelaat.)
- WHY: Two birds: (a) consumer UX — normal people log in with fingerprint/face, no passwords, no wallets; (b) the R4M Pass wedge (USE_CASE_LEDGER #3): verify once via national eID, return forever via passkey at near-zero marginal cost — the architecture that undercuts per-authentication eID fees for the legally over-served login market.
- WHAT: WebAuthn/passkey registration + login for the 2b user accounts (@simplewebauthn/server or equivalent): POST /api/users/:id/passkey/register, POST /api/users/login/passkey. Sessions via httpOnly cookie. Wire into the 2c consumer app ("Inloggen met vingerafdruk"). The identity chain becomes: eID root (once, roadmap 11a) → person_hash (uniqueness) → passkey (returning-user auth, free). No user-facing jargon.
- VERIFY: typecheck + boot + demo; scripted WebAuthn flow with a software authenticator in tests: register passkey → logout → login via passkey → balance visible; sessions expire; jargon-grep still zero hits.
- UITBREIDING (founder 07-07): step-up-bevestiging vóór geld vertrekt. Een uitbetaling/besteed-actie vereist een verse passkey-bevestiging (WebAuthn user-verification) — de toestel-biometrie ontgrendelt lokaal een sleutel, R4M krijgt alleen de ondertekende assertie, nooit de biometrie. Robuust tegen netwerk/toestel-falen: de bevestiging is idempotent aan de bestaande obligation gekoppeld (verbroken verbinding → herbevestigen, nooit dubbel). Eerlijke UI-copy over payout-timing: instant op saldo, 1–2 werkdagen op de bank (of instant-tegen-fee) — nooit 'instant op de rekening' beloven.
☑ 2f. Inbox402 money-integrity & consumer auth — ✅ 2026-07-02 v0.12.0 (MM4 founder-sessie). Shipped: (a) payment-without-delivery guard — unknown inbox 404s BEFORE any 402 challenge (mounted ahead of the payment middleware, works configured and unconfigured); (b) session enforcement on all /me/* consumer routes (requireSelf: 401 zonder sessie, 403 andermans sessie — frontend gebruikte de cookie al); (c) per-inbox price_micro (default $2, floor $2, cap $50) + priceUsd bij registratie + POST /api/inbox/:handle/price (eigenaars-check voor consumer-inboxen) + dynamische per-prijs paymentMiddleware-cache in paywall.ts + prijs uit de inbox in alle consumer-strings; (d) inbox-betalingen tellen mee in x402_sales. Tests: 47 groen (nieuw: consumer.test.ts met echte magic-link-sessieflow + jargon-guard; paywall-404-vóór-betaling; $25-expertprijs-split; floor/cap-afwijzing). Manual verification ✅ 2026-07-02 (v0.13.0, testnet-config met echte CREATOR_ADDRESS): $25-inbox → live 402-challenge met maxAmountRequired exact 25_000_000 micro naar het creator-adres; default $2-inbox → 2_000_000. Dynamische prijzen bevestigd end-to-end.
- (oorspronkelijke WHY/WHAT/VERIFY: research findings 2026-07-02, zie docs/SECUREMAIL_X_INBOX402.md Bijlage B)
- WHY: Three findings block a real consumer product and two of them are money-integrity: (a) PAYMENT WITHOUT DELIVERY — the x402 middleware settles the sender's $2 BEFORE the handler runs; POST /inbox/:handle to a nonexistent handle then 404s: the sender paid and got nothing (violates "never fake success" from the paying side). (b) NO ACCESS CONTROL on consumer endpoints — GET /api/app/me/:id, /me/:id/berichten, POST /me/:id/uitbetalen are open by numeric id; magic-link sessions exist (auth.ts) but consumerApp.ts never enforces them: anyone can read anyone's balance/messages and file payout requests. (c) PRICE NOT USER-SETTABLE — positioning promises "€2 floor, experts €25-50" but INBOX_PRICE_USD is one hardcoded constant and the paywall is statically configured at boot.
- WHAT: (a) Handle-existence guard BEFORE the paid leg: mount a pre-check on POST /inbox/:handle ahead of the x402 middleware that 404s unknown handles before any payment challenge is issued (plus a test that a paid-path 404 can no longer occur after settlement). (b) Session enforcement in consumerApp.ts: all /me/* routes require the session cookie from auth.ts and the session's user_id must equal :id (else 401/403); update app screens to send credentials. (c) price_micro column on inboxes (default 2_000_000, min 2_000_000, max 50_000_000) + per-inbox dynamic price resolution in the paywall path — if x402-express cannot price dynamically per request, split this sub-item out with the architectural note rather than shipping a fake setting; register-endpoint accepts priceUsd, split math uses the inbox's own price. (d) Small accounting fix: record inbox payments in x402_sales so public x402 revenue stats stop undercounting.
- VERIFY: typecheck + boot + demo + full npm test green, extended with: unpaid POST to unknown handle → 404 with NO 402 challenge issued; GET /api/app/me/:id without session → 401, with someone else's session → 403, with own session → 200; register inbox at €25 → x402 challenge (or configured stub) reflects 25_000_000 micro and the 85/8/4/3 split of a delivered message computes on €25; x402_sales gains a row per delivered inbox message.
☑ 2g. OTP-code login — ✅ 2026-07-02 v0.13.0 (founder preference: "inloggen door gewoon een code te ontvangen"). 6-cijferige code per mail naast de magic-link (zelfde rij, 15 min, single-use, max 5 pogingen → 429); POST /api/app/login/code; consumer-app toont code-invoer wanneer er echt gemaild wordt (dev-mode logt direct in); Resend-mail toont de code groot met de link als alternatief. Tests: juiste/foute code, brute-force-lock.
☑ 3. Settlement-adapter abstraction — "the rail never changes, the plugs do" — ✅ 2026-07-02 v0.13.0 (MM4 founder-sessie). Shipped per kit §2.1: src/settlement/adapter.ts (vier-staten SettlementResult, deriveIdempotencyKey met R4M_DEPLOYMENT_ID), attempts.ts (settlement_attempts-rij vóór broadcast; replay beweegt geen geld), adapters/erc20-base.ts (de ene ERC-20-adapter, geparameteriseerd — USDC/EURC zijn configs), registry.ts (SETTLEMENT_ADAPTER env, default usdc-base, onbekend id → weigert te booten). Alle geldpaden (verdict, dividends, inbox-withdraw) settelen via de adapter met deterministische keys; publicApi balance via adapter; /api/public/config publiceert settlement:{adapter,currency,network}; src/x402/payouts.ts = dunne compat-laag voor scripts. Contract gedocumenteerd in docs/SETTLEMENT_ADAPTERS.md. Demo-output ongewijzigd (zelfde pending-reasons); 54 tests groen incl. replay-idempotentie (broadcast exact één keer), failed-broadcast → eerlijk pending → retry settelt.
- WHY: THE state-integration feature of the use case: a country adopts R4M by plugging in ITS settlement rail (EURC, XSGD, JPYC, PayPal, an e-CNY-style sovereign system), not by forking the rail. Done now, while the settlement surface is two files, under the protection of items 1–2. This item implements the kit's normative interface (SOVEREIGN_KIT §2.1) — not a variant of it.
- WHAT: Create src/settlement/adapter.ts with the kit §2.1 types verbatim: SettlementAdapter { id; currency: {code, decimals}; settle(req: SettleRequest): Promise<SettlementResult>; balance(): Promise<bigint>; verifyDeposit(req: DepositCheck): Promise<DepositVerification> }, four-state SettlementResult (settled | pending | rejected | reversed). The existing two-state Settlement in src/x402/payouts.ts maps cleanly and losslessly: {pending:false, tx} → {status:"settled", ref: tx, finality:"probabilistic"}; {pending:true, reason} → {status:"pending", reason, retryable:true, pollAfterMs:60000}. The erc20-base adapter never emits rejected/reversed (chain transfers don't reverse); those states exist in the type for fiat adapters. Move the viem code into src/settlement/adapters/erc20-base.ts as the ONE adapter (parameterized by token address + chain + decimals, so USDC and EURC are configs of the same adapter). verifyDeposit returns {verified:false, reason:"on-chain deposit verification lands in roadmap item 6"} until item 6 — an honest stub, never a fake pass. Idempotency from day one: a settlement_attempts table (idempotency_key UNIQUE, status, ref, created_at) persisted BEFORE broadcasting; replay of a key returns the recorded outcome and moves no money. Callers derive the key deterministically: sha256(deploymentId + ":" + kind + ":" + sourceRef + ":" + beneficiary) (deploymentId from env R4M_DEPLOYMENT_ID, default dev) — this derivation is stable before and after item 4's obligations table, which carries the same key. Add src/settlement/registry.ts selecting by env SETTLEMENT_ADAPTER=usdc-base (default, unchanged behavior). All callers (marketplace, dividends, inbox, publicApi treasury balance) import from the registry; src/x402/payouts.ts becomes a thin re-export or is deleted. Document the contract in docs/SETTLEMENT_ADAPTERS.md, mirroring kit §2.1–§2.2 (never fabricate a tx; return pending on inability; rejected is for needs-human-action only).
- VERIFY: typecheck + boot + full npm test green with zero behavior change; demo output identical (splits, pending reasons); curl localhost:4099/api/public/config now includes settlement: { adapter: "usdc-base", currency: "USDC", network: "base-sepolia" }; new test: calling settle() twice with the same idempotency key against a stubbed adapter records exactly one transfer and returns the identical result both times.
☑ 3b. TASK-ALLOCATION INTEGRITY — "why can't 10 people photograph the same pothole?" — ✅ 2026-07-02 v0.17.0 (MM4 founder-sessie). Shipped: (a) claim-expiry per taak (claim_ttl_minutes, default 60, instelbaar 1-1440 bij aanmaak; sweep bij list/claim/submit — heropent met gewiste claimed_by/claimed_at); (b) evidence-dedup: UNIQUE index op submissions.evidence_hash (best-effort op legacy dev-DB) + route-check met vriendelijke 409 "already submitted (task #N) — the same proof is never paid twice", over ALLE taken; (c) geofence-eerlijkheid: submissions dragen optioneel lat/lon; verdict-respons bevat geo.distanceFromTargetM + taskRadiusM wanneer beide kanten coördinaten hebben (haversine — informatie, geen geveinsde zekerheid); (d) fairness-cap: max K gelijktijdige claims per werker (env WORKER_MAX_CLAIMS, default 3) → 429; (e) multi-witness bevestigd als expliciet product: count>1 creëert N apart gefinancierde instanties (test: 3×$2 exact gereserveerd). Tests: 78 groen.
- WHY: The marketplace's fairness and the sponsor's money depend on allocation rules: one task = one worker (claim-lock exists), but claims never expire (a claimer can squat a task forever), the same evidence file could be submitted to multiple tasks, there is no geofence enforcement (task has lat/lon/radius but nothing checks the submission), and nothing rate-limits one worker hoovering every task in an area. The BALANCE of the whole economy is demand-driven — funded tasks cap total work — but within that cap, allocation must be fair and fraud-resistant.
- WHAT: (a) Claim expiry: claims older than N minutes (task-configurable, default 60) auto-release the task back to open — sweep on read or a tiny interval job. (b) Evidence dedup: UNIQUE index on submissions.evidence_hash — the same photo can never be paid twice, across all tasks. (c) Geofence honesty: submissions carry optional lat/lon; if the task has a geofence, verifier response includes distance-from-target so the human verdict is informed (hard enforcement when attested location lands later — do not fake certainty the data can't give). (d) Per-worker fairness cap: max K concurrent claims per worker (default 3, sponsor-configurable). (e) Multi-witness as a product: sponsor can order count>1 instances of the same observation explicitly — M-of-N corroboration is a premium the sponsor CHOOSES and funds, never accidental duplication.
- VERIFY: typecheck + boot + demo; tests: second claim on expired task succeeds after expiry; duplicate evidence_hash → 409; 4th concurrent claim by same worker → 429/409; sponsor ordering count=3 creates exactly 3 payable instances.
☑ 4. Durable obligations + auto-settler (obligation-first, per kit §2.2) — ✅ 2026-07-02 v0.18.0 (MM4 founder-sessie). Shipped: obligations-tabel (kind/beneficiary/to/amount/source_ref/idempotency_key UNIQUE + UNIQUE(kind,source_ref,to), status pending/settled/rejected/reversed, attempts, last_error); src/settlement/settler.ts met createObligation (rij + _pending-ledgerrij atomisch, IN de geldpad-transactie — obligation-FIRST), trySettleOne (settled → superseding ledgerrij met echte ref, rejected → geparkeerd + _rejected-rij, nooit auto-retry), settleDue (FIFO, balance-check, nooit gedeeltelijk); ALLE drie betaalpaden omgebouwd (verdict, dividends per ontvanger, inbox-withdraw — saldo gaat atomisch óver in de verplichting); settle-crash wordt gevangen: verdict blijft compleet + eerlijk pending (kill-switch-test herschreven naar deze sterkere semantiek); settler draait op boot + interval (SETTLER_INTERVAL_MS, default 60s) + POST /api/admin/settle-now (admin); publiek: GET /api/public/obligations + GET /api/public/solvency (committed vs treasury, deficit nooit verborgen). claimPayout/settled_payouts vervangen door obligations op deze paden. Tests: 82 groen (FIFO betaalt exact wat saldo dekt, replay beweegt niets, solvency-cijfers, settle-now). Live geverifieerd: demo → 23 pending obligations $52,12 publiek zichtbaar; solvency toont eerlijk tekort; werker-uitbetaling settelde direct on-chain.
- WHY: Today "honestly pending" is a ledger note that never resolves — a worker owed money must wait for a human to notice. The use case demands the promise close itself: the moment the treasury is funded, every queued cent moves, oldest first. Obligation-FIRST (row inserted before every settle attempt, not only on failure) is what makes retries and crash-recovery safe by construction — this matches kit §2.2 exactly.
- WHAT: New table obligations in src/db.ts: id, kind CHECK('worker_payout','dividend','inbox_withdrawal'), beneficiary_label, to_address, amount_micro, source_ref (task/dividend/inbox id), idempotency_key UNIQUE, status CHECK('pending','settled','rejected','reversed') DEFAULT 'pending', adapter_ref, attempts, last_error, created_at, settled_at, with UNIQUE(kind, source_ref, to_address). EVERY payout path (marketplace verdict, dividends loop, inbox withdraw) first inserts the obligation row + a _pending ledger row, then calls settle() with the obligation's idempotency key; on settled the row is marked settled and a superseding worker_paid/dividend_paid/inbox_withdrawal ledger row records the real ref — ledger rows are append-only, settlement supersedes, never edits. New src/settlement/settler.ts: settleDue() walks status='pending' rows FIFO, checks adapter.balance() first, pays what the balance covers, marks rows settled with ledger rows carrying the real ref — never partial-pays a single obligation, never invents success; rejected results park the row for human review (ledgered _rejected with reason, never auto-retried). Run on boot, on setInterval (env SETTLER_INTERVAL_MS, default 60s), and via POST /api/admin/settle-now. Expose GET /api/public/obligations (count + total pending USD — the honest debt figure, public by design) and GET /api/public/solvency (kit §2.2.6: publishes obligations + pool remainders + vault balances vs adapter.balance(), both numbers plus the boolean — an underfunded state is legal but never hidden; the deficit equals open obligations exactly).
- VERIFY: typecheck + boot + demo (with no treasury key, demo payouts now create obligation rows: curl localhost:4099/api/public/obligations shows count>0 and matching total; /api/public/solvency shows the honest deficit; demo output text updated to reflect the pending→queued wording, no fake success anywhere); unit tests: (a) seed obligations, stub adapter with balance covering 2 of 3, run settleDue(), assert exactly 2 settled FIFO with ledger rows and the third untouched with last_error null; (b) crash-replay — re-running settleDue() over already-settled rows moves nothing (idempotency).
☑ 5. Role-based API auth: sponsor / verifier / admin keys — ✅ 2026-07-02 v0.14.0 (MM4 founder-sessie). Shipped: api_keys-tabel (sha256-hash, rol, label, sponsor_id, revoked_at); src/authz.ts met requireRole/roleCheck + bootstrap (nul sleutels && !production → allow-all met luide bootwaarschuwing; sleutels bestaan || production || R4M_REQUIRE_AUTH=true → afdwinging); beschermd: POST /pools + /tasks (sponsor eigen pools|admin, met eigenaarschapscheck), verdict (verifier|admin), echt dividend (admin, vóór alle andere checks; dry-run blijft open); worker-routes bewust open. npm run keys -- create|list|revoke (sleutel één keer getoond); demo stuurt Bearer-keys uit env (dotenv). Founder-sleutels aangemaakt (admin #1, verifier #2) → lokale server dwingt nu af; sleutels in .env. BONUS-bugfix door tests gevonden: sendEmail claimde succes zonder Resend-antwoord te checken — nu r.ok (eerlijkheid: nooit "check je mailbox" als er niets vertrok). Tests: 60 groen (authz-matrix: 401 zonder sleutel, 403 verkeerde rol, sponsor-eigenaarschap, revoked key dood, workers open). Live geverifieerd: curl-matrix + volledige demo onder afdwinging. Fly krijgt R4M_REQUIRE_AUTH=true.
- WHY: No real pilot can start while anyone on the network can approve their own submission and trigger a payout, or fire a dividend with dryRun:false. Auth is the precondition for every deployment profile and the starter kit.
- WHAT: New table api_keys (id, key_hash sha256, role CHECK IN ('sponsor','verifier','admin'), label, sponsor_id nullable, created_at, revoked_at). Middleware src/auth.ts: requireRole(...roles) reading Authorization: Bearer r4m_<key>. Protect: POST /api/pools, POST /api/tasks → sponsor(own pools)|admin; POST /api/submissions/:id/verdict → verifier|admin; POST /api/dividends (non-dry-run), POST /api/admin/* → admin. Worker-side routes (register, claim, submit, identity, inbox register/read/withdraw) stay open for now — flag follow-up item if a pilot needs worker auth. Key management: npm run keys -- create --role admin --label ops (script scripts/keys.ts, prints the key once, stores only hash). Bootstrap: if zero keys exist AND NODE_ENV!=='production', auth middleware allows all with a boot warning, so demo/dev keeps working; R4M_REQUIRE_AUTH=true forces enforcement anywhere. Update scripts/demo.ts to create and use keys when enforcement is on.
- VERIFY: typecheck + boot + demo (dev mode, unchanged); then R4M_REQUIRE_AUTH=true npm run start and assert: curl -X POST localhost:4099/api/submissions/1/verdict … → 401; same call with a created verifier key → passes auth; sponsor key cannot call /api/dividends (403). Tests cover the 401/403/200 matrix.
☑ 5b. Spendable balance — "je centen gebruiken in de app" — ✅ 2026-07-02 v0.15.0 (MM4 founder-sessie). Shipped: POST /api/app/me/:id/betaal-bericht (sessie-gebonden; betaalt een bericht aan een andere inbox volledig uit het beschikbare saldo; zelfde 85/8/4/3-split via deliverPaidMessage met internal-vlag → geledgerd als balance_spent + inbox_message_paid_internal, telt NIET mee in x402_sales); atomische transactie (onvoldoende saldo → 400 met nul boekingen; jezelf betalen → 400); UI-kaart "Stuur zelf een betaald bericht" in de inbox-tab. Compliance-gate werkt echt: ALLOW_BALANCE_SPENDING default UIT → 403 met eerlijke reden ("wacht op juridische aflijning") — de vraag staat op de advocaat-lijst. Tests: 64 groen (schakelaar, exacte micro-verplaatsingen afzender −100%/ontvanger +85%, insufficient → nul boekingen, zelf-betaling).
- WHY: Franky: de klant moet alleen de evidentie zien "dat hij de centen kan gebruiken in de app om iets mee te kopen of dat hij het kan overzetten op zijn bankrekening." Vandaag kan een gebruiker alleen verdienen en uitbetalen; besteden houdt geld in het ecosysteem (betaal iemands inbox met je saldo — het Earn.com-model) en maakt het saldo dubbel nuttig. ⚠️ COMPLIANCE-GATE: besteedbaar tegoed schuift richting e-money — expliciete vraag voor de advocaat-scoping-call; bouwen mag, de LIVE-schakelaar wacht op de perimeter-lijn.
- WHAT: (a) POST /api/app/me/:id/betaal-bericht {naarHandle, onderwerp, bericht} — betaalt een bericht aan een andere inbox rechtstreeks uit het eigen beschikbare saldo (interne ledger-overboeking: afzender-saldo −prijs, zelfde 85/8/4/3-split als een externe x402-betaling, ledgered als inbox_message_paid_internal); weigert eerlijk bij onvoldoende saldo. (b) UI: in de inbox-tab "Stuur zelf een betaald bericht" (ontvanger-adres + prijs zichtbaar vóór verzenden). (c) Feature-flag ALLOW_BALANCE_SPENDING (default false) — een flag die iets doet, geen theater: uit = endpoint antwoordt 403 met de eerlijke reden ("wacht op juridische perimeter").
- VERIFY: typecheck + boot + demo + tests: interne betaling verplaatst exact de juiste micro-bedragen (afzender −100%, ontvanger +85%, fees geboekt), onvoldoende saldo → 400 zonder enige boeking (transactie), flag uit → 403, jargon-grep blijft nul.
☑ 6. On-chain deposit verification for pools and dividends — ✅ 2026-07-02 v0.16.0 (MM4 founder-sessie). Shipped: verifyDeposit echt geïmplementeerd op de erc20-base-adapter (checkDepositReceipt, testbaar geïsoleerd): receipt bestaat + status success + Transfer-log van HET geconfigureerde token naar DE treasury ≥ minAtomic + leeftijd ≤ DEPOSIT_MAX_AGE_DAYS (default 30); consumed-check = de bestaande UNIQUE-regels. Poort op POST /api/pools én echte dividenden achter REQUIRE_ONCHAIN_FUNDING=true (default uit → demo ongewijzigd, ledger zegt eerlijk funding:declared; aan → funding:onchain). Faal → 402 met exacte reden. Tests: 73 groen (alle vier faalwijzen + succes + gate-gedrag). LIVE CHECK GEDAAN tegen Base Sepolia (geen open note nodig): echte faucet-storting 0x80b2d7…f621 → pool $15 aanvaard als funding:onchain; zelfde storting voor $25 → 402 "amount short by 5 USDC — this deposit covers only 20 USDC"; verse nep-hash → 402 "tx not found on this network".
- WHY: "Money before promises" is currently enforced by a regex, not the chain — a 64-hex string of ab funds a $100 pool (the demo does exactly this). For a real sponsor or state deposit, R4M must check the money actually arrived. This is the honest-numbers culture, structurally.
- WHAT: Implement verifyDeposit(txRef, minMicro) on the erc20-base adapter (replacing the item-3 honest stub): fetch the receipt via publicClient, assert success, and that it contains a Transfer log of the configured token to the treasury address with value ≥ minMicro; also assert the tx hash isn't older than env DEPOSIT_MAX_AGE_DAYS (default 30) and hasn't been consumed before (the item-2 UNIQUE constraints are the consumed-references table). Gate POST /api/pools and POST /api/dividends behind it when env REQUIRE_ONCHAIN_FUNDING=true (default false so local demo/seed still run; deployment profiles for real pilots set it true). Failure → 402 with the honest reason ("tx not found / wrong token / wrong recipient / amount short by $X"). Ledger pool_funded rows gain verified: onchain|declared in detail.
- VERIFY: typecheck + boot + demo (flag off, unchanged); unit tests mock the public client for all four failure modes AND the success mode — these are the required assertions of this item. Live check against Base Sepolia (a real faucet-funded deposit creates a pool; the demo's 0xabab… hash → 402 with reason) is performed only if a funded testnet treasury is available to the run; otherwise it is recorded in this file as an open manual-verification note — an autonomous run must not fabricate a live-chain result.
☐ 5c. ⏭ GATED — Echte besteedbaarheid — "mensen moeten er direct iets mee kunnen doen" (founder-eis 2026-07-02 avond) (gate: partnerkeuze giftcard-API is een founder-beslissing met echte tarieven — routines slaan dit over; de beslisnotitie voorbereiden mag wél als apart klein werk)
- WHY: Franky: "als ze money collectioneren moeten ze dit ook direct kunnen gebruiken en zich iets kopen ermee. Zoiets moet vanzelfsprekend zijn. Alleen op die manier zal ik dit kunnen overeind houden." Verdiend saldo dat nergens heen kan is een dood product. Naast 5b (in-app besteden, juridische schakelaar) en de bank-route (Stripe) is er een derde, compliance-LICHTE route die panel-platformen wereldwijd al gebruiken: cadeaubonnen/telefoonkrediet als uitbetaling (bol.com/Amazon/supermarkt-bonnen, mobiel tegoed) — geen e-money-vraagstuk (het platform kóópt een bon voor de gebruiker), direct tastbaar, en precies wat onderzoeksbureaus als vergoeding gewoon zijn.
- WHAT: (a) Verken en kies één giftcard-API-partner met EU-dekking en per-stuk-inkoop (kandidaten: Tillo, Reloadly, Runa/ex-WeGift, Amazon Incentives — beslissingsnotitie met echte tarieven/minima vóór er gebouwd wordt; wie-betaalt-filter: de marge zit in de bon-korting, nooit in de gebruikersvergoeding); (b) uitbetaal-scherm krijgt naast IBAN een tweede optie "Kies een bon" (zelfde obligation-first-model: obligation kind 'voucher_payout' → partner-API → code per mail via Resend → settled met bon-referentie); (c) feature-flag tot de partner getekend is; (d) jargon-/eerlijkheidsregels: bon pas "geleverd" tonen als de code er ECHT is.
- VERIFY: typecheck + boot + demo + tests: voucher-obligation volgt het volledige pending→settled-pad met gemockte partner; mislukte partner-call → eerlijk pending, nooit een fantoom-bon; e-mail met code alleen bij r.ok; flag uit → 403 met reden.
☐ 5d. ⏭ GATED — Overheid betalen met je saldo — boetes, retributies, parkeergeld (founder-idee 2026-07-02 avond) (gate: PSD2-perimeter staat op de advocaat-lijst — géén code vóór de juridische route vastligt)
- WHY: "Misschien handig als mensen hiermee een stuk van hun autoboetes kunnen betalen." Strategisch goud voor het overheidsverhaal: de rail wordt TWEERICHTINGSVERKEER — de staat betaalt burgers (dividenden, panels) én burgers betalen de staat (boetes met gestructureerde mededeling, retributies, parkeer) over dezelfde controleerbare rail. Voor de burger: verdiensten direct nuttig zonder bank-omweg. Voor de stad: snellere inning, minder wanbetaling.
- WHAT: VERKENNING eerst, bouw daarna: (a) juridische route — betaling aan een derde (overheids-IBAN) namens de gebruiker = betalingsdienst (PSD2) → kán alleen via de vergunde partner (Stripe/PSP die derden-IBAN met gestructureerde mededeling ondersteunt, of een overheids-betaalkoppeling); expliciete vraag op de advocaat-lijst. (b) Product: uitbetaal-scherm krijgt derde optie "Betaal een boete/factuur van de overheid" (gestructureerde mededeling + bedrag; saldo → obligation kind 'gov_payment' → partner betaalt → bewijs in de app). (c) Zelfde eerlijkheid: pas "betaald" bij echte bevestiging; feature-flag tot partner + perimeter er zijn. (d) Pitch-kant: opnemen in het 16a-portaal-verhaal ("uw stad int én betaalt over dezelfde rail").
- VERIFY: beslisdocument met de juridische route + partnerkeuze vóór één regel code; daarna: gemockte partner-tests op het volledige pending→betaald-pad, gestructureerde-mededeling-validatie (BE-formaat +++xxx/xxxx/xxxxx+++), flag-uit → 403 met reden.
☑ 7. Honest-math v2: settled vs pending, never conflated — ✅ 2026-07-02 v0.19.0 (MM4 founder-sessie). Shipped: /api/public/honest-math rekent nu uit ledger + obligations — settledToWorkersUsd (alleen superseding worker_paid-rijen, 85%-werkersdeel i.p.v. taakprijs), pendingToWorkersUsd (open obligations), avgWorkerEarningsPerMonthUsd uitsluitend uit settled (pending kan het gemiddelde nooit opblazen), dividends/inbox elk met {settledUsd, pendingUsd, reversedUsd}, reversedFromWorkersUsd (fiat-terugboekingen tellen tegen settled en staan apart vermeld, kit §2.2.5); grounded-ceilings-context ongewijzigd. Demo stap 13 toont settled vs owed-queued; dev-app honest-tab kreeg een "paid vs owed, queued"-samenvattingsregel. LIVE geverifieerd per VERIFY: demo zonder treasury-key → settledToWorkersUsd: 0, pendingToWorkersUsd: 2.125, dividends pending $50, avg 0. Tests: 85 groen (nieuw: honestmath.test.ts — pending nooit als betaald, settle → bedrag verhuist naar settled, reversal verdwijnt nooit stilletjes).
- WHY: /api/public/honest-math currently counts approved-task gross as "paid to workers" even when settlement is pending, and reports task price (100%) rather than the worker's 85% — the one endpoint whose whole job is to never overstate, overstates.
- WHAT: Rework src/routes/publicApi.ts honest-math to compute from the ledger + obligations: settledToWorkersUsd (settled worker rows only), pendingToWorkersUsd (open obligations), avgWorkerEarningsPerMonthUsd from settled only, plus dividends: {settled, pending} and inbox: {settled, pending}. Reversals (fiat rails, kit §2.2.5) count against gross — never hidden from the averages. Keep the grounded-ceilings context block verbatim. Update app/ honest-math view labels to show both numbers ("paid" vs "owed, queued").
- VERIFY: typecheck + boot; run demo without treasury key, then curl /api/public/honest-math must show settledToWorkersUsd: 0 and pendingToWorkersUsd > 0 (today it would claim earnings); test asserts pending never inflates the average.
☑ 8. EURC adapter — the first proof that the plug model works — ✅ 2026-07-02 v0.20.0 (MM4 founder-sessie). Shipped: eurc-base in de registry als tweede instantiatie van dezelfde erc20-base-adapter (nul nieuwe adapter-code); EURC_ADDRESS env met OFFICIËLE Circle-defaults per netwerk (Base Sepolia 0x8084…359F, mainnet 0x60a3…db42 — geverifieerd tegen developers.circle.com contract-registry), lege env-waarde = default; fail-closed guard: eurc-base met verkeerd EURC-adres weigert te booten; currency-export volgt de plug (eurc-base ⇒ €/EUR, ook op testnet); banner toont "Settlement: <adapter> — pays out in <munt>"; hardcoded valuta-labels weg (marketplace-402 zegt nu adapter-munt, inbox-prijzen volgen currency.symbol, dev-app-placeholder neutraal); .env.example documenteert SETTLEMENT_ADAPTER + EURC_ADDRESS. LIVE geverifieerd per VERIFY: boot met SETTLEMENT_ADAPTER=eurc-base → banner + /api/public/config = {adapter:eurc-base, currency:EURC}, inbox-registratie zegt "€2 per message". Tests: 89 groen onder BEIDE adapters (npm test én SETTLEMENT_ADAPTER=eurc-base npm test; suite geparameteriseerd, nieuw: eurc.test.ts).
- WHY: One adapter is an architecture claim; two is a fact. EURC on Base is the EU/Belgium home-market currency (MiCA-aligned, Circle-issued) — the exact rail the first pilot city would ask for, and it exercises the item-3 interface with near-zero new code.
- WHAT: Register eurc-base in src/settlement/registry.ts as a second instantiation of the erc20-base adapter (env EURC_ADDRESS, Base Sepolia + mainnet defaults documented in .env.example from item 1), selectable via SETTLEMENT_ADAPTER=eurc-base. Currency label flows through /api/public/config, ledger detail, and app UI (no hardcoded "USDC" strings left — grep and fix). Amounts stay 6-decimal micro-units.
- VERIFY: typecheck; boot with SETTLEMENT_ADAPTER=eurc-base → banner and /api/public/config report EURC; full test suite green under both adapters (parameterize the suite over adapter id with a stubbed chain).
<!-- FOUNDER-BOUWRIJ (bijgewerkt 2026-07-03 nacht): eerst de LAUNCH-TRACK — founder: "de basisversie moet in dágen online, dag en nacht aan werken" — dus 19 (beveiliging/ops) → 24 (launch-pakket); daarna de klantenlijn 16a → 16 (verifier-UI) → 16b → 16c → 22. Blokken fysiek in deze volgorde zodat 'pak het bovenste onafgevinkte item' de founder-volgorde volgt. -->
<!-- Item 19 is te groot voor één run (8 losse operabiliteits-brokken + een volledige backup/off-site/restore-laag). Gesplitst 2026-07-03 in 19a (runtime hardening — security & beschikbaarheid, de founder-launcheis) en 19b (durable + off-site encrypted backups + restore-drill). 19a eerst. -->
☑ 19a. 🚀 LAUNCH-POORT deel 1 — Runtime hardening — ✅ 2026-07-03 v0.22.0 (MM4 build-sprint, ultracode + adversarial-verify). Shipped: GET /healthz (db-ping + adapter-metadata + pending-obligation-count + version; netwerkvrij standaard, ?deep=1 probet de chain-balance met 3s-timeout); structured JSON request-logging (src/middleware/logging.ts — enkel method/path/status/ms, NOOIT body/query/IP; money-paden getagd money:true; stil onder VITEST; volledige /api/...-paden via req.originalUrl zodat de mount-prefix niet verdwijnt); express-rate-limit-fabriek (src/middleware/rateLimit.ts — 100/IP/min default, RATE_LIMIT_-tunable, RATE_LIMIT_DISABLED voor de testsuite) gemonteerd op worker/inbox-register, login, claim, submit én inbox-inbound (vóór de paywall); graceful shutdown (SIGTERM/SIGINT → stop, settler-interval gestopt, in-flight settle afgewacht vóór DB-close zodat een deploy nooit dubbel betaalt, wal_checkpoint(TRUNCATE), close); non-CSP security-headers (src/middleware/security.ts — nosniff/DENY/no-referrer/HSTS/…, x-powered-by weg; bewust GEEN restrictieve CSP want app/index.html heeft inline scripts); egress-poort DISABLE_OUTBOUND_WEBHOOKS + SSRF-guard (src/net/ssrf.ts) op register én delivery, met redirect:"manual". Adversarial-verify (4 skeptic-agents) vond en ik fixte: IPv4-mapped-IPv6 metadata-bypass (hex-vorm ::ffff:a9fe:a9fe), fe80::/10-gat (fe81–fe8f), redirect-based SSRF, en een shutdown-dubbelbetaling (→ nieuw money-item 4b voor de resterende harde-crash-window). WHATWG-URL normaliseert hex/octaal/integer-IPv4 → de guard vangt ze via u.hostname. Live geverifieerd: mapped-hex/fe85/integer-IP/localhost/ftp → 400, resolvebaar publiek → 200, SIGTERM drained + geen -wal, demo volledige lus, headers op elke response. Tests: 113 groen (nieuw: ssrf/hardening/ratelimit/webhook-egress). (oorspronkelijke WHY/WHAT/VERIFY hieronder)*
- WHY (orig.): (naar voren gehaald 2026-07-03: founder wil de basisversie in dágen online — beveiliging en operabiliteit gaan vóór echte gebruikers; "de website met de back-end en het inloggen moet echt secure en veilig zijn")
- WHY: A 90-day state evaluation and a first sponsor pilot both die on the first unexplained outage or the first abusive script. Boring operability is part of "fully functional application." The Inbox402 notify_webhook fetch is also the codebase's one operator-uncontrolled outbound flow (and an SSRF surface: 169.254.169.254 = cloud metadata) — the kit's egress claim (A12) needs a switch to be true. Privacy-by-design: request logs must carry NO body, NO query string (magic-link tokens live there), and NO raw client IP by default.
- WHAT: GET /healthz (db ping, adapter metadata, pending-obligation count, version — fast & network-free by default; ?deep=1 probes adapter balance with a timeout); structured JSON request logging (tiny middleware, no heavy dep — method/path/status/ms only, never body/query/IP; silent under VITEST); express-rate-limit on unauthenticated POSTs (worker/inbox/consumer register, login, claim, submit, inbox inbound) with profile-tunable limits (RATE_LIMIT_WINDOW_MS, RATE_LIMIT_MAX, RATE_LIMIT_DISABLED) — generous enough that the demo never trips it; graceful shutdown (SIGTERM/SIGINT → stop accepting, clear settler interval, wal_checkpoint(TRUNCATE), close db); non-CSP security headers (X-Content-Type-Options, X-Frame-Options, Referrer-Policy, Strict-Transport-Security, X-Permitted-Cross-Domain-Policies, drop X-Powered-By) — NO restrictive script-src CSP because app/index.html uses inline scripts; env DISABLE_OUTBOUND_WEBHOOKS=true to switch off inbox notify_webhook delivery entirely (sovereign profiles set it), plus an SSRF guard (src/net/ssrf.ts: reject non-http(s), reject IP-literals in private/reserved/link-local ranges incl. 169.254.0.0/16, reject localhost/.local/.internal; optional DNS-resolution anti-rebinding layer at registration behind SSRF_RESOLVE_DNS, off in tests) applied at BOTH inbox registration and delivery.
- VERIFY: typecheck + boot; curl /healthz → 200 with all checks; hammer an open endpoint past the limit → 429 appears (dedicated test with a low-max limiter instance); kill -TERM boots cleanly next start (WAL checkpointed, no -wal growth — manual check against a file DB); with DISABLE_OUTBOUND_WEBHOOKS=true a delivered inbox message with a webhook triggers no outbound fetch (test asserts fetch not called); webhook registration with http://169.254.169.254/... → 400, http://localhost/x → 400, ftp://… → 400; security headers present on a response; request log line carries no body/query/IP; full demo + test suite still green (89+).
☑ 19b. 🚀 LAUNCH-POORT deel 2 — Durable + off-site encrypted backups + restore-drill — ✅ 2026-07-04 v0.26.0 (MM4 build-sprint, ultracode + 4-lens adversarial-verify). Shipped: src/backup/ (crypto.ts = AES-256-GCM + scrypt-KDF, self-describing container [MAGIC|salt|iv|tag|ct], fail-closed decrypt; s3.ts = dependency-free SigV4 path-style PUT, off-site ships the ENCRYPTED blob ONLY; backup.ts = VACUUM INTO consistent online snapshot → integrity-verify → encrypt-at-rest → retention-prune → optional off-site). npm run backup + npm run restore -- <file> [--verify|--to|--force]. Honesty rules enforced: S3 configured zonder key → hard refuse (nooit plaintext off-site); mislukte upload → eerlijk off-site FAILED + non-zero exit; restore verifieert integriteit + ledger-tabel VÓÓR overschrijven en weigert clobber zonder --force. Adversarial-verify (4 skeptic-lenzen) vond 5 echte defecten, allemaal gedicht: (1) plaintext-snapshot verweesd als de encrypt-write faalt → try/finally garandeert verwijdering; (2)+(3) plaintext-window + backups world-readable → mkdir 0700 / files 0600; (4) temp-bestand PID-collisie (containers als PID 1 op gedeeld volume) → randomUUID; (5) BACKUP_S3_PREFIX met .. ontsnapt de prefix → fail-closed traversal-guard. (De skeptic stelde óók een WAL-sidecar-swap voor die corruptie zou introduceren — bewust NIET toegepast, veiligere volgorde behouden + rationale gedocumenteerd.) Docs: docs/BACKUP_AND_RESTORE.md (procedure + R2/B2-config + drill-log) + README. LIVE restore-drill uitgevoerd + vastgelegd: live 576 ledger / 363 obligations → encrypted backup (640 KiB, -rw-------) → verify integrity=ok → restore → byte-consistent 576/363. Tests: 157 groen (nieuw test/backup.test.ts, 19 asserts incl. SigV4 tegen AWS-referentievector, encrypt→decrypt byte-identiek, wrong-key/tamper fail-closed, S3-no-key refuse, traversal-reject, owner-only perms). OPEN — handmatige verificatie: de LIVE off-site PUT is niet uitgevoerd (geen R2/B2-credentials in de run); zodra een private bucket + sleutels bestaan: npm run backup → object verschijnt → npm run restore -- <blob> --verify slaagt.
- WHY: Founder-eis 3/7: "data die niet mag verloren gaan." A backup on the same machine/VM is not a backup. A first pilot's ledger is the audit trail — losing it is losing the whole promise.
- WHAT: scripts/backup.ts (npm run backup) doing VACUUM INTO a timestamped file under backups/ + retention (keep last N, env-tunable); an AES-256-GCM-encrypted copy (key from .env, never in git) uploadable to a private off-site bucket (R2/B2, S3-compatible) — the encryption + local restore path is fully built & tested; the live off-site upload is gated on credentials the run may not have (record an open manual-verification note, never fake it); a restore note + npm run restore/documented procedure in README; one real restore-drill executed and recorded.
- VERIFY: typecheck + boot; npm run backup produces a file that opens in sqlite3 and contains the ledger; encrypt→decrypt round-trips to a byte-identical DB that opens and contains the ledger (test); missing key → honest refusal, never a plaintext off-site copy; restore-drill documented + run once; full demo + test suite still green.
- 19d. Backup-planner MM4-onafhankelijk maken (founder-vraag 10-07: "moet mijn Mac aan voor de backups / draait R4M 24/7 los van mijn computer?"). VASTSTELLING: R4M zelf draait 24/7 op Fly, volledig los van MM4 (geen enkele executie-afhankelijkheid). MAAR de nachtelijke off-site backup hangt nu aan de MM4-launchd-job com.r4m.daily-backup (04u40): staat MM4 uit / 8TB niet gemount → die nacht geen R2-kopie (live data blijft veilig op Fly, enkel de extra kopie gemist). WHAT: de daily-backup verhuizen naar een cloud-cron die 24/7 draait ongeacht MM4 — opties: (a) Fly scheduled machine / Fly cron die backup.ts in het prod-image draait en naar R2 pusht (BACKUP_*-secrets staan al op Fly); (b) Cloudflare Worker cron-trigger die de backup-flow aanstuurt; (c) GitHub Action met secrets. Kies (a) als eerste — zelfde image, geen nieuwe infra. De MM4-job mag blijven als tweede, redundante kopie (belt-and-suspenders). GATE: niet dringend in de testfase; aanraden vóór echte productie/echt geld (dan mag geen enkele backup-nacht van een laptop afhangen). FOUNDER-COMMIT 10-07: "gaan we zeker doen, maar niet nu" — vast ingepland en prioritair zodra de launch-poort / echt geld nadert; niet laten wegzakken. VERIFY: cloud-cron draait een nacht zonder dat MM4 aan staat → versleuteld object verschijnt in R2 → restore --verify slaagt.
☐ 19c. 🚀 LAUNCH-POORT deel 3 — Externe pentest + verantwoorde onthulling (founder-stelling 07-07: "een app met miljoenen transacties is een ideaal doelwit — moet ik me voorbereiden?" → ja, gefaseerd)
- WHY: Alles tot nu is intern getest (162 tests + adversariële agents), maar níémand van buiten heeft betaald geprobeerd in te breken. De harde poort: geen echte-geld-launch zonder externe pentest. Doelwit-analyse is gunstig — wij zijn bewust een saai doelwit (geen namen in de databank, klein vlottend saldo, fiat bij de vergunde partner, kasboek toch al publiek, bonnen op de keten) — maar "saai doelwit" moet een expert bevestigen, niet de bouwer.
- WHAT: (a) scope-document voor een Belgische/EU-pentest-partij (app + API + portaal + geldpaden + sleutelbeheer); (b) verantwoorde-onthullings-pagina (/security.txt + beleid: melden mag, we antwoorden, we betalen bij echte vondst — kleine bug-bounty); (c) bevindingen → zelfde eerlijkheid als alles: gefixt + gelogd; (d) herhaal-cadans (jaarlijks of bij grote architectuurwijziging); (e) incident-draaiboek koppelen (GDPR-datalekmelding, zie CLAUDE.md).
- VERIFY: rapport ontvangen, alle hoog/kritiek gedicht + hertest, security.txt live, draaiboek op de tab. GATE: dit item blokkeert de mainnet/echt-geld-launch.
☐ 19e. ⏭ GATED — Multi-server schaal-poort: gedeeld grootboek + cross-proces-slot vóór echt geld op schaal (sol-pro her-audit 13-07-2026) (gate: pre-mainnet — niet nu bouwen; nodig zodra echt geld ÉN >1 app-instance samenkomen)
- WHY: Vandaag draait R4M single-instance (fly.toml min_machines_running = 1) op één lokale SQLite. Daar is dubbele deposit-boeking onmogelijk: de UNIQUE INDEX ON (funding_tx) (src/db.ts:214) + de app-checks (marketplace.ts:51 "one deposit funds exactly one pool", dividends.ts:80 "one deposit distributes exactly once") dekken alles, en de settle-lock is een in-proces mutex. Die garantie leunt op "één machine, één database". Schaal je naar meerdere Fly-machines mét echt geld zonder gedeelde/gerepliceerde DB, dan heeft elke instance z'n eigen SQLite → het unieke slot spant niet meer over alle instances en dezelfde storting kan op instance A én B geboekt worden. Sol-pro's her-audit noemde dit terecht een mainnet-blokker (naast treasury-KMS en het dubbel-grootboek aanzetten); GÉÉN bug in de huidige testnet-app. Correspondeert met de bekende "cap-atomiciteit in-process" en "settle-lock in-proces" noten uit docs/AUDIT_INDEPENDENT_2026-07-13.md.
- WHAT: Één ingreep op platformniveau (niet per land / niet per toepassing — alle profielen erven het): (a) verhuis de money-state van lokale SQLite naar een gedeelde bron — route 1: Fly single-writer primair + read-replicas (alle geld-schrijfacties naar één primaire machine); route 2: gedeelde DB (Fly Postgres, of Turso/LiteFS voor SQLite) zodat de UNIQUE-constraints cross-instance spannen. (b) Vervang de in-proces settle-mutex (src/settlement/settler.ts withSettleLock) door een cross-proces-slot (Redis / Postgres-advisory-lock / DO) zodat maar één instance tegelijk een obligation settelt. (c) Deployment-guard: weiger >1 instance te draaien tenzij een gedeelde-DB/lock-config gezet is (fail-closed, zelfde patroon als de mainnet/treasury-guards). Bewaar alle bestaande idempotentie-sleutels (funding_tx, settlement_attempts.idempotency_key, obligations UNIQUE) — die dragen 1-op-1 over naar de gedeelde DB.
- VERIFY: twee app-instances tegen dezelfde gedeelde DB; dezelfde funding_tx gelijktijdig naar beide → exact één boeking, de andere 409; gelijktijdige settleDue() op beide instances → elke obligation exact één keer gesetteld (nul dubbele on-chain transfers); >1 instance zonder gedeelde-DB-config → boot weigert (fail-closed). GATE: blokkeert samen met 19c de mainnet/echt-geld-launch-op-schaal.
☑ 4b. GEDAAN 2026-07-04, v0.24.1 — settle crash-window dubbel-betaling gedicht — on-chain nonce vastgepind + vóór broadcast opgeslagen; tx-hash meteen na broadcast opgeslagen (fataal venster = 1 DB-write i.p.v. 90s receipt-wait); retry van een 'pending'-rij doet éérst een chain-check (probeLanded) en zendt anders opnieuw uit op DEZELFDE nonce (chain accepteert 1 tx/nonce → vervanging, nooit dubbel). 3 crash-tests (crash-na-broadcast, echt-nooit-geland, replay), 132 groen. — was: 4b-oud. 💰 MONEY-INTEGRITY (ontdekt tijdens 19a-adversarial-verify, 2026-07-03) — settle crash-window kan dubbel betalen (pre-existing latent bug in de settler, blootgelegd door de skeptic-agents; 19a's graceful-drain dekt de gewone SIGTERM-case, maar een HARDE crash (kill -9, OOM, stroomuitval) in het millisecondevenster tussen een geslaagde broadcast en de settlement_attempts-UPDATE naar 'settled' blijft over)
- WHY: recordedSettle (src/settlement/attempts.ts) zet de attempts-rij op 'pending' vóór de broadcast, broadcast't, en zet dán 'settled'. Crasht het proces ná een geslaagde on-chain transfer maar vóór de 'settled'-UPDATE, dan blijft de rij 'pending'. Bij de volgende boot ziet recordedSettle een bestaande 'pending'-rij → herhaalt de broadcast → dezelfde begunstigde wordt een tweede keer betaald. De comment "no money moved the first time" klopt niet in dit venster. Dit schendt "money before promises / never fake success" met ECHT geld — enige reden dat het zeldzaam is: het venster is klein en vereist een gefinancierde schatkist.
- WHAT: Maak re-broadcast van een 'pending' attempts-rij veilig: vóór opnieuw uitzenden, verifieer op de chain of de vorige broadcast al geland is (query naar een Transfer met de idempotency-referentie / bewaar het tx-hash-vermoeden en check de receipt) — pas her-uitzenden als er GEEN bevestigde transfer is. Alternatief per kit: een deterministische on-chain nonce/idempotency-sleutel zodat een tweede broadcast dezelfde tx is (geen dubbele beweging). Sluit aan op item 6 (verifyDeposit/receipt-logica bestaat al) en item 3 (adapter-contract). Bewaar de vier-staten-eerlijkheid: onzeker = pending, nooit een verzonnen success.
- VERIFY: typecheck + boot + demo + tests: gemockte adapter waarbij broadcast slaagt maar de 'settled'-UPDATE faalt (gesimuleerde crash) → bij de volgende settleDue() wordt NIET opnieuw uitgezonden (de chain-check ziet de transfer) en de obligation wordt alsnog correct als settled afgesloten; een broadcast die écht nooit landde → mag wél opnieuw; nul dubbele betalingen in beide gevallen.
☐ 24. 🚀 LAUNCH-PAKKET — begrijpelijk, aantrekkelijk, regio-correct (founder 2026-07-03: "basisversie online in dágen, niet weken")
- WHY: Founder-richtlijn 3/7 nacht: de basisversie moet in dagen online; mensen moeten er met plezier komen kijken om te verdienen, bij te leren of info toe te staan. Dag 1 van docs/LAUNCH_IN_DAYS.md is al gebouwd (Fly + magic-link/OTP-login); dag 2 wacht op de Stripe-sleutel (founder-actie); dag 3 op de founder-GO. Wat de motor nog mist is de begrijpelijkheids- en gastvrijheidslaag + de wettelijke regio-schakelaar ("alle wetten moeten opgevolgd worden — in bepaalde regio's wel of niet uitbrengen").
- WHAT: (a) Leer-sectie op 12-16-jarige-niveau in de app én op de productpagina: hoe werkt het, wat verdien je echt (honest-math-taal, plafonds eerlijk), hoe blijft je naam operationeel ongebruikt (identiteitsbewijs verzegeld, alleen-audit) — dit is óók het founder-leermateriaal zodat Franky alles simpel zelf kan bekijken, bijleren en zijn product kan verkopen; (b) onboarding-polish consumer-app (eerste-bezoek-flow: 3 stappen naar je eerste betaalde inbox); (c) regio-gate bij registratie: profiel-instelbare allowlist (start BE/EU), eerlijke melding elders ("nog niet beschikbaar in jouw regio — de wet gaat voor"); (d) docs/LAUNCH_IN_DAYS.md actualiseren tot afvinkbare checklist met wat al klaar is (Fly ✓, login ✓, Resend-domein open, Stripe open, GO open); (e) jargon- en eerlijkheidsregels overal onverkort.
- VERIFY: typecheck + boot + demo + tests; leerpagina's aanwezig en jargon-vrij (grep); registratie uit niet-toegelaten regio → eerlijke weigering met reden (test); LAUNCH_IN_DAYS-checklist toont per regel klaar/open met datum.
- 24b. Geverifieerde-mensen-teller — "welkom bij de geverifieerde mensen" als groeiverhaal (founder-idee 09-07, geboren uit de eerste eID-verificatie): publieke teller (app + productpagina) van het aantal geverifieerde unieke mensen, gevoed door het ECHTE identities-cijfer (assurance ≥ substantial) via een publiek endpoint — nooit een verzonnen of opgeblazen getal (regel 3; vandaag eerlijk: 1, en dat is charmant in het founder-verhaal). De verkoopkracht zit in wat geen enkele concurrent kan zeggen: elke andere app telt gebruikers mét bots en dubbels — onze teller is de enige waar élk cijfer een bewezen uniek mens is, en de teller kan dat zélf bewijzen (audit-tool + kasboek). Uitvoering: klein (endpoint + één UI-regel), maar de zin "welkom bij de geverifieerde mensen — jij bent nr. N" is het lidmaatschapsgevoel dat de app promoot zonder één advertentie. GATE: pas publiek tonen vanaf een drempel die de founder kiest (een teller op 3 is intern leuk, publiek pas wanneer het verhaal draagt — founder beslist het moment). — kaartbetaling naar je inbox (LAUNCH_IN_DAYS dag 2a) — ✅ 2026-07-03 v0.21.0 (MM4 founder-sessie, founder-vraag "kunnen we de Stripe-testsleutel al integreren?"). Shipped: src/routes/stripePay.ts — POST /api/stripe/checkout/:handle (Checkout-sessie in de deployment-munt; onbekende inbox → 404 VÓÓR er een sessie bestaat — betalen-zonder-levering kan niet) + POST /api/stripe/webhook (handtekening-verificatie over de rauwe bytes; levering ALLEEN op bevestigde checkout.session.completed met exact het inbox-bedrag — afwijkend bedrag → geparkeerd amount_mismatch, nooit geleverd; retries idempotent via UNIQUE session + status-transitie). Bericht geparkeerd in stripe_pending_messages tot betaling bevestigd. deliverPaidMessage kreeg een eerlijke via-parameter (x402|internal|card): kaart-levering ledgert inbox_message_paid_card en boekt op x402_sales.channel='card'; honest-math toont cardSales apart — kaart-omzet lekt nooit in de x402-cijfers. Fail-closed: geen sleutel → 503 met de fix; sk_live weigert te booten** (founder-beleid: live pas na BV + advocaat-perimeter) — beide live geverifieerd. .env.example legt stap voor stap uit waar de sk_test vandaan komt. Tests: 96 groen onder beide settlement-plugs (nieuw: stripepay.test.ts + stripe-nokey.test.ts; SDK gemockt — de tests bewijzen ÓNZE geldregels).
- ✅ GEACTIVEERD + LIVE BEWEZEN 2026-07-03 nacht (samen met founder): apart alleenstaand Stripe-sandbox-account "R4M" (gescheiden van Turbeau2Rock), sk_test via klembord in .env (nooit getoond, klembord geschoond); webhook-endpoint via de Stripe-API aangemaakt naar https://r4m-franky.fly.dev/api/stripe/webhook (secret alleen-bij-aanmaak opgevangen); beide secrets op Fly gezet en v0.21.0 gedeployed. DE EERSTE ECHTE KAART-BETALING OP DE PUBLIEKE APP is gedaan (founder, testkaart 4242): bericht geleverd ná bevestiging, split exact geboekt (1,70/0,16/0,08/0,06), honest-math cardSales {count:1, revenueUsd:2}, x402-kanaal onaangetast. LAUNCH_IN_DAYS dag 2a: AF.
☐ 30. 🚀 Stripe-testplug been UIT — uitbetalen naar je bankrekening via Stripe Connect (LAUNCH_IN_DAYS dag 2b)
- WHY: Het sluitstuk van "in dagen online": verdiensten moeten naar een echte bankrekening kunnen (founder: "als mensen iets verdienen, moeten ze direct kunnen betalen of iets mee verkrijgen"). Stripe Connect Express draagt de vergunning en de KYC — wij blijven het platform (LAUNCH_IN_DAYS-model). Dit is óók de eerste fiat-settlement-adapter, dus hij bewijst de kit §2.5-states (rejected/reversed) in het echt.
- Founder-eis 05-07 (meerdere brievenbussen): mensen hebben in realiteit meerdere KYC'd rekeningen/wallets. Ontwerp: één geverifieerd persoon mag N uitbetaal-bestemmingen koppelen (bank via Stripe, wallet A/B) en kiest per uitbetaling — maar bestemmingen vermenigvuldigen NOOIT rechten (dividend/stem/account blijven aan de person-hash hangen, niet aan de brievenbus). Onderzocht 05-07: Stripe Connect is exact hiervoor gebouwd — platform bezit nooit fondsen, Stripe draagt e-money-licentie + KYC op elke ontvanger (stripe.com/guides/how-psd2-impacts-marketplaces-and-platforms). Advocaat-vraag verengd tot: past onze split+crypto-poot binnen Connect-voorwaarden?
- WHAT: (a) Connect Express onboarding-flow voor consumer-accounts: POST /api/app/me/:id/bank-koppelen → account-link-URL (Stripe's eigen KYC-flow), status-opslag (stripe_account_id op users, geïsoleerd zoals payout_channels — quasi-PII, verwijderbaar); (b) src/settlement/adapters/stripe-connect.ts als SettlementAdapter (registry-id stripe-connect-test): settle() = Transfer naar het connected account met de idempotency-key als transfer_group/idempotency-header, states gemapt per kit §2.5; balance() via de Stripe balance-API; (c) uitbetaal-scherm: naast het bestaande pad een "naar je bankrekening"-optie zodra het account gekoppeld is — zelfde obligation-first-rail, geen nieuwe geld-mechaniek; (d) testmodus-only guard geldt al (item 29). Geen sleutel → eerlijk pending, exact zoals de ontbrekende treasury-key vandaag.
- VERIFY: typecheck + boot + demo + tests (gemockte Stripe): onboarding-link aangemaakt, settle → settled met transfer-ref, geweigerde transfer → rejected geparkeerd (nooit auto-retry), replay beweegt niets, obligation-rail end-to-end; zonder sleutel → alles eerlijk pending; jargon-grep nul ("bankrekening", nooit "Connect").
☑ 16a-a. GEDAAN 2026-07-04, v0.25.0 — Sponsorportaal (kern): sessie-login draagt de sponsor-rol server-side (klant ziet nooit een sleutel); organisatie-account (1/gebruiker); kostenraming vooraf (totaal + commissie, niets aangemaakt); project-draft = eerlijk 'awaiting_funding' → pas na financiering wordt de bounty gepubliceerd (geld vóór beloftes); deposit-route (fundingTx) activeert + publiceert; live vullings-dashboard (aanbrengingen/aanwervingen/budget); sponsor-isolatie (404 op andermans project). Werkende UI: /sponsor.html. 6 tests, 138 groen. Rest → 16a-b.
☐ 16a-b. Sponsorportaal (rest): factuur-route echt koppelen aan 16c; projectsjablonen bevestigingsscherm/looptijd; dossier + uniciteitscertificaat na afloop (16b); huisstijl-tokens 9c. — was: 16a. Sponsor-zelfbedieningsportaal — "steden of staten loggen in, zetten hun project op en betalen ervoor" (founder-eis 2026-07-02 avond)
- WHY: De klant (stad, bureau, staat) mag geen curl of consultant nodig hebben: inloggen → project opzetten → betalen → live volgen → dossier. Dit ís de B2B-wig als product: zelfbediening maakt elke licentie schaalbaar en elke demo verkoopbaar. Founder 3/7: ook regio's/provincies zetten hier "een hele hoop sociale taken" op — B2G-venue van hetzelfde portaal. "Mits ze ervoor betalen" is structureel al waar (geld-vóór-beloftes: geen gefinancierde pool = geen taken; item 6 verifieert de storting op de chain) — dit item bouwt de deur ervoor.
- WHAT: (a) Sponsor-login: e-mail+code-login (bestaande auth-flow) gekoppeld aan een sponsor-account; de item-5-sponsorsleutel wordt intern beheerd (de klant ziet nooit een sleutel — sessies dragen de rol). (b) Project-wizard in een sponsor-webportaal (app/sponsor/ of route in de bestaande app): naam → soort (panel/onderzoek/taken) → aantal deelnemers/taken → vergoeding p.st. → looptijd → totaalkost incl. platformcommissie vooraf getoond. De wizard start vanuit PROJECTSJABLONEN (founder 3/7: "ik moet templates maken voor die bedrijven"): voorgebakken recepten per soort — burgerpanel, burgerbegroting-verificatie, tevredenheidsmeting, focusgroep, leerprogramma — met verstandige defaults (aantallen, vergoedingen, looptijd, dossieropties), zodat een stad in drie klikken van sjabloon naar gefinancierd project gaat. (c) Betaalstap met twee routes: FACTUUR (concept-factuur uit 16c, project start pas na "betaald"-markering door admin) of STORTING (on-chain, item-6-verificatie automatisch); tot betaling staat het project eerlijk als 'wacht op financiering'. (d) Live vullings-dashboard per project (Frankys live-detectie-idee): voltooiingen, unieke deelnemers, dubbelpogingen geweigerd, resterend budget. (e) Na afloop: knop naar dossier + uniciteitscertificaat (16b). (f) Zelfde huisstijl-tokens als de consumer-app (9c-klaar); geen jargon; alles NL eerst.
- VERIFY: typecheck + boot + demo + tests: sponsor-sessie kan alléén eigen projecten zien/maken (403 op andermans — hergebruik item-5-eigenaarschap); project zonder financiering kan geen taken publiceren (402); wizard-totaal = exact wat de pool-financiering eist; factuur-route maakt 16c-conceptfactuur; live-dashboard-endpoint telt correct (voltooid/uniek/geweigerd); jargon-grep nul.
☐ 16. Sponsor verification UI (close the README's own TODO)
- WHY: The revenue loop's bottleneck is verdicts: today approving evidence — the act that releases every payment — is a raw curl. A sponsor pilot needs a screen, or the funded pool stalls and no revenue exists. (Product surface, not marketing: it operates the money flow.)
- WHAT: New view in app/ (extend index.html/app.js, no framework): "Verify" tab listing submissions with verdict IS NULL (new GET /api/submissions/pending, verifier|admin key, joined with task + worker + evidence link + attestation validity from item 15), approve/reject buttons with required reason posting to the existing verdict endpoint, key entered once and kept in localStorage. Show the resulting split and (if pending) the queued obligation honestly in the success toast.
- VERIFY: typecheck + boot + seed; drive the flow against the served app (scripted via fetch against the same endpoints; a manual browser pass when one is available): claim+submit via demo, approve with reason → task shows paid, ledger row appears in the Public Ledger tab, obligation counter increments when treasury unfunded; GET /api/submissions/pending without key → 401 when auth enforced.
☐ 16b. Klant-backlog & rapportage — het volledige dossier per klant (founder-eis 2026-07-02: "volledige backlogs aanbieden aan de klanten")
- WHY: Een bureau of stad koopt niet alleen deelnemers — het koopt verantwoording. Elke klant moet zijn complete historiek kunnen inzien én meenemen: welke opdrachten, hoeveel unieke deelnemers, wat betaald, wanneer, met welk bewijs. Dit is tegelijk het B2B-verkoopargument ("u krijgt het volledige dossier, altijd") en de overheids-eis (audit-klaar). Zonder dit blijft de klantrelatie een e-mail-archief.
- WHAT: (a) Per-sponsor dossier-endpoints (auth via item 5-sleutels, sponsor ziet ALLEEN het eigen dossier): GET /api/sponsor/:id/dossier — alle pools/opdrachten met status, funding, resterend budget; per opdracht de voltooiingen (geanonimiseerd: uniek-mens-hash-bewijs, tijdstip, vergoeding — nooit identiteiten); alle ledger-rijen die de sponsor aangaan. (b) Exports: CSV én JSON per opdracht en per periode (?format=csv&from=&to=), plus een genereerbaar uniciteitscertificaat per afgeronde opdracht ("N deelnames, N unieke geverifieerde personen, X dubbelpogingen geweigerd" — ondertekend met een hash van de onderliggende ledger-rijen zodat het naspeurbaar is). (c) Maandoverzicht-endpoint (facturatie-spoor: volumes, commissie, btw-klaar). (d) UI-tab in het sponsor-portaal (uitbreiding van item 16): historiek + download-knoppen. Geen PDF-generator in dit item — CSV/JSON eerst (bureaus willen data, geen opmaak); PDF-certificaat als follow-up wanneer een echte klant erom vraagt.
- VERIFY: typecheck + boot + demo + tests: sponsor-sleutel A kan dossier A lezen en krijgt 403 op dossier B; CSV-export bevat exact de ledger-bedragen (som klopt tot op de micro); certificaat-hash valideert tegen de ledger-rijen en faalt bij manipulatie; geanonimiseerd = geen enkel identiteitsveld in enige export (test grept op e-mail/naam-velden).
☐ 16c. Founder back-office: facturatie & logboeken — "facturen versturen, direct in de applicatie" (founder-eis 2026-07-02)
- WHY: De omzet is B2B-facturen (tribunaal: €2.000–7.500/studie) — dus factureren hoort ín het product, niet in een losse Word-sjabloon. En de founder moet zijn hele operatie kunnen inzien zonder terminal: logboeken, uitbetaal-wachtrij, backup-status. Werkdocumenten, geen presentatiefolder (founder: "niet alsof het een volledige presentatiefolder is").
- WHAT: (a) invoices-tabel: sequentiële nummering zonder gaten (wettelijk verplicht; per jaar: 2026-001…), klantgegevens (sponsor-ref + facturatie-adres + btw-nummer), opdracht-referentie, bedrag excl/incl btw (21% default, configureerbaar), vervaldatum, status (concept → verzonden → betaald/vervallen). (b) Endpoints (admin-sleutel, item 5): factuur aanmaken vanuit een opdracht (bedragen vooringevuld uit het 16b-dossier), sobere zakelijke PDF/HTML-weergave (één pagina, geen folder), versturen per e-mail via Resend, status bijwerken; volledig factuur-spoor geledgerd als invoice_*-events. (c) ⚠️ COMPLIANCE-NOOT: België verplicht sinds 2026-01-01 gestructureerde e-facturatie (Peppol) voor B2B — wij bouwen GEEN eigen Peppol-access-point; dit item levert een UBL/Peppol-klare export + koppelinstructie voor een verzendpartner of het boekhoudpakket (keuze met de boekhouder); PDF+mail blijft de menselijke kopie. (d) Founder-logboeken-tab in het beheer (uitbreiding dev.html): ledger-filters, openstaande uitbetaal-aanvragen, obligaties, factuurstatussen, laatste auto-backup-runs — read-only inzicht op één scherm.
- VERIFY: typecheck + boot + demo + tests: factuurnummers strikt sequentieel zonder gaten (transactie, ook bij gelijktijdige aanmaak); zonder admin-sleutel → 401; bedragen exact uit het opdracht-dossier; UBL-export schema-valide; verzenden zonder RESEND_API_KEY → eerlijke status "concept" met reden, nooit "verzonden" gefaket; ledger bevat het volledige factuur-spoor.
☑ 16d-a. Bounty-motor (kern) — GEDAAN 2026-07-04, v0.23.0: gefinancierde bounty (402 zonder pot) · sybil-proof aanbrenger (identiteit verplicht) · first-touch per kandidaat-hash (409 op tweede) · werkgeversattest = kwalificatiepoort · milestone 1 (aanwerving) + milestone 2 (anciënniteit) exact, obligation-first, nooit gefaket · commissie 8–15% hard afgedwongen · kandidaat = alleen salted hash. Endpoints /api/bounties/*; 11 nieuwe tests, totaal 124 groen. ⚠️ GO-LIVE blijft achter advocaat-vraag #18 (erkenning arbeidsbemiddeling).
☐ 11c. Identity-hardening #2 — sessie-binding op /verify (uitgesteld uit de C1-fix, 2026-07-12) ⚠️ NIET VERGETEN
- WHY: De C1-fix (v-volgend, POST /api/identity/verify) sloot de parallelle achterdeur: hosted weigert élke provider (403, FLY_APP_NAME), alleen demo is lokaal bereikbaar onder R4M_ALLOW_DEMO_VERIFY, en assurance kan nooit uit de body naar high (echte eID enkel via de handtekening-geverifieerde OIDC-callback). Wat bewust UITGESTELD is: punt #2 van het fix-voorstel — /verify binden aan een ingelogde sessie én aan de EIGEN worker van de beller (nooit een willekeurige workerId). Reden voor uitstel: het demo/test-harnas maakt workers via POST /api/workers zónder sessie/user-koppeling, dus sessie-binding nu zou npm run demo (de proof-of-economy) + 23 tests breken. Géén productierisico vandaag: op Fly is /verify volledig dicht (#4), dus #2 hardt enkel de lokale dev-deur.
- WHAT: Wanneer het consumer-app-login-been aan de worker-kant landt (zie ook 16d-c-a): (a) /verify vereist currentUser(req) (401 zonder sessie); (b) resolve de eigen human-worker van die user (workers.user_id = user.id) en weiger een afwijkende workerId met 403; (c) verbouw scripts/demo.ts + de identity-gerelateerde tests naar een sessie-gebaseerd fixture (login → user-gekoppelde worker → verify) zodat de demo blijft kloppen; (d) daarna mag de dev-flag/hosted-gate strenger (bv. sessie ook lokaal verplicht).
- VERIFY: /verify zonder sessie → 401; met sessie maar vreemde workerId → 403; demo-fixture (eigen worker) → 200; npm run demo + volledige suite groen; jargon-grep nul.
☐ 11d. Treasury-sleutel: KMS/HSM + multisig — de ECHTE fix (uitgesteld naast de interim-cap, 2026-07-12) ⛔ GATE vóór echte crypto-waarde ⚠️ NIET VERGETEN
- WHY: De audit-bevinding (single-point-of-total-loss): de treasury-private-key tekent élke payout, met als enige plafond het on-chain saldo — geen HSM/KMS-grens, geen multisig. De sleutel leeft in de proces-omgeving (TREASURY_PRIVATE_KEY, via viem in settlement/adapters/erc20-base.ts). De interim-fix (2026-07-12, v-volgend) legde een code-niveau backstop op: per-transactie- en rollende-24u-caps (R4M_MAX_PAYOUT_MICRO / R4M_MAX_DAILY_MICRO, fail-closed, obligation blijft pending, luid gelogd; test test/treasury-caps.test.ts). Dat begrenst het verlies, maar beschermt de sleutel zelf niet — wie de env/host compromitteert, tekent nog steeds tot aan de cap. Caps zijn een emmer onder het lek, geen dichte kraan.
- WHAT: (a) De payout-signing achter een KMS/HSM-grens (bv. AWS KMS / GCP KMS / cloud-HSM, of een remote-signer) zodat de ruwe private key nooit in het app-proces of in .env staat; viem/de adapter roept een sign-API aan i.p.v. privateKeyToAccount. (b) Multisig / threshold op de schatkist (bv. Safe of MPC-signing) zodat één gecompromitteerde sleutel geen enkele payout kan bewegen — m-van-n boven een drempelbedrag. (c) Spending-caps blijven als extra laag (defence-in-depth), maar mogen dan strenger + per-rol. (d) Sleutel-rotatie + toegangslogging op de KMS-grens; overdracht-ceremonie in docs/KEY_ESCROW.md bijwerken (de treasury-key valt vandaag onder de escrow-envelop — herzien wanneer hij naar KMS verhuist). ⛔ HARDE GATE: dit MOET af zijn vóór er échte (mainnet, niet-testnet) waarde in de schatkist komt — de interim-caps dekken alleen de testnet-fase (founder-beslissing 2026-07-12: "test rails; we tighten before real crypto value").
- VERIFY: de ruwe TREASURY_PRIVATE_KEY staat nergens in proces-env/.env/logs bij een echte deploy; een payout vereist ≥ m handtekeningen boven de drempel; sleutel-rotatie getest; caps blijven groen; audit-tool bevestigt de KMS-grens.
☐ 16d-c. Bounty-hardening uit de skeptic-audit 04-07 (2 restpunten)
- WHY: Adversariële review v0.23.0: (1) /refer is ongeauthenticeerd en neemt een willekeurige referrerWorkerId — first-touch-squatting op andermans naam blijft mogelijk (rate-limit staat er al; reject geeft de slot vrij, dus niet meer permanent); (2) zelf-aanbrenging (aanbrenger = kandidaat) is per ontwerp ondetecteerbaar (hash-only) — beleidskeuze expliciet maken.
- WHAT: (a) /api/bounties/:id/refer achter worker-sessie-auth (consumer-app-login koppelen aan workerId) zodra de app-kant landt; (b) founder-beslissing: zelf-aanbrenging toestaan (werkgever attesteert toch) of contractueel verbieden in de sponsor-voorwaarden — vastleggen in het productregister.
- VERIFY: refer zonder geldige worker-sessie → 401; beslissing zelf-aanbrenging gedocumenteerd.
☑ 16d-b. GEDAAN 2026-07-04, v0.24.0 — 7 sjablonen (zorg/transport/accountancy/IT/bouw + interim-white-label + KMO-arbitrage, verankerd op echte BE-premies) via /api/bounties/templates + /from-template (zelfde geldregels, 402 zonder pot); fiscale laag: cap-vlag op premie #3/12m per aanbrenger (herkwalificatie-bewaking) + fiche 281.50-export (admin, bruto uit obligations, >€250-drempel). 129 tests groen. Rest (16a-wizard-UI-integratie) volgt met 16a. — was: 16d-b-oud. Rekruteringsbounty-sjabloonfamilie — de knelpuntberoep-aanbrengpremie als gefinancierd €100-moment (reality-filter 2026-07-04; 100_EURO_MOMENTS #40–42 → GO)
- WHY: Drie GO-cases (transport C/CE, accountancy, IT/engineering) delen exact één bouwwerk: een aanbrengpremie waarbij een werkgever een gefinancierde pool stort (geld-vóór-beloftes) en een geverifieerd-unieke burger de premie claimt bij een geattesteerde aanwerving. Bestaande BE-ankers mét naam: SBB €2.000 (expliciet óók externen), Xwift €1.000 (2021), Talent-IT €1.000. Dit is de breedste €100-moment-familie én een COMMERCIËLE flow → 8–15% commissie mag (ijzeren regel 4). Verkoopkanaal-hefboom: één accountantskantoor heeft honderden werkgever-klanten.
- WHAT: (a) Bountysjablonen in de 16a-wizard per knelpuntberoep (transport/accountancy/IT/zorg/bouw) met verstandige defaults (bedrag, milestone-schema, kwalificatievereiste). (b) Harde kwalificatiefilter: premie pas claimbaar nadat de werkgever bij indiensttreding de objectieve vereiste bevestigt (C/CE+code95, ITAA-inschrijving, …) — géén premie op ongekwalificeerde spam-kandidaten. (c) First-touch-registratie (primitief bestaat): één premie per uniek aanbrenger-kandidaat-paar, ooit — sybil-proof via één-mens-één-account. (d) Milestone-payouts: milestone-1 bij indiensttreding (DIMONA-attest bestaat sowieso), rest na 3 mnd anciënniteit; IT-variant krijgt een eerste-interview-milestone (€25–50). (e) Fiscale compliance-laag = verkoopfeature: auto fiche 281.50-data boven €250/begunstigde/jr + bewaking van de ~2×/12-maanden-cap per aanbrenger (beschermt het 33%-divers-inkomenregime tegen herkwalificatie tot beroepsinkomen; sanctie geheime commissielonen tot 100% bij ontbrekende fiche). (f) Commissie 8–15% op de pool (commerciële flow); 0% blijft voor burger-distributies. (g) Twee GO-venues erbij (reality-filter 2026-07-04, #44+#45): (g1) White-label uitzend-rail — sybil-proof refer-a-friend als B2B-module voor interimkantoren (betaler live bevestigd: Hays.be €250/plaatsing, onbeperkt); kantoor = klant, verkoopargument = fraudepreventie + uitbetalingssnelheid; verplichte volume-cap-bewaking per aanbrenger (Hays' onbeperkt-model botst met de BE 2×/12-cap → herkwalificatierisico). (g2) KMO-bureau-arbitrage — publieke bounty-pool (€2.500-default) vervangt de €10.000+ selectiebureaufee (75% besparing); poster-case voor de app-store-pagina; categorie extern gevalideerd (Draftboard $4,1M/20%, ~70 bedrijven — maar 20% betaald door de AANBRENGER, R4M factureert de werkgeverszijde). Beide draaien op exact de a–f-motor; alleen sjabloon-defaults + kanaalpositionering verschillen. ⚠️ GATE — advocaat-vraag #18: wordt R4M een erkenningsplichtig particulier bureau voor arbeidsbemiddeling, of valt "bijstand aan werkgevers bij het vinden van geschikte werknemers" (uniciteit+settlement, de burger brengt aan) in de niet-erkenningsplichtige zone in Vlaanderen (Vlaanderen erkenning)? Beantwoorden vóór go-live; motor + sjablonen mogen wél al gebouwd worden.
- VERIFY: typecheck + boot + demo + tests: pool zonder financiering kan geen bounty publiceren (402); premie zonder werkgeversattest-van-kwalificatie blijft pending, nooit uitbetaald/gefaket; tweede claim op hetzelfde aanbrenger-kandidaat-paar geweigerd; milestone-1 betaalt exact het geconfigureerde deel, rest pas na anciënniteitsattest; fiche 281.50-export bevat elke premie >€250 en de cap-teller vlagt de 3e premie/jr/aanbrenger; jargon-grep nul.
☐ 22. AI-assistlaag — lokaal model geïntegreerd (founder-vraag 2026-07-02: "automatiseren, verbeteren, versterken")
- WHY: AI hoort in de machinekamer, niet in de etalage: R4M verkóópt menselijk oordeel en menselijke uniciteit — AI mag dat versnellen, nooit vervangen. GRONDWET van dit item: AI adviseert, een mens beslist — zeker over geld en verdicten (houdt ons ook buiten AI-Act-hoog-risico: human-in-the-loop, gedocumenteerd). Privacy: alles wat gebruikerscontent raakt draait LOKAAL (Ollama staat al op de MM4: hermes3:70b, qwen2.5:14b, deepseek-ocr) — persoonsdata verlaat de machine niet (GDPR-register bijwerken bij bouw).
- WHAT: (a) src/ai/provider.ts naar het adapter-model van item 3: AI_PROVIDER=deepseek|minimax|anthropic|ollama|none (default none — fail-closed: geen provider = features onzichtbaar, nooit nep-advies). FOUNDER-KEUZE 2026-07-02: voorkeur DeepSeek-Flash / MiniMax-M3 (goedkoop+snel, API-key in .env) boven de lokale 8TB-modellen. ⚠️ GDPR-GATE per workload: alles met persoonsgegevens (bewijsfoto's, berichten) mag alléén naar een provider mét EU-verwerkersovereenkomst óf na pseudonimisering — anders lokaal (ollama blijft de fallback voor gevoelige workloads); keuze per workload vastleggen in docs/GDPR_COMPLIANCE.md-register vóór activatie. (b) Verificatie-assistent (grootste hefboom — het verdict is de flessenhals van de hele economie): bij een submission een advies-object {oordeel: lijkt-geldig|verdacht, redenen[], ocr-tekst} voor de verifier-UI (item 16), duidelijk gelabeld "AI-advies — beslissing is aan jou", nooit auto-approve; deepseek-ocr voor tekst-in-foto. (c) Fraude-signalen: perceptual hash naast de exacte evidence-hash (bijna-identieke foto's over taken), timing-/patroonvlaggen per worker — als signaal op het dossier, nooit als automatische straf. (d) Inbox-assist: samenvatting + concept-antwoord per betaald bericht (lokaal model), opt-in in de UI. (e) "Vraag een geverifieerd mens"-API: de verdien-kant-verpakking voor agents — een x402-endpoint waar een agent een menselijke beoordeling/foto/verificatie koopt en R4M het als taak aan geverifieerde mensen routeert (de Apify-trigger uit SIGNALS_ADAPT, als product). (f) De AI-dispatcher (founder-idee 2026-07-02): kansen automatisch aandienen — AI matcht gefinancierde opdrachten/panels met de juiste geverifieerde gebruikers (regio, taal, track record, beschikbaarheid) en zet ze als intekenbare lijst in hun app ("3 kansen deze week: samen €47"); afwerken = direct uitbetaald via de bestaande obligation→settler-rail. Staten/steden voeden dit via het sponsor-portaal (16a) met terugkerende programma's — de dispatcher is de brug tussen "staat zet iets klaar" en "burger verdient vandaag". Matching is advies+volgorde, nooit uitsluiting: elke gefinancierde kans blijft voor iedereen zichtbaar (eerlijkheids- en antidiscriminatieregel — géén AI die bepaalt wie mag meedoen).
- VERIFY: typecheck + boot + demo + tests: provider none → alle AI-features afwezig zonder gaten; ollama-mock → advies-object met verplicht "AI-advies"-label; verdict blijft 100% menselijk (geen codepad dat zonder mens uitbetaalt — test bewijst dat); perceptual-dup flagt bijna-identieke testafbeeldingen; jargon- en GDPR-regels intact (geen gebruikerscontent naar externe API's bij AI_PROVIDER=ollama).
• ❌ ~~31. Geboortekluis v2 — de startkapitaal-rekening (founder-opzet 3/7 nacht: "het kind start tegen 18 of 21 letterlijk met een mooie zakcent, opgebouwd door zijn ouders, sponsors, zichzelf of zelfs de staat")~~ — VERVALLEN per founder-beslissing 2026-07-06: de kinderkluis is uit de R4M-rail; de 3% ging naar de verdiener (split nu 88/8/4). Het kluis-mechanisme is geen R4M-product meer. WHY/WHAT/VERIFY hieronder blijven staan als historiek.
- WHY: Founder-verduidelijking 3/7: de kluis is een STARTKAPITAAL-rekening per kind, opgebouwd uit stortingen mét naam — ouders/familie, sponsors, het kind zelf (spel- en leerverdiensten, item 25) en zelfs de staat (geboortepremie — het Baby-Bonds-model, precedenten al geverifieerd: CT Baby Bonds, UK Child Trust Fund, SG CDA). GEEN automatische afhouding op platform-flows. Dit is meteen een B2G-product: een stad/staat die per geboorte of per mijlpaal in de kluis stort, gebruikt exact dezelfde rail — en "elke storting heeft een naam" is money-before-promises van nature.
- WHAT: (a) vault_deposits-tabel: stortingen met funder-naam + bron (ouder|familie|sponsor|staat|eigen_verdienste), elke storting geledgerd; kluis-saldo = som van échte stortingen, niets anders; (b) vrijgave-leeftijd instelbaar per kluis (18 of 21 — founder noemde beide; wettelijke check minderjarigen-vermogen/voogdij op de advocaat-lijst); (c) ouder-account-koppeling met GDPR-kids-poorten (bouwt op de item-25-verkenning); (d) de bestaande 3%-flow-accrual wordt OPT-IN per deployment/pool (zie Founder decisions hieronder) in plaats van default; (e) storten kan via de bestaande rails: kaart (Stripe, item 29) of factuur (16c).
- VERIFY: typecheck + boot + demo + tests: storting met funder-naam → exact kluis-saldo + ledger-rij; opname vóór vrijgave-leeftijd → geweigerd met eerlijke reden; met flow-accrual UIT ontstaan er geen vault_accrual-rijen meer op gewone betalingen; jargon-grep nul; advocaat-vragen (voogdij, schenkingsregels, vrijgave-leeftijd) toegevoegd aan de lijst.
☐ 9a. Deployment profiles — loader, schema, and the four profile files
- WHY: "Deployable for ANY state or country" must be a file, not a consulting engagement: one profile bundles identity root + settlement adapter + currency + compliance flags, so standing up R4M-for-country-X is R4M_PROFILE=x npm start. Split from 9b because compliance ENFORCEMENT is a money-path change that deserves its own run under test protection.
- WHAT: profiles/ directory with be-eu.json, sg.json, jp.json, sovereign-generic.json, each declaring: settlementAdapter, currencyLabel, identityProviders (ordered, e.g. BE: itsme→eudi→worldid; SG: eudi-equivalent placeholder+worldid), requireOnchainFunding, requireAuth, allowDemoIdentities, dividendMinPerRecipient, locale, compliance flags (travel-rule threshold, self-hosted-wallet cap €1000, KYC-at amount — values from docs/IDENTITY_DIVIDEND.md §4 and docs/ASIA_DEPLOYMENT.md). Loader src/profile.ts (zod-validated schema) merges profile → env → defaults, with env winning; src/config.ts consumes it. Expose the active profile (minus secrets) at GET /api/public/profile. In THIS item, every compliance flag ships marked "advisory": true in the profile file itself — a flag that silently does nothing is fake and not allowed, so until 9b enforces it, the profile says out loud that it is advisory.
- VERIFY: typecheck; boot each of the four profiles and curl /api/public/profile matches the file (advisory markers visible); R4M_PROFILE=be-eu demo run completes; profile-schema test rejects an unknown flag.
☐ 9b. Deployment profiles — compliance-flag enforcement
- WHY: An advisory flag becomes a real control: the be-eu self-hosted-wallet cap must actually stop a transfer, or the profile is theater.
- WHAT: Bind every flag that is enforceable today: a per-transfer amount above selfHostedWalletCapMicro → the obligation is held for manual release (status stays pending, honestly ledgered held_compliance with the reason), released via POST /api/admin/release-obligation (admin key, ledgered). Flip enforced flags from "advisory": true to enforced in the profile files; flags still unenforceable (e.g. full travel-rule payloads pre-adapter-support) stay explicitly advisory.
- VERIFY: typecheck + boot + demo; test: a payout above the cap in be-eu profile lands as a held_compliance obligation, not a transfer; admin release settles it with a ledger trail; below-cap payouts unaffected.
☐ 9c. White-label theming — "de app in de kleuren van de stad" (founder-eis 2026-07-02: mega pro, geen aftandse app)
- WHY: Een stad/organisatie die een licentie koopt moet de burger-app in de EIGEN huisstijl kunnen aanbieden (logo, kleuren, naam, taal) — vertrouwen komt bij burgers van hún stad, niet van ons merk. White-label is een verkoopargument in elke gemeentedeal en hoort bij het profiel (9a), nooit bij een fork.
- WHAT: (a) branding-blok in het deployment-profiel: { appName, shortName, primaryColor, accentColor, logoSvg (pad of inline), locale }; default = R4M-huisstijl. (b) GET /api/public/branding serveert het actieve blok; de consumer-app laadt het bij boot en zet CSS-variabelen + merkbalk + manifest-velden (naam, theme-color, icoon) dynamisch — één codebase, elke deployment zijn eigen gezicht. (c) Jargon-/eerlijkheidsregels blijven identiek onder elk merk (de huisstijl verandert, de regels nooit). (d) Demo-profiel stad-demo met afwijkende kleuren als bewijs.
- VERIFY: typecheck + boot + demo; boot met R4M_PROFILE=stad-demo → app toont stadsnaam/-kleuren/-logo en manifest volgt mee; default boot = ongewijzigde R4M-stijl; test: branding-endpoint bevat nooit verboden jargon en profielwissel vergt nul codewijziging.
☐ 10. PayPal Payouts adapter — the no-crypto fallback rail
- WHY: docs/FUTURE_RAILS.md: PayPal Payouts is the one big-brand rail paying individuals today, EU-available. A state pilot will have residents without wallets; the use case needs a fiat plug to be credible, and it proves the interface handles non-chain rails (string refs, rejected/reversed states, no balanceOf).
- WHAT: src/settlement/adapters/paypal-payouts.ts implementing SettlementAdapter against the PayPal Payouts REST API (sandbox): env PAYPAL_CLIENT_ID/SECRET/MODE; settle() creates a payout item to an email-typed beneficiary with sender_batch_id = the idempotency key (PayPal enforces uniqueness on it — native idempotency), returns the payout item id as ref; map PayPal item states per kit §2.5 (SUCCESS→settled/final, PENDING/ONHOLD→pending, UNCLAIMED→pending with 30-day expiry to reversed, RETURNED/REVERSED→reversed, FAILED/BLOCKED→rejected). balance() via the PayPal balances API; where that API is unavailable to the account type, return a typed unknown that the settler treats as attempt-and-let-PayPal-answer per obligation and /api/public/solvency publishes as balance: "unknown" — unknown is NEVER treated as sufficient, and nothing is guessed. Fees: PayPal's per-payout fee is borne by the operator/sponsor funding, never deducted from the recipient's amount — a recipient receives the full amountMicro or the obligation stays pending/rejected (the 0%-on-dividends rule binds adapters too). Beneficiary addressing: new payout_channels table (worker_id, channel CHECK('paypal_email'), value, created_at) — payout emails are quasi-PII, deliberately isolated in one deletable table (documented as such, alongside guardian_contact, in the kit §6.3); adapter refuses (honest pending, reason "no payout email on file") when absent. Registry id paypal-payouts. No credentials → adapter constructs but every settle returns pending with reason, exactly like the missing treasury key today. Batching per recipient per day (kit §2.5 fee economics) is NOT in this item — append it as a follow-up backlog item when pilot data justifies it.
- VERIFY: typecheck + boot with adapter selected and no creds → demo completes with pending obligations reasoned "PayPal not configured"; mocked tests for success/pending/reversed/rejected/no-email paths are the required assertions. One real sandbox payout (item id recorded on ledger) only if sandbox creds are present in the run's env; otherwise recorded as an open manual-verification note.
☐ 11a. itsme / EUDI OIDC — the generic authorization-code flow
- WHY: Identity is the legal key that unlocks large payments (IDENTITY_DIVIDEND §4) and the sybil gate is the dividend module's whole integrity story; today the production seam (/api/identity/itsme/callback) is a comment, not code. Split from 11b (rebind + hash upgrade) — the flow alone is a full run.
- WHAT: In src/routes/identity.ts (+ new src/identity/oidc.ts): standard authorization-code flow with PKCE — GET /api/identity/oidc/:provider/start?workerId= (state+nonce stored, redirect to provider authorize URL from env/profile: issuer, client id/secret, redirect uri), GET /api/identity/oidc/:provider/callback exchanges the code, validates the ID token (issuer, audience, nonce, signature via provider JWKS), extracts sub, and calls the existing core verify logic (same salted personHash, same 409 sybil path). Providers itsme and eudi share the generic OIDC client, differing only in config. Ship a mock OIDC provider for tests (test/support/mock-oidc.ts) so the full flow is asserted without itsme credentials — the mock is test scaffolding, clearly labeled, not a fake production path.
- VERIFY: typecheck + boot; test drives start→callback against the mock issuer: worker becomes verified with assurance high, second worker with same sub → 409; bad nonce / wrong audience / expired token each → 400 and no identity row; GET /api/identity/providers reports ready:true iff config present.
- BEWIJSKETEN-toevoeging (founder-vraag 05-07: "anders moeten ze ons op ons woord nemen"): bewaar per verificatie het door de provider GETEKENDE ID-token als verzegeld bewijsstuk — versleuteld at rest met een aparte audit-sleutel, hash-gekoppeld aan de identity-rij, alléén te openen bij een externe audit. Zo draagt elke rij een itsme/EUDI-getekend wortel-bewijs (laag 1 van de bewijsketen, tab /bewijs) zónder de pseudonimiteits-belofte te breken (naam nooit operationeel gebruikt; her-identificatie alleen door de audit-sleutelhouder, bij audit of gerechtelijk bevel): de sub blijft verzegeld en is operationeel nooit leesbaar. VERIFY-extra: audit-tool kan de provider-handtekening van elk verzegeld token valideren tegen de JWKS; operationele endpoints kunnen het token aantoonbaar níet lezen.
☐ 11b. Identity hardening: HMAC person-hash + governed rebind flow
- WHY: Kit §3.1 pins HMAC-SHA-256(key=salt, provider ‖ 0x00 ‖ subject) as the conformance form (delimiter-safe, HSM-native); the current string-concat sha256 must be upgraded before any vector ships. Rebind is the itsme-documented path for re-issued subs and the itsme→EUDI 2027 migration.
- WHAT: (a) Switch personHash to the HMAC construction; existing dev/demo hashes are invalidated — acceptable now precisely because no production identities exist (state that in the migration note; re-run of demo/seed re-verifies). (b) POST /api/identity/rebind: verified re-auth may update person_hash for the same worker (new credential verified in the same session as the old one, old hash row replaced atomically), ledgered as an identity event, but never merges two workers.
- VERIFY: typecheck + full test suite green; HMAC test vectors (published test salt) pass, including the delimiter-ambiguity case the legacy concat form fails; rebind test: same worker rebinds successfully, second worker attempting to claim the rebound hash → 409.
☐ 11c. Mandaten-laag — één mens, meerdere hoedanigheden (founder-vraag 2026-07-04: "je kan ook CEO en mens zijn — hoe houden we dit onder controle?")
- VERRIJKING founder 07-07 (koop-autorisatie/hiërarchie): binnen een bedrijf moet de app de hiërarchie kennen: alleen wie een mandaat kreeg van de bevoegde (CEO/hoofd) kan namens het bedrijf kopen/tekenen — "krachtige mensen laten kopen zonder dat er iets kan mislopen, want zonder autorisatie kóm je er gewoon niet in." Bouwt op de bestaande rol-sleutels (requireRole sponsor/verifier/admin, item 5) + per-mandaat aparte potten; elke mandaat-actie geledgerd (wie-namens-wie, natelbaar). Dit is meteen het B2B-procurement-verhaal: aankoopbevoegdheid technisch afgedwongen i.p.v. per policy-document.
- WHY: Natuurlijk persoon ≠ rechtspersoon. De mens is uniek (1 hash, 1 sleutel); bedrijven bestaan in het ondernemingsregister (KBO) en werken via MANDATEN: "deze geverifieerde mens mag handelen namens BV X" — bewijsbaar, intrekbaar, zoals itsme-for-business vandaag en de EUDI-wallet straks EU-breed. Scheiding zit in de potten (privé-saldo vs. BV-facturatie), nooit in dubbele identiteiten. Heilige regel: dividenden/stemmen tellen uitsluitend natuurlijke personen — een mandaat vermenigvuldigt niemand.
- WHAT: (a) mandates-tabel: person_hash ↔ rechtspersoon (KBO-nr), rol, geldig-van/tot, ingetrokken-op; (b) zakelijke inbox/pool hangt aan de rechtspersoon, ontgrendeld door mandaathouders — geld landt bij de entiteit, geledgerd apart van privé; (c) intrekking werkt onmiddellijk (ontslag = pet weg, mens blijft); (d) dividend-/stem-query's blijven exclusief op natuurlijke personen (bestaande WHERE kind='human' + identiteits-join — test die dit expliciet bewaakt); (e) KBO-verificatie van het mandaat: fase 1 = attest-upload + sponsor-check, fase 2 = registerkoppeling.
- VERIFY: tests: mandaat geeft toegang tot BV-pot en NOOIT een extra dividend-deel of stem; intrekking sluit onmiddellijk; privé/BV-stromen strikt gescheiden in het ledger; jargon-check nul.
☐ 12a. Ledger hardening I: bigint-safe amounts + graded visibility
- WHY: The kit promises these (§4.1, §4.3, §6.2 item 6) and the roadmap previously didn't build them anywhere — a kit promise with no backlog item is exactly the overpromise this file forbids. src/db.ts stores Number(amountMicro); exact only to 2^53. Graded visibility is the difference between a pilot brand and a population-scale surveillance feed.
- WHAT: (a) Migrate ledger.amount_micro (and other money columns) to lossless TEXT bigint encoding with read/write helpers; all arithmetic stays bigint. (b) LEDGER_VISIBILITY = open | graded (profile-settable, published at /api/public/config): open keeps today's full-row public /ledger; graded serves T0 aggregates publicly and the full ledger only via an authenticated auditor endpoint GET /audit/ledger (admin/auditor key now; mTLS is a deployment-boundary concern documented in the kit). (c) Authenticated GET /me/ledger (worker-scoped rows — the citizen's own receipts).
- VERIFY: typecheck + full suite + demo; property test: a 2^60-micro ledger row round-trips exactly; graded boot hides per-row /ledger (401/404) while /config truthfully reports the visibility tier; open boot unchanged.
☐ 12b. Ledger hardening II: merkle checkpoints, anchored roots, standalone verifier — ⬆️ NAAR VOREN (founder-GO 06-07, controlelaag-stelling: "gebruik de keten als controleorgaan, geen eigen uitvindsels"; 12b = de kasboek-verankering die dat waarmaakt; eerstvolgende motor-steen ná de lopende bouworder sprint 2)
- WHY: Kit §4.2 and acceptance row A10 — the mechanism that lets outside observers prove a graded ledger was never rewritten. Certification depends on it; the starter kit's evaluation guide (item 13) must be able to include the drill for real.
- WHAT: Per kit §4.2: canonical leaf serialization (fixed field order, RFC 6962 leaf/node domain separation), ledger_checkpoints table (range_end_id, merkle_root, prev_checkpoint_hash, signed_at, signature) on a configurable cadence; GET /api/public/ledger/root, /proof/:id (inclusion), /consistency?from=&to= (RFC 6962 consistency). Anchoring: on-chain write of the root when a funded treasury exists, else the checkpoint export file — anchoring venues beyond the chain are an operator runbook note, not code. Ship scripts/verify-ledger.ts: standalone (no server imports), validates exported checkpoints + proofs, exits nonzero on tamper.
- VERIFY: typecheck + suite; run demo, checkpoint, export, scripts/verify-ledger.ts passes; mutate one historical row in a copy of the DB → verifier fails loudly; consistency proof across two checkpoints passes and detects an injected reorder.
☐ 12c. Kasboek-loket — "vraag het aan het kasboek" met getelde bewijzen (founder-idee 2026-07-04: "kan ik niet inbouwen dat ze vragen kunnen stellen aan het kasboek?")
- WHY: Openbaarheid zonder telbewijs is een brochure; een loket dat vragen beantwoordt mét reproduceerbaar telbewijs is een rekenmachine — transparantie-als-dienst wordt zelfbediening. Demo-versie draait al (/demo/kasboek: 4 vaste vragen, client-side geteld, met controle-commando per antwoord); dit item bouwt de volwaardige versie.
- WHAT: (a) GET /api/public/telbewijs/:vraag — vaste, veilige vragencatalogus (géén vrije queries): som-per-ronde, 100%-split-toets per gebeurtenis, dubbelpogingen-geweigerd per periode, beloofd-vs-betaald, één-mens-één-deel-toets per dividend; (b) elk antwoord = {vraag, antwoord, telweg (welke rijen, welke som), resultaat-hash, reproduceer-commando} — een geteld bewijs dat je kan doorsturen; (c) per-ronde-attest: sponsor/gemeente krijgt per uitdeling een downloadbaar telbewijs-PDF/JSON voor de gemeenteraad; (d) UI-pagina in mensentaal (uitbreiding van /demo/kasboek naar productie); (e) koppelt later aan 12b (merkle-verzegeling: dan is ook het bewijs zelf on-chain verankerd).
- VERIFY: tests per vraag (bekende dataset → exact antwoord + stabiele hash); split-toets faalt luid op een kunstmatig scheve rij; attest reproduceerbaar met het meegeleverde commando; jargon-check nul.
☐ 13a. Sovereign starter kit I: container + bootstrap
- WHY: The concrete ask in the state track (China 90-day technical evaluation; any ministry's engineers) is "give us something we can run and audit ourselves." (Internal artifact — publishing it anywhere is gated on Franky's GO.) Split from 13b (guide + vectors) — each half is a full run.
- WHAT: Dockerfile (multi-stage, non-root) + docker-compose.yml (app + volume for sqlite) + scripts/bootstrap.ts (npm run bootstrap -- --profile sovereign-generic) which: generates IDENTITY_SALT and admin/verifier/sponsor keys, writes .env, runs seed with clearly-labeled evaluation data, boots, runs the full demo as a smoke test, and prints an evaluation report (endpoints, keys, profile, honest-math snapshot). Repo hygiene the kit promises: commit LICENSE (Apache-2.0), add .nvmrc (Node 20 LTS pin), generate an SBOM (npm sbom --sbom-format cyclonedx in the build), and put the working tree under local git version control (local only — no remote, no publish, standing rule 6).
- VERIFY: docker compose up --build from the repo root on a Docker-equipped host → app healthy on :4021; docker compose exec app npm run demo completes; npm run bootstrap on bare metal produces a working .env + booting server; LICENSE and .nvmrc exist; SBOM generated. If the run's host lacks Docker, the compose steps are recorded as an open manual-verification note and only the bare-metal bootstrap path is checked off — split the item again if needed.
☐ 13b. Sovereign starter kit II: evaluation guide + test vectors
- WHY: The kit (§5.1, §5.3) promises machine-readable vectors and a 90-day protocol an evaluator can run without us; those must exist as files, not prose.
- WHAT: (a) vectors/split.json (remainder cases 1, 3, 7, 999_999, 10_000_001 micro; property: parts sum exactly to input), vectors/identity.json (HMAC vectors with published test salt, delimiter-ambiguity cases), vectors/ledger.json (canonical leaves, 1/2/3/7-leaf roots, inclusion/consistency incl. invalid-proof rejections), and vectors/adapter-conformance.ts (runs against any SettlementAdapter: settle-replay idempotency, balance monotonicity, verifyDeposit rejections, crash-recovery replay, pending→settled supersession) — wired into npm test. (b) docs/EVALUATION_GUIDE.md: week-by-week checks mapped to the kit's §5.2 protocol, each step a copy-paste command with expected output, covering only what exists at this point in the backlog (boot, ledger audit, sybil test, dividend dry-run at population scale, adapter swap exercise, obligation settlement drill, merkle tamper drill from 12b). Salt-custody note per R4M_JOURNEY (state HSM holds the salt; R4M sees proofs, not data).
- VERIFY: npm test runs all vector suites green (usdc-base against a stubbed chain passes the conformance run); every command in EVALUATION_GUIDE.md executed once top-to-bottom against a local boot with outputs matching.
☐ 14a. Sovereign-rail adapters I: regulated local-currency tokens (XSGD/JPYC pattern)
- WHY: The sovereign-generic profile needs a settlement plug that matches how states actually move money. XSGD and JPYC are ERC-20s — the erc20-base adapter with different constants — but JPYC has 18 decimals, which forces the decimals generalization the interface promised.
- WHAT: Registry entries xsgd-like / jpyc-like: erc20-base instantiations driven entirely by profile config (token address, chain RPC env, decimals) — generalize atomic-unit conversion per adapter (currency.decimals, kit §2.1) with property tests; audit every toMicro/fromMicro call site for a hardcoded 1e6 assumption.
- VERIFY: typecheck + suite green; decimals conversion property test (6 vs 18) passes; full parameterized suite green under a stubbed 18-decimals adapter; demo unchanged under usdc-base.
☐ 14b. Sovereign-rail adapters II: instruction-file mode (e-CNY pattern)
- WHY: For a closed rail R4M cannot touch directly, the honest maximum is emitting signed payout instructions for the state's own system to execute — the pending discipline IS the feature.
- WHAT: New src/settlement/adapters/instruction-file.ts: settle() appends a canonical, hash-chained JSON line (beneficiary, amount, currency, source_ref, idempotency_key, prev_hash) to exports/payout-instructions-<date>.jsonl and returns pending with reason "instruction exported — awaiting sovereign rail confirmation"; a new POST /api/admin/confirm-instruction (admin key) records the state's execution reference and settles the obligation. Nothing is ever marked paid on export alone. Verifier script scripts/verify-instructions.ts validates the hash chain.
- VERIFY: typecheck + tests: instruction-file run of the demo yields a valid hash-chain (scripts/verify-instructions.ts passes, and fails when a line is tampered); confirm-instruction settles the obligation and honest-math moves the amount from pending to settled only then.
☐ 15. Robot task-attestation format (owner-mediated, per ROBOT_ECONOMY §5)
- WHY: Robot payroll binds to the OWNER at the contract layer — no manufacturer permission needed. What's missing is evidence discipline: a machine's submission needs a verifiable attestation trail so a verifier (human or agent) can trust physical work, which is what makes robot task volume sellable to sponsors.
- WHAT: Define docs/ROBOT_ATTESTATION.md + zod schema src/attestation.ts: { version, robot: {workerId, handle, hardwareId?}, owner: {handle, signature}, task: {id, claimedAt}, evidence: [{type: photo|sensor_log|completion_check, sha256, capturedAt, uri}], location?: {lat, lon, accuracyM}, signedPayloadHash } — owner signs the payload hash with the robot's registered payout key (EIP-191 via viem, key already on file as payout_address; signature proves control of the earning wallet, which is exactly the owner-mediated model). Extend POST /api/tasks/:id/submit: when the submitting worker kind != 'human', an attestation object is REQUIRED and validated (schema, signature recovers payout_address, task id matches, evidence hashes present); stored in a new attestations table linked to the submission; verdict endpoint response includes attestation validity. Humans unchanged.
- VERIFY: typecheck + boot; demo extended: the robot worker claims a task and submits WITH a valid attestation (demo signs with a viem test key) → approved and split settles to the robot's address; tests: missing attestation → 400, tampered evidence hash → 400, signature from wrong key → 401, human submission unaffected.
☐ 17. Inbox402 email-gateway bridge
- WHY: Inbox402 is live as an API but attention arrives as email; the bridge (per docs/INBOX402.md) is what makes the module usable by a person with a Gmail account — the first R4M surface with proven demand at a public price anchor.
- WHAT: src/routes/inboxBridge.ts: (a) POST /api/inbox-bridge/inbound accepting the common inbound-email-webhook shape (from, to, subject, text — provider-agnostic: works with Cloudflare Email Routing / Postmark / SES payloads via a small normalizer), mapping to's local part to an inbox handle; unknown senders receive a generated challenge reply body containing the x402 payment link/instructions (returned to the bridge caller for the email provider to send — R4M does not send mail itself yet, and never claims delivery it didn't make); senders on a per-inbox allowlist (inbox_allowlist table, managed via authed endpoints) pass through free into inbox_messages with paid_micro=0 clearly marked allowlisted. (b) Per-inbox settings: challenge text template, allowlist CRUD. (c) docs/INBOX402.md gains the copy-paste Gmail/Outlook filter recipe pointing at the bridge.
- VERIFY: typecheck + boot; curl the inbound webhook as an unknown sender → response contains a 402 challenge body with the correct handle price and NO message row created; allowlisted sender → message row created flagged unpaid-allowlisted and excluded from earnings totals (test asserts honest-math/inbox accrual unchanged); paid path still works via the existing x402 route.
☐ 18a. i18n I: infrastructure + EN catalog + app switcher
- WHY: "Any state or country" includes its citizens and its evaluators: the rail is only deployable where it is readable. Split from 18b — infrastructure and translation quality are different risks.
- WHAT: src/i18n.ts with message catalogs under locales/ (en.json complete in this item); locale from Accept-Language → profile locale → en. Migrate user-facing strings to keys: API response error/note/fix texts (grep all routes), the app UI strings in app/ (data-i18n attributes + tiny client loader with a language switcher), and demo-facing texts where user-visible. Ledger event NAMES and JSON keys stay English (they are API surface); numbers/currency formatted per locale in the app only.
- VERIFY: typecheck + boot + demo (EN, unchanged output); catalog-completeness test (every key used in code exists in en.json) green; app renders with zero missing-key fallbacks logged.
☐ 18b. i18n II: NL + FR + DE + EN catalogs (ZH when the China track activates)
- WHY: A Belgian pilot needs ALL THREE official languages — Dutch, French and German (Ostbelgien is literally a launch customer for citizen panels!) — plus English for evaluators (founder 2026-07-02: "er moeten allerlei opties in talen zijn"). The language picker ships in the app UI, and each deployment profile (9a/9c) sets its default locale.
- WHAT: locales/nl.json, locales/fr.json, locales/de.json complete against the key set (en.json from 18a); locales/zh.json moves to the China-track activation moment. Translations must be reviewed-quality, not machine-mangled: every ZH string the routine is not certain of goes into a "needs_native_review" sidecar list, appended as a new backlog item rather than shipping wrong Chinese silently; apply the same honesty to NL for domain-specific legal/financial terms.
- VERIFY: typecheck + boot; curl -H 'Accept-Language: nl' /api/dividends -d '{bad body}' → Dutch error; app renders fully in each language via the switcher with zero missing-key fallbacks logged; catalog-completeness test (all keys present in all three files) green; the needs_native_review item exists in this file if any string was flagged.
☐ 20. GDPR-implementatiepakket (ARTES-module, founder: "ik moet absoluut correct de wereld instappen")
- WHY: Belgisch bedrijf = GDPR vanaf dag één; privacy-by-design is bovendien de pitch. De verklaring en mapping bestaan (v0.18.1); dit item bouwt de RECHTEN als code i.p.v. als mailbox.
- WHAT: (a) POST /api/app/me/:id/verwijder (sessie-gebonden): wist account+e-mail+inbox-koppeling; boekhoudkundig verplichte rijen blijven, losgekoppeld (user_id → NULL + tombstone), geledgerd als account_deleted; (b) GET /api/app/me/:id/export (JSON-download van alle eigen gegevens — overdraagbaarheid); (c) consent-log-tabel voor elk moment waarop de gebruiker iets aanvaardt (registratie, eerste uitbetaling/KYC) met tijdstip+versie van de verklaring; (d) WCAG 2.1 AA-pass over de app (contrast, focus, alt, toetsenbord) + fix-lijst; (e) datalek-runbook in docs (wie meldt wat binnen welke termijn — actuele Omnibus-termijnen verifiëren bij de advocaat); (f) verwerkersovereenkomsten-checklist (Resend, Fly, Cloudflare) afvinkbaar vóór echte lancering.
- VERIFY: typecheck + boot + demo + tests: verwijder-endpoint wist user/e-mail en laat boekhoudrijen los-gekoppeld staan (grep: geen e-mail meer vindbaar); export bevat exact de eigen gegevens en niets van anderen; consent-log schrijft bij registratie; jargon-grep blijft nul; axe/contrast-spotcheck gedocumenteerd.
☐ 21. Verkiezingsgraad-stemverkenning (staande wens founder — géén prioriteit, wél nooit loslaten)
- WHY: "Eén mens = één stem" is de kern-primitief; de verkiezingstrap (treden 1-6 betalen vandaag al) eindigt bij de horizon 2030. Dit item houdt de verkenning levend zonder de bouwrij te blokkeren: eerst weten, dan ooit bouwen.
- WHAT: Een BESLISDOCUMENT (docs/VOTING_EXPLORATION.md), geen code: (a) juridisch landschap digitaal stemmen BE/EU (wat mag vandaag: sociale verkiezingen, ordes, coöperaties; wat vergt wetswijziging); (b) technisch ontwerp-schets: wat er bovenop de uniciteitslaag nodig is voor STEMGEHEIM + E2E-verifieerbaarheid (blind signatures/mixnets — eerlijk benoemen wat we NIET hebben); (c) de trede-1-businesscase sociale verkiezingen 2028 (partner-model met sociale secretariaten); (d) certificerings-/auditpad (welke instantie keurt stemtech). Gate: alleen oppakken als trigger 7 uit SIGNALS_ADAPT afgaat óf een concrete klant (orde/coöperatie) zich meldt.
- VERIFY: het document bestaat, elk juridisch punt heeft een bron, en het bevat een expliciete GO/NO-GO-aanbeveling per trede — geen enkele claim die de journalist-toets niet overleeft.
☐ 21a. OneVote trede-0 — "1 mens = 1 stem = 1 claim, not overridden" als werkende code (founder 2026-07-03: "het eerste autonome voting systeem op basis van x402-verificatie — die voorsprong wil ik behouden")
- WHY: Het stem-primitief is exact wat de motor al kan bewijzen (salted-hash-uniciteit + UNIQUE-constraints + publiek grootboek) — maar er bestaat nog GEEN stem-code, dus de claim "eerste stemsysteem op x402" is vandaag niet waar. Dit item maakt de smalle, eerlijke versie ECHT: geverifieerd-uniek stemmen met publiek telbewijs voor NIET-POLITIEKE stemmingen (burgerpanels, coöperaties, productbeslissingen — dezelfde kopers als markt-map #1/OneVoice). Geen stemgeheim-claim: dat blijft achter het 21-beslisdocument + externe audit (harde regel trigger 7).
- WHAT: (a) votes-tabel (vote_id, vraag, opties, open/close, sponsor) + ballots-tabel met UNIQUE(vote_id, person_hash) — de databank zélf dwingt 1-mens-1-stem af; (b) first-write-wins, never overridden: een tweede poging wijzigt NIETS en wordt geledgerd als ballot_rejected_duplicate (de poging is bewijs, geen fout die verdwijnt); (c) publiek telbewijs: GET /api/public/votes/:id/tally — totalen + aantal geweigerde duplicaten, extern natelbaar tegen het ledger; (d) who-pays: sponsor financiert de stemming (pool-patroon — money before promises), optioneel x402-betaalde stemregistratie voor agent-gedreven panels; (e) jargon-vrij: stemmers zien "je stem is geteld — controleer het telbewijs", nooit hashes/chains; (f) demo-stap: stemming → zelfde mens stemt 2× → tweede geweigerd + geledgerd → tally klopt publiek.
- VERIFY: typecheck + boot + tests: dubbele stem → 409 + ledgerrij; tally = exact aantal unieke geverifieerde stemmers; sponsorloze stemming kan niet geopend worden; demo-lus groen. Real-money-testpad (PRODUCT_REGISTER P5): sponsor-gefinancierde stemming op mainnet-micro na founder-GO. ⛔ publiek endpoint = deploy-gate.
☐ 23. Opt-in gedrags- en dataprogramma's — retailers, verzekeraars, datadelen (founder-ideeën 2026-07-02 nacht; VERKENNING met ethiek- en who-pays-poorten)
- WHY: Drie founder-ideeën in één familie: (1) retail-partnerschappen (Carrefour-achtigen) die BONNEN financieren voor deelname/informatie; (2) beloning voor goed gedrag ("wie zich aan de snelheidslimiet houdt, hou je aan de limiet als je ervoor beloond wordt"); (3) opt-in datadelen waar bedrijven de informatie kopen en de deler betaald wordt. ETHIEK-POORT (eigen ijzeren regel, uit de audit die v1 doodde): betalen voor wat mensen DOEN (opt-in, afgebakend, met betaler-bij-naam) — NOOIT de staat die 'braafheid' beloont (social-credit-grens) en NOOIT passieve surveillance-inkomsten. EERLIJK PLAFOND (HONEST_ECONOMICS, blijft staan): passief datadelen is $2-5/maand — dat verkopen we nooit als €100-moment.
- WHAT: Verkenningsdocument + ledger-entries per programma, elk door het who-pays-filter: (a) Retail-bonnen-partnerschappen: keten financiert bonnen (via 5c-infrastructuur) in ruil voor geverifieerd-uniek panelwerk/aankoopinzichten — de keten is de betaler, de bon de uitbetaling; (b) Veilig-rijden-beloning via VERZEKERAARS (bestaande UBI/telematics-markt: premiekorting voor opt-in rijdata — de private, vrijwillige route naar exact Frankys effect; de staat komt er nooit aan te pas); (c) Opt-in datacoöperatie: afgebakende datasets (bv. energieverbruik, winkelvoorkeuren) die leden bewust delen en die aan kopers-bij-naam verkocht worden, opbrengst naar de delers — met het $2-5/maand-plafond zichtbaar in élke gebruikerscommunicatie; (d) GDPR-laag per programma: aparte opt-in, doel, koper en intrekbaarheid per dataset (register + verklaring bijwerken).
- VERIFY: het document bestaat; elk programma heeft betaler-bij-naam + ethiek-toets (DOEN vs. leven) + eerlijk bedrag met bron; minstens één programma-kandidaat met een echte partner-deur; geen enkele claim die de journalist-toets niet overleeft.
• ❌ ~~25. ⏭ GATED — Kids: spelen-leren-verdienen, opbrengst in de geboortekluis tot 18 (founder-idee 2026-07-03)~~ — VERVALLEN per founder-beslissing 2026-07-06: de kinderkluis is uit de rail; zonder kluis vervalt dit item mee. WHY/WHAT/VERIFY hieronder blijven staan als historiek.
- WHY: Founder: "games voor kids, maar de revenues worden bewaard tot ze ouder zijn" — 100% van wat het kind ZELF verdient → eigen kluis, vrijgave 18 (juridisch ook netter dan uitbetalen aan minderjarigen). VERDUIDELIJKING FOUNDER 3/7 nacht: de kluis is het kind zijn EIGEN verdienrekening, GEEN afhouding op alle platform-flows ("niet iets aparts bij alles wat ik doe opzij zetten"). Toekomst-idee erbij (zelfde nacht): de digitale ID van het kind kan later zijn eigen toestellen omvatten (iPad/gsm vanaf ~15) — opt-in dataprogramma's op die toestellen (item-23-familie) laten het kind dan zelf in zijn kluis verdienen. Poorten: GDPR-kids/ouderlijke toestemming, opt-in per programma, en het eerlijke plafond (passieve toesteldata $2-5/mnd — nooit als meer verkopen). Educatieve spel-taken maken het platform aantrekkelijk voor gezinnen.
- WHAT: VERKENNING eerst (beslisdocument vóór één regel code): (a) ethiek-poort — geen gok-mechanics, geen dark patterns, leeftijdsgrenzen; (b) juridisch — GDPR-kids (ouderlijke toestemming <16 BE), grens spelen-met-spaarpot vs. kinderarbeid; (c) wie betaalt — edu-uitgevers, overheids-leerprogramma's (koppel item 26), gezinsmerken door het who-pays-filter; (d) motor-mapping: vault- en taken-rail bestaan; wat ontbreekt is kids-accounts (gekoppeld aan ouder-account) + spel-schil.
- VERIFY: beslisdocument met bronnen per poort + GO/NO-GO-aanbeveling; gate valt pas na founder-GO én advocaat-check kids-accounts.
☐ 26. AI-leren op R4M — betaalde lessen + overheids-leerprogramma's "geen schrik van AI" (founder 2026-07-03)
- WHY: Founder: "betaalplannen om AI-lessen te geven en leren" + "door het land info hoe ze al lerend verder kunnen gaan en geen schrik moeten hebben van AI" + "overheden moeten hierop inspelen om mensen te motiveren, om te voorkomen dat ze gedemotiveerd raken door AI". Leren en verdienen in één lus. De betaler bestaat langs twee kanten: de cursist (betaalplan) óf overheid/werkgever (opleidingsbudgetten, sectorfondsen, provincies) financiert leer-taken.
- WHAT: (a) verkenning + één pilot-curriculum ("AI gebruiken om te verdienen op R4M" — het platform leert je het platform); (b) leer-taken als task-kind in de bestaande marketplace (voltooiing = kleine vergoeding uit een gefinancierde pool — leren wordt letterlijk beloond); (c) de betaalplan-kant pas na de Stripe-leg (LAUNCH_IN_DAYS dag 2); (d) B2G-verpakking in het 16a-portaal (provincie zet een leerprogramma op — zelfde venue als de sociale-taken-lijn); (e) AI-oefenruimte met échte modellen (founder 3/7: "echte API-keys in de app om mensen echte AI te leren testen"): cursisten proberen echte AI ín de les — maar ALTIJD gemeterd per les en gedekt door het betaalplan of de sponsor-pool (kosten- en misbruikpoort: nooit open sleutels, harde token-limieten per gebruiker), via de item-22-provider-adapter met de GDPR-poort per workload. Raakt de kern niet: het is een taaksoort + een meterstand, geen nieuw geldpad.
- VERIFY: pilot-curriculum bestaat; een leer-taak doorloopt de volledige pool→claim→verdict→obligation-rail in tests; who-pays gedocumenteerd per route; jargon-grep nul.
☐ 27. ⏭ GATED — Steun je artiest: saldo besteden aan artiesten (founder-idee 2026-07-03; zelfde gate als 5b)
- WHY: Founder: "als mensen zelf kiezen om met de centen die ze verdienen bepaalde van hun artiesten te steunen" — besteden houdt geld in het ecosysteem en geeft het saldo betekenis, en dit is Frankys thuismarkt (35 jaar muziek: hij kent de artiestenkant en kan dit verkopen). Mechanisch = 5b (interne besteding) met artiest-profielen; geen nieuwe geld-mechaniek.
- WHAT: na de 5b-gate (ALLOW_BALANCE_SPENDING wacht op de advocaat-perimeter): artiest-inboxen/profielen (geverifieerde artiest = gewone inbox met artiest-label), "steun"-knop = interne betaling over het bestaande 5b-pad, artiest ziet supporter-aantallen maar nooit identiteiten.
- VERIFY: tests op het bestaande 5b-pad met artiest-label; gate-gedrag blijft (flag uit → 403 met eerlijke reden); jargon-grep nul.
☐ 28. ⏭ ZWAAR GATED — Praat-kanaal: psychologische ontlading (founder-idee 2026-07-03; eerst beslisdocument, nooit bouwen zonder founder+advocaat)
- WHY: Founder: "je moet er zelfs tegen gepraat kunnen worden — als mensen zich niet goed voelen, kunnen ze via dit kanaal een soort van psychologische ontlading doen." Menselijk waardevol én het gevoeligste idee van de lijst: alles wat op geestelijke gezondheidszorg lijkt raakt medische regelgeving, de AI-Act (hoog risico) en de eigen grondwet "AI adviseert, mens beslist".
- WHAT: BESLISDOCUMENT, geen code: (a) wat mag (luister-/ventileerkanaal zonder zorgclaims, warme doorverwijzing naar echte hulp — 1813, Tele-Onthaal 106), wat nooit (diagnose, therapie, crisis-afhandeling door AI); (b) veiligheidsontwerp: crisis-signaal → altijd menselijke doorverwijzing, nooit een AI-antwoord alleen; (c) privacy: pseudoniem, nooit trainingsdata, lokaal waar mogelijk; (d) who-pays eerlijk beantwoorden (welzijnsbudgetten/werkgevers — of een gratis nevenkanaal zonder omzetclaim: dan als funnel-idee labelen, per REALITY_FILTER-poort 5).
- VERIFY: document bestaat met bronnen; expliciete NOOIT-lijst; niets gebouwd vóór founder-GO + advocaat-lijn.
☐ 2h. Inbox402 dubbele poort — "alleen geverifieerd-unieke afzenders" per inbox (founder-gat 2026-07-04: de €2-oplichter)
- WHY: €2 betalen is een spamfilter, geen leugendetector. De gerichte oplichter (€2 inzet, €100 buit) wordt pas gestopt door de dubbele poort: betalen ÉN bewijzen dat je een uniek mens bent — dan verbrandt hij bij misbruik zijn éne digitale zelf, met betaald geldspoor. Dit is bovendien exact het rank-3-product (markt-map #3) toegepast op onze eigen inbox — dogfooding van AgentProof.
- WHAT: (a) per-inbox-instelling verified_senders_only (default uit; eigenaar zet aan via bestaand prijs-endpoint-patroon); (b) afdwinging in de paywall-keten: na betaling, vóór levering — afzender zonder geldige personhood-attestatie → 403 met eerlijke uitleg + terugboeking geledgerd (geen betalen-zonder-levering, ook hier); (c) ban-permanentie: inbox-eigenaar kan afzender-hash blokkeren; geblokkeerde hash kan die inbox nooit meer bereiken (UNIQUE-regel, geen nieuw account-truc); (d) consumer-app: schakelaar + uitleg in mensentaal ("alleen echte, unieke mensen mogen aankloppen").
- VERIFY: tests: aan/uit-gedrag, 403-pad met terugboeking exact micro-bedrag, ban blijft over herregistratie heen; jargon-check nul; demo-stap in vitrine-inbox-pagina ("zet de dubbele poort aan").
☐ 2i. Zeggenschap over je geldstromen — pauzeren, weigeren, doorschenken (founder 2026-07-04: "mensen gaan verschieten dat ze bereikbaar zijn voor geld — kan je dit weigeren? of doorsturen naar je kinderen of ouders?")
- WHY: Bereikbaar-voor-geld is voor sommigen een feest en voor anderen een schrik — zeggenschap is de remedie én pure doctrine (de mens is nooit onderdanig aan de rail; meegenomen worden = zelf beslissen). Weigeren en doorschenken maken de rail menselijk: "geld door jou heen naar wie jij liefhebt."
- WHAT: (a) inbox-pauze: schakelaar per inbox ("even niet bereikbaar") — gepauzeerd = 404-klasse antwoord vóór elke betaalvraag (geen betalen-zonder-levering); (b) uitkering weigeren: opt-out-vlag per distributie — geweigerd deel blijft bij de funder of vloeit per zijn keuze naar de pot~~/kluizen~~ (kluis-optie vervallen per 2026-07-06), geledgerd als share_declined (nooit stilletjes herverdeeld); (c) schenk-doorstroom: route je inkomsten (percentage of volledige stroom, per bron) automatisch naar een andere geverifieerde mens ~~of rechtstreeks een geboortekluis in~~ (kluis-doel vervallen per founder-beslissing 2026-07-06 — de kinderkluis is uit de rail) — intrekbaar, geledgerd als gift_forward, met plafond-melding; (d) ⚠️ fiscale kanten van schenkingen → advocaat-lijst vóór activering buiten testnet; (e) consumer-app: drie schakelaars in mensentaal.
- VERIFY: tests: pauze blokkeert vóór 402; declined-share beweegt exact volgens funder-keuze en verdwijnt nooit stil; doorstroom levert bij de begunstigde met correcte ledger-keten; alles intrekbaar; jargon-check nul.
☐ 32. 🎪 DEMO-VITRINE — 🚨 FOUNDER-GO 04-07 ("morgen moeten de apps gebouwd en getest worden for real") → sprint 2+3 hebben VOORRANG op alles behalve money-integrity (4b). Sprint 1 = live (6 demo's; sinds 2026-07-06 nog 5 — D2 Geboortekluis uit de rail). Nu: Witness → PanelProof → DroneSign (sprint 2), dan namespacing + 21a → OneVote → OneBonus (sprint 3). Elke demo: échte flow end-to-end getest op testnet + de VERIFY-eisen hieronder — geen mockups. Mainnet-echte-euro's blijven wachten op founder-funding (aparte GO).
(oorspronkelijke opdracht:) — 10 werkende laag-demo's, elk morgen toonbaar aan een dienst/stad/land (founder 2026-07-04: "ik moet echte pudding verkopen — use this for that and it solves this world problem for good")
- WHY: De motor kan ~80% van dit al; wat ontbreekt zijn dunne, tóónbare vensters per laag. Elke demo = één echte flow op de bestaande motor + één pagina + één koper + één wereldprobleem-zin. Geen mockups: elke knop beweegt echt geld (testnet) en schrijft echt in het publieke kasboek. Publicatie onder de bestaande GO'de fly-app (r4m-franky.fly.dev/demo/*) — géén nieuw publiek oppervlak.
- WHAT (10 demo's, 3 sprints):
Sprint 1 — motor kan het vandaag al (dagen):
(B1) Inbox402 — bestaat live; alleen demo-landingspagina met de curl-402-flow zichtbaar gemaakt. Koper: iedereen. Zin: "spam is dood; aandacht heeft een prijs die naar de mens gaat."
(B2) DataPoort — de bestaande x402-endpoints (/api/data/*) met live 402-demo + agent-betaalknop. Koper: AI-bedrijven. Zin: "agents kopen data per cent — zonder account."
(D1) CitizenRail — stad-stort→dividend-dry-run→publiek telbewijs als wandelbare pagina. Koper: steden/fondsen. Zin: "elke inwoner exact één keer, 0%, natelbaar."
(D2) ~~Geboortekluis — live vault-standen + de 3%-instroom per klus zichtbaar. Koper: steden/ouders. Zin: "niemand start op nul."~~ — ❌ VERVALLEN per founder-beslissing 2026-07-06 (kinderkluis uit de rail; demo-pagina verwijderd).
(E1) RobotPayroll — robot-registry→klus→88/8/4 naar geverifieerde eigenaar (robot-owner-rule draait al in de demo). Koper: RaaS-vloten/makers. Zin: "machines op de loonlijst van de mensheid."
Sprint 2 — kleine delta's (week):
(C1) Witness — sponsor maakt gefunde taak→claim→foto→verdict→uitbetaling, als proper doorloopbare demo. Koper: steden/retail. Zin: "gedekt bewijs ter plaatse, nooit 2× betaald."
(C2) PanelProof — panelzitting met N gegarandeerd-unieke stoelen + weigering van de duplicaat. Koper: onderzoeksbureaus. Zin: "31% van onderzoek is nep; hier 0%." Founder-verrijking 07-07: (i) dedup per enquête via de per-dienst-vingerafdruk — de motor onthoudt DAT deze unieke mens deze enquête al claimde, nooit WIE (enquêtes onderling niet koppelbaar); (ii) "afvinken → uitbetalen" = de bestaande verdict-flow, in twee standen: R4M-als-laag (het panelsysteem van de klant meldt "stoel voltooid" via onze API — dit is de strategie, Go Vocal-model) óf later een mini-vragenlijst-module ín de app (apart bouwsel, pas na de laag-stand); (iii) x402-rol: bureau betaalt per geverifieerde voltooiing over de 402-deur (per-seat-fee), deelnemer-uitbetaling over dezelfde rail.
(E2) DroneSign — bediensessie digitaal ondertekend door geverifieerd mens + publiek sessielog (ledger #11). Koper: vloten/verzekeraars. Zin: "wie vloog daar? Voortaan onweerlegbaar."
Sprint 3 — na namespacing + 21a (weken 1-4-fundament):
(A1) OneVoice/OneVote — burgerbevraging: verifieer→stem 1×→tweede geweigerd + geledgerd→publiek telbewijs. Koper: gemeenten/Go Vocal. Zin: "één burger, één stem — bewijsbaar."
(A2) OneBonus — bonus-claim-check cross-brand (per-tenant namespace). Koper: iGaming. Zin: "63,8% van gokfraude stopt aan de deur."
- VERIFY: per demo: de flow draait end-to-end tegen de echte motor (testnet), de pagina overleeft de jargon-check, elke demo eindigt op het publieke kasboek als bewijs, en de founder kan hem in <3 minuten aan een leek tonen. Elke opgeleverde demo → link in PRODUCT_REGISTER + kaart op de founder-site.
☐ 33. ⏭ GATED — R4M als betaal-laag tussen digitale munten (retail-brug via x402) — founder-idee 2026-07-06
- WHY: Founder: digitale munten zijn nog amper bij de retail-klant geraakt (stilaan wel via digitale euro/dollar); misschien brengt R4M een insteek die dáár van belang is — de uniciteitspoort + eerlijke rail als brug waar gewone mensen veilig met digitale waarde kunnen omgaan zonder jargon. Eerlijke toets vooraf: betaal-bruggen bestaan al (beurzen, PSP's); R4M's smalle claim is niet "wij zijn de brug" maar "elke deelnemer aan de brug is een geverifieerd uniek mens, elke beweging publiek natelbaar, 88/8/4, 0% op distributies". Mogelijke synergie met de x402-agent-tak (agents rekenen af over dezelfde deuren).
- GATES (alle vier vóór één regel code): (a) who-pays-filter met een betaler bij naam; (b) advocaat: MiCA/PSD2-perimeter — dit raakt de kern van "nooit zelf betaalinstelling worden"; (c) ijzeren regel 5 blijft: consument ziet nooit munt-jargon — als dat niet kan voor dit product, sterft het idee; (d) motor eerst bewezen (klantenlijn 16) — dit is een tak, geen koerswijziging.
- WHAT (pas na gates): verkenningsdocument in de studeerkamer: welke retail-pijn lost de uniciteitslaag hier op (fraude bij on/off-ramps? verified-human-limieten per persoon i.p.v. per account?), wie betaalt, en of de bestaande motor-deuren (402 + kasboek + solvency) volstaan als eerste bewijs.
- VERIFY: beslisdocument met winnaar/verliezer-knoop op de tab; geen code vóór founder-GO.
☐ 34. ⏭ GEPARKEERD (founder-beslissing 2026-07-07: "voorlopig gaan we dit niet mengen") — Twee munten naast elkaar in één deployment
- BESLISSING: één munt per deployment blijft de regel — een land kiest euro óf dollar (de plugs bestaan allebei sinds v0.20.0). Mengen misschien later; als er ooit gewisseld wordt, dan als expliciete, benoemde stap via een vergunde wisselaar — mogelijk gewoon als x402-deur op dezelfde rail (de wisselaar draagt de vergunning, wij blijven de rail). Herzien op founder-woord. Addendum 07-07 (founder: "waarom spelen we niet zélf wisselkantoor?"): technisch triviaal, juridisch niet — zelf wisselen = CASP-dienst onder MiCA (vergunning, kapitaal, compliance-apparaat) en breekt de kernlijn "nooit zelf betaalinstelling/uitgever". De winst pakken we wél: het deur-model (vergunde wisselaar huurt de 402-deur, wij nemen commissie op de commerciële flow) — en "zelf de vergunning halen" blijft een schaal-optie voor later (advocaat-vraag 28).
- WHY: Founder: "binnen R4M moet het mogelijk zijn om beide te sturen — euro en dollar." Vandaag kiest een deployment één plug (SETTLEMENT_ADAPTER: usdc-base óf eurc-base; beide gebouwd en getest sinds v0.20.0). Internationale sponsors betalen in dollar, Belgische deelnemers willen euro — één motor die beide rails tegelijk rijdt maakt het landenpakket sterker. Precisering die de claim eerlijk houdt: gereguleerde digitale euro's/dollars (EURC/USDC, MiCA) zijn 1-op-1 gedekt dóór echt geld bij de uitgever — inwisselbaar; R4M dekt zelf niets en wordt nooit uitgever.
- WHAT: (a) settlement-registry laat twee actieve adapters toe; (b) valuta per pool/inbox/obligation (kolom currency, geen impliciete mix); (c) saldo's per munt apart — nooit stilzwijgend converteren (wisselkoers = aparte, benoemde stap of gewoon niet aangeboden); (d) honest-math en kasboek tonen per munt; (e) demo-lus draait één ronde in elke munt.
- VERIFY: typecheck + tests onder beide adapters tegelijk; een euro-pool en een dollar-pool naast elkaar zonder cent-lek; jargon-grep nul; kasboek-regels dragen de munt.
☐ 35. Refunds & terugvordering — de product-flow (founder-vraag 2026-07-07: "kan mijn klant het geld terugvorderen vóór de 90 dagen?")
- WHY: Ja — kaartbetalingen zijn disputeerbaar tot typisch ±120 dagen (chargeback), lós van Stripe's 90-dagen-parkeergrens. De motor boekt terugboekingen al eerlijk (reversed-status, reversedFromWorkersUsd publiek in honest-math — kit §2.2.5), maar de PRODUCT-flow ontbreekt: wat gebeurt er zichtbaar en eerlijk als een sponsor geld terugvraagt?
- WHAT: (a) vrijwillige refund van ongebruikte pool-rest (sponsor vraagt terug → remaining_micro naar obligation "sponsor_refund", geledgerd, kasboek-zichtbaar); (b) chargeback-draaiboek: dispute op geld dat al aan werkers uitbetaald is — wie draagt het (platform-reserve? sponsor-voorwaarden?) → advocaat-lijst vóór activering; (c) UI-taal in mensentaal ("teruggevraagd door de sponsor"), nooit stilzwijgend.
- VERIFY: tests: refund vermindert pool exact en verschijnt in kasboek; dispute-scenario laat werkers-uitbetalingen onaangetast tenzij expliciet beleid; jargon-grep nul.
- TOEPASSING (founder 07-07): verified ticket-buy-back / face-value resale (entertainment-tab). Fan geeft ticket terug → organisator/volgende geverifieerde fan koopt aan face value → geld terug op saldo (instant zodra doorverkocht; bank 1-2 dagen). Elke schakel = bewezen uniek mens → prijs-cap op face value, geen scalp. Differentiator vs DICE/Twickets/Ticketmaster-exchange = personhood op elke schakel + natelbare rail + 0% skim. Bouwt op deze refund/settlement-laag.
☐ 4c. Money-integrity: inbox-levering vs. settle-fout (gevonden 07-07 bij de dataverkoop-bug)
- WHY: x402-express settelt ná de handler. Voor de datadeuren is dit gefixt (boeking pas na bevestigde settle, mét keten-ref in het kasboek). De inbox-deur levert het bericht echter ín de handler (deliverPaidMessage) — bij een settle-fout ná levering is het bericht bezorgd en gesplitst zonder betaling. Zeldzaam (geverifieerde volmacht faalt zelden bij settle) maar niet nul; de getekende volmacht blijft wel geldig en kan her-geseteld worden.
- WHAT: ontwerp-keuze: (a) parkeer de levering tot settle bevestigd is (levering in res.finish zoals de datadeuren, response wordt "aangeboden — levering volgt bij bevestiging"), of (b) lever direct en registreer een her-settle-verplichting op de volmacht (deliver-then-collect, geledgerd). Beide eerlijk in het kasboek; keuze samen met 4b (settle crash-window).
- VERIFY: test met gemockte settle-fout: geen levering zonder boeking of expliciete collect-verplichting; jargon-grep; kasboek toont de waarheid in beide takken.
☐ 36. Publiek "hash-bewijs"-widget — de uniciteits-wiskunde zelf natikbaar (founder-eis 07-07: "ik moet dit live kunnen bewijzen")
- WHY: De sybil-demo toont de weigering; dit toont de motor eronder. Een auditor/commissie tikt een invoer in → ziet altijd dezelfde gezouten hash, één teken anders → totaal andere hash, en dat het onomkeerbaar is. Bewijst tegelijk de eerlijke claim: het is de STANDAARD (SHA-256), niet onze eigen crypto.
- WHAT: kleine publieke demo-pagina /demo/hash.html (naast x402.html): invoerveld → toont hash met een DEMO-zout (nooit het echte IDENTITY_SALT), naast elkaar twee bijna-gelijke invoeren, met de uitleg-zin "dit is de wereldstandaard, wij verzonnen niets". Puur client-side of een read-only endpoint; raakt geen echte identiteiten.
- VERIFY: pagina live, jargon-grep nul, expliciet gelabeld "demo-zout, niet het echte zout".
☐ 37. Consumenten-opdrachtenfeed — "vind je enquête/taak in de app" (founder-inzicht 07-07: de app moet méér zijn dan een goedkeur-portaal)
- STRATEGISCHE KEUZE (founder beslist): twee standen, allebei geldig, allebei deels in de motor. (A) R4M-als-laag — de vraag leeft in het klantsysteem (Go Vocal/stad), R4M levert uniciteit + geld via een link; snelste B2G-verkoop. (B) R4M-als-bestemming — de deelnemer opent de R4M-app en ziet dáár de opdrachten/enquêtes van steden/bureaus die voor hem gelden; dit maakt R4M een eigen merk met terugkerende gebruikers (het "€100-momenten"-verhaal). Waarschijnlijk BEIDE, per klanttype — maar dit expliciet kiezen vóór bouw.
- WHY: de consumenten-app heeft vandaag Saldo/Inbox/Uitbetalen; de taakmarkt draait in de motor (claim → bewijs → verdict → split) maar heeft nog géén consumenten-facing "hier zijn opdrachten voor jou"-scherm. Founder-inzicht: als een stad 1.000 mensen een enquête laat invullen, moet die enquête live in hun net-gedownloade app staan.
- WHAT: (a) opdrachten-tab in de consumenten-app: lijst van open stoelen die voor deze geverifieerde mens gelden (met filter regio/type/vergoeding); (b) invullen-in-de-app óf doorlink naar het klant-formulier (afh. van stand A/B per project); (c) claim-flow zichtbaar in mensentaal: "stoel geclaimd", teller, en na voltooiing/betaling verdwijnt de opdracht uit je lijst (bestaat al in de motor — één stoel, geclaimd=vast, betaald=weg, TTL-vrijgave; hier alleen de UI); (d) PWA-notificatie "nieuwe betaalde opdracht in jouw regio" (opt-in).
- NOORDERSTER-NOTITIE (founder 07-07): dit item is het embryo van de lange-termijn-visie — een sociaal platform waar mensen verdienen aan hun momenten van waarde (betaalde berichten, offerte-oproepen, panels, taken, rechten-radar) en waar níémand met de pot kan weglopen omdat de split publiek en statutair vastligt. Sociale-app-status blijft: ONDERZOEK, engine eerst (founder-beslissing 02-07). Elke earn-piste (44/45/…) is een module die deze feed later voedt — geen aparte bouwsels.
- VERIFY: een geverifieerde testmens ziet alleen stoelen die voor hem gelden; claimt → stoel weg voor anderen; voltooit → betaald + verdwenen uit zijn lijst; dubbel claimen geweigerd; jargon-grep nul.
☐ 38. Projectconfiguratie-laag — selectie-standen, quota, en genodigden ZONDER dat R4M namen ziet (founder-inzicht 07-07)
- ⚠️ DOCTRINE-VAL (eerst lezen): de founder-wens "steden moeten mensen/identiteiten kunnen invullen, misschien uit lijsten" botst frontaal met de kern "uniciteit bewaard; geen naam operationeel bewaard, identiteitsbewijs verzegeld en alleen-audit". R4M mag NOOIT een namenlijst/ledenlijst/rijksregisternummers van een stad opslaan — dan zijn we plots een PII-verwerker en is de hele privacy-belofte dood. De genodigden-stand moet dus PII-vrij: de stad nodigt uit met (a) éénmalige uitnodigingscodes/tokens die zij verdeelt (wij zien alleen "code X is ingewisseld door een geverifieerd uniek mens"), of (b) een allowlist op HASH-niveau (de stad hasht haar eigen lijst met hetzelfde zout-mechanisme; wij matchen hashes, zien nooit namen). Nooit een naam-upload.
- WHAT: (a) selectie-stand per project als config: first-come / loting (sortition, seed publiek natelbaar) / genodigd; (b) quota (per wijk/leeftijd/taal — als attribuut zonder identiteit, of via de invite-code die de stad zelf aan een categorie koppelt); (c) invite-token-mechanisme (genereer N codes, eenmalig, verlopen) + hash-allowlist-optie; (d) alles in het sponsor-portaal, in mensentaal; (e) certificaat toont "N genodigden, N unieke deelnames, N dubbelpogingen geweigerd" zonder één naam.
- VERIFY: een genodigden-project werkt end-to-end met codes/hashes; grep bewijst dat er nergens een naam/e-mail/rijksregisternummer van een deelnemer in de DB belandt; loting is reproduceerbaar (publieke seed); jargon-grep nul.
- 38b. Attribuut-band bij verificatie (founder-strikvraag 07-07: "€500 voor elke 16-jarige — hoe weet de stad dat de juiste mensen inschrijven?"): op het eID-moment leest de motor het kenmerk (geboortejaar→leeftijdsband, gemeente→woonplaats-ja/nee), slaat alléén de ja/nee-band per project-namespace op en gooit het kenmerk weg — "kenmerk gecheckt, kenmerk weggegooid" (zelfde patroon als de identiteit; EUDI selective disclosure maakt dit later nog schoner). Certificaat wordt: "M inwisselingen, elk uniek mens, elk leeftijd✓ woonplaats✓". CHECK vóór bouw: minimumleeftijd van de wortel zelf (itsme-leeftijdsvloer!) + uitbetalen aan minderjarigen (ouderlijk mandaat?) = advocaat-vraag 61 (arbeid-variant = vraag 53).
☐ 39. Meertaligheid (i18n) — de app + portaal + templates in meerdere talen (founder-inzicht 07-07: "dit zal in verschillende talen moeten")
- WHY: een landenpakket (BE→EU→...) is per definitie meertalig; een Brusselse bevraging alleen al vraagt NL/FR (+ vaak EN). Vandaag zijn de strings NL-first, EN-tweede en verspreid — dat schaalt niet naar tien talen.
- WHAT: (a) strings uit de code halen naar één berichten-bestand per taal (nl/fr/en/de… ), UI kiest op basis van gebruiker/deployment; (b) de projectsjablonen (item 16a-b) meertalig maken zodat een klant zijn bevraging in de juiste taal aanbiedt; (c) taal per deployment/land als default + per gebruiker overschrijfbaar; (d) e-mails/foutmeldingen mee (de NL-fout-fix van 06-07 wordt dan i18n-gestuurd). Geen automatische AI-vertaling van juridische/geld-tekst zonder menselijke check.
- VERIFY: app draait volledig in NL én FR zonder hardcoded string; taal wisselen verandert alles incl. foutmeldingen; jargon-check per taal.
☐ 40. Sterke projectsjablonen-bibliotheek (founder-eis 07-07: "mijn templates zullen heel sterk moeten zijn")
- WHY: de klant (stad/bureau) moet in minuten een goed project kunnen opzetten zonder de founder. Referral-sjablonen bestaan (knelpuntberoepen); bevraging/panel/foto-taak-sjablonen ontbreken nog (was al 16a-b).
- WHAT: bibliotheek van kant-en-klare sjablonen per use-case (burgerbevraging, panel N-uniek, schadefoto, veldwerk, premie-uitdeling), elk met: standaard bedrag-range, selectie-stand-default, benodigde bewijsstukken, en meertalige teksten (koppelt aan 39). Sjablonen = data, niet code, zodat de founder/klant ze kan uitbreiden.
- VERIFY: een niet-technische klant zet in een testsessie een volledige bevraging op vanaf een sjabloon; jargon-grep nul.
- 40b. Launch-toolkit per project (founder wie-waar-wat-hoe, 07-07): kant-en-klaar communicatiepakket dat de KLANT uitrolt via zíjn kanalen (brief/mail/social/persbericht-sjablonen in te gieten in eigen huisstijl, QR+deelname-link per project, loket-draaiboek voor baliehulp aan digitaal kwetsbaren, helpdesk-afspraak lijn 1 = klant / lijn 2 = wij, telbewijs-dashboard per ronde). Communicatie-annex hoort in het parameter/GO-document (advocaat-vraag 11): wie communiceert (de klant), via welke kanalen, met welk materiaal (het onze). Digitale inclusie is onderdeel van het pakket — een stad mag niemand uitsluiten (gelijkheidsbeginsel), dus loket-assistentie + papieren-code-optie horen erbij.
☐ 41. Multi-platform: één app op Android/iOS/iPad/Windows/Mac — PWA-first, native-wrapper later (founder-eis 07-07)
- GOEDE STAND (eerlijk): R4M is vandaag al een PWA — dat draait NU al op álle vijf platforms vanuit ÉÉN codebase: installeerbaar op beginscherm (Android/iOS/iPad) en als desktop-app (Windows/Mac), met automatische updates en geen app-store-poortwachter. "Vijf aparte native apps bouwen" is NIET nodig en zou vijf keer onderhoud zijn.
- ⚠️ COMMISSIE-VAL (advocaat/store-policy-check): een échte listing in de Apple App Store / Google Play brengt hun regels mee — Apple/Google kunnen 15–30% eisen op "in-app aankopen". Voor R4M is dit vermoedelijk oké (uitbetalen naar de bank is géén digitale aankoop; person-to-person/diensten zijn doorgaans vrijgesteld) — maar dat MOET bevestigd worden vóór een store-listing, anders eet een platform 30% van de flow. Op de advocaat/store-lijst.
- WHAT (gefaseerd): (a) PWA hardening: manifest + icons + offline-shell + "toevoegen aan beginscherm"-prompt (grotendeels er al) — dekt alle vijf platforms meteen; (b) later, ALLEEN als store-aanwezigheid nodig is voor vertrouwen/vindbaarheid: native wrappers om dezelfde web-app (Capacitor voor iOS/Android, Tauri voor Windows/Mac) — één codebase, dunne schil, geen herbouw; (c) store-commissie-check afgerond vóór (b).
- VERIFY: de PWA installeert en draait op Android, iOS, iPad, Windows én Mac vanuit één build; store-beslissing pas na de commissie-check.
☐ 42. Games — personhood + settlement-laag voor game-economieën (founder-inzicht 07-07)
- WHY: games krijgen steeds meer échte waarde in-game (items, identiteiten, player-economieën); hun grootste plaag is bots/multi-accounting/gold-farming. R4M's uniciteitspoort + settlement-rail lost precies dat op. Grote, groeiende markt.
- VEILIGE SCOPE (bouwbaar op de bestaande rail): één-mens-één-account (anti-bot/anti-farm), geverifieerde speler-marktplaats (P2P item-trade tussen bewezen mensen, 0% van spelersgeld), fair drops/launches, leeftijdsverificatie zonder identiteit te bewaren, esports-prijzengeld & creator-payouts natelbaar.
- ⚠️ GATED VISIE-DEEL (apart, nooit als eerste pitchen): "echte-geld-waarde in-game / >€100k via x402 / Second-Life-economie" raakt de Belgische KANSSPELWET (loot boxes zijn in BE verboden kansspel sinds 2018), RMT-verboden in game-ToS, MiCA (is het item een crypto-asset?) en consumentenbescherming. Beslisdocument + advocaat vóór één regel code aan dit deel. Frame R4M NOOIT als "koop tokens om mee te schieten".
- VERIFY: verkenningsdocument (veilige scope vs gated visie), who-pays (game-studio/marktplaats), advocaat-vragen (kansspelwet, RMT, MiCA) toegevoegd; geen build op het gated deel vóór GO.
☐ 43. R4M MCP-server — de poorten als agent-gereedschap (founder-vraag 07-07: "is dit een API/CLI/MCP-product?")
- WHY: het agent-tijdperk consumeert diensten als tools (MCP). Een R4M-MCP-server maakt elke poort direct vastpakbaar voor élke AI-agent: verifieer-personhood-status, betaal-402-deur, lees-kasboek/telbewijs, claim-stoel. De API bestaat al — dit is dunne verpakking met grote vindbaarheid, en het versterkt de verdedigbare claim ("x402 betaalt uit aan bewezen mensen — en agents kunnen het als gereedschap gebruiken").
- WHAT: MCP-server (read-first: kasboek/telbewijs/status publiek; betaal-tools met x402-flow; NOOIT sleutels van gebruikers aannemen — de agent brengt zijn eigen betaalcapaciteit mee), + een nette CLI-wrapper voor developers/demo's. Jargon-regels gelden ook voor tool-descriptions.
- VERIFY: een agent-sessie kan via de MCP-server het kasboek lezen en een 402-deur betalen op testnet; tests; jargon-grep op tool-descriptions.
☐ 44. Inbox402 offerte-/reclamemodus (founder-piste 07-07: "bedrijven sturen jou een offerte mits ze €2 betalen")
Mechaniek bestaat al (inbox is afzender-agnostisch; agent-betaalt-mens $2 live bewezen 07-07). Te bouwen als modus, ná launch-poort: (a) zelfgekozen interesse-labels (opt-in, gebruiker beheert — NOOIT profielen verkopen, doctrine), (b) omgekeerde offerte-oproep ("ik zoek zonnepanelen — offertes welkom deze maand, $X per offerte" = betaalde leads, omgekeerd) — PRIMAIRE variant (founder 07-07: "dit is de echte goudmijn"; eigen product-rij: P11 LeadFlip in PRODUCT_REGISTER); farming-risico expliciet ontwerpen: cap per categorie per person_hash (onze sybil-laag maakt caps afdwingbaar — concurrenten niet), earn deels op antwoord/engagement i.p.v. alleen ontvangst, conversie-feedback voor bedrijven, (c) afzender-type "bedrijf" met naamvermelding. GATE: advocaat-vraag 32 vóór commerciële uitrol.
Founder-scherpstelling 07-07 (de consent-pool — dit wordt de kern): persoon geeft ÉÉN keer consent via R4M ("ik laat betaalde promoties toe in pool X") → R4M is het consent-register: bewijsbaar (timestamp, geverifieerd uniek mens), granulair (per pool/categorie, zelf gekozen), altijd intrekbaar (één klik). Dit lost het ePrivacy-probleem aan de wortel op: geen per-bericht-discussie maar klassiek opt-in, netjes gedocumenteerd. Adverteerder (bedrijf/overheid) koopt bereik in de pool ZONDER identiteiten te zien: hij krijgt levering + bewijs ("N unieke, toestemmende, echte mensen bereikt"), nooit data. Betaald per AFGELEVERD bericht (niet "per bekeken" beloven — kijk-verificatie is een bekend moeilijk probleem); 88% naar de persoon. Het adverteerder-risico ("werkt het?") blijft bij de adverteerder — normaal reclamerisico — maar zijn FRAUDE-risico verdwijnt: geen bots, geen dubbele mensen, structureel (sybil-weigering). Bestaand bewijs dat dit model werkt: de panel-industrie (Dynata/Cint-klasse) doet exact dit voor marktonderzoek, miljardenmarkt — wij voegen toe wat zij niet kunnen: bewijsbare uniciteit + 88% naar de mens. Overheid-variant: publieksinformatie-campagnes (vaccinatie, premies) naar geverifieerde burgers.
☐ 45. Rechten-Radar (founder-piste 07-07: "op de R4M-site per regio tonen waar je misschien recht op hebt")
Het antwoord op de eigen tegenwerping "misschien is dat nutteloze reclame": geen promoties maar RECHTEN — premies, toeslagen, sociale tarieven die mensen laten liggen (non-take-up = gedocumenteerd beleidsprobleem; Vlaanderen heeft er zelfs een agenda voor: automatische rechtentoekenning). Doctrine-veilig ontwerp: gebruiker kiest zélf regio + labels (geen PII, geen verificatie van inkomen door ons); later EUDI selective disclosure ("bewijs kenmerk zonder identiteit") = killer-use-case voor de Europa-tab. CLAIM-DISCIPLINE: rechtenverkenner.be (Vlaamse overheid) bestaat al als opzoek-site — wij zijn NIET de eerste vindmachine; onze wig = de afleverrail (consent-pool 44: push naar toestemmende geverifieerde unieke mensen, met telbewijs voor de overheid) + de uitbetaalrail (recht gevonden → geld ontvangen op dezelfde rail). Past "de weg naar je eerste €100" letterlijk: de radar toont waar jóúw honderden euro's liggen. WHO-PAYS nog niet gepasseerd (kandidaat: overheids-uptake/campagnebudget, uitvoerende bedrijven duidelijk gelabeld) → eerst payer-onderzoek, dán pas PRODUCT_REGISTER-rij. Ná launch-poort.
☐ 46. R4M Pass op het open web — "bewijs dat je een echt mens bent" als badge/widget/Chrome-utility (founder-piste 09-07, geboren uit de eerste eID-verificatie)
Het idee: de Pass-laag breekt uit de app — een gebruiker kan op ELKE website of webgroep zijn R4M-pas tonen ("achter dit account staat één bewezen uniek mens") zonder zijn identiteit prijs te geven. Vormen: (a) verify-API/widget voor websites (forums, marktplaatsen, comment-secties, datingsites — de site betaalt per check, zelfde model als PanelProof P7), (b) Chrome-extensie als distributievorm (gebruiker toont zijn pas waar hij wil), (c) "R4M-verified"-badge met publiek natelbaar telbewijs. App-koppeling: dit ís de Pass-laag (P7-familie, VERIFICATION_MARKET_MAP) — geen nieuw bouwsel maar een nieuw afzetkanaal voor de bestaande motor. CLAIM-DISCIPLINE (eerlijke nuance, ook in elke pitch): de pas bewijst dat het ACCOUNT aan één bewezen uniek mens toebehoort — niet dat de persoon die op dít moment typt die mens is (sessies kunnen gedeeld/gekaapt worden). De zin die klopt: "achter dit account staat één bewezen uniek mens die ervoor instaat." Vijandenkaart: World ID doet exact dit met biometrie — onze wig is de eID-wortel (bank-grade, GDPR-schoon, geen iris-scans) + EU/MiCA-vertrouwen. WHO-PAYS nog niet gepasseerd (kandidaat: platforms betalen per check of per maand per badge) → eerst payer-onderzoek, dán PRODUCT_REGISTER-rij. Ná launch-poort; de teller (24b) is de marketing-voorloper van ditzelfde verhaal. Dossier uitgewerkt 09-07 (cijfers+use cases+vijandenkaart+EN-pitch): tab → /webpass — kernfeit: bots werden in 2026 de meerderheid van het internetverkeer (Cloudflare 57/43); agent-mandaat-regel bestaat al in de motor (robot-via-eigenaar, demo stap 7).
☐ 47. Gate402 — de checkpoint-poort: betaald (x402) + écht uniek mens (Pass) + alles geboekt (kasboek) in één doorgang (founder 09-07: "dit geeft te veel macht — apart uitwerken") ⏭ GATED
Eén poort vóór eender welke toegang (API-gateway, kiosk, tourniquet, machine): per doorgang twee sloten (betaald? + écht?) en elk event in het eerlijke kasboek = het controlepaneel dat de klant koopt (wie/wat/wanneer/betaald/geweigerd, spend-caps, kill-switch, batch-controle vóór uitbetaling). Bouwstenen bestaan alle drie (x402-paywall P1, Pass+robot-owner P3, kasboek); nieuw = poort-SDK/endpoint + controlepaneel-UI + caps/kill-switch. Register-rij P13 (werkcode Gate402, naam = founder), dossier op de tab: /gate. GATE: ná de launch-poort (19c) + eerste betalende poort-klant bevestigd (who-pays) — geen bouw vóór een naam op de factuur. Testpad bij geboorte gedefinieerd (register): testnet 3-afnemers-scenario (mens ✓ / agent-mét-eigenaar ✓ / agent-zónder ⛔) → mainnet-micro na GO → eerste klant-euro via x402-facturatie zelf.
• ~~Kluis-aandeel in de consumer-split (gesteld 2026-07-03 nacht): de founder wil géén automatische afhouding-op-alles; de kluis is een STARTKAPITAAL-rekening die ouders/sponsors/het kind zelf/de staat met naam vullen ("tegen 18 of 21 letterlijk starten met een mooie zakcent" — zie item 31, verdienkant in item 25). De 3% vault-share is vandaag al een config-knop (SPLIT_VAULT_BPS, som moet 10000 zijn). Vraag: zetten we de consumer-launch-default op 0% kluis (bv. 88/8/4/0 — werker houdt 88%) en houden we het kluis-aandeel als OPT-IN feature per deployment/pool (het Connecticut-Baby-Bonds-verhaal dat een stáát kan aanzetten en financieren)? Wacht op founder-GO — de split is een publieke belofte, dus teksten/pitches bewegen mee.~~ → ✅ BESLIST 2026-07-06 (founder): de kinderkluis is volledig uit de rail — split 88/8/4 (de 3% naar de verdiener); het kluis-mechanisme is geen R4M-product meer (items 25 en 31 vervallen). Motor, tests, app en demo's zijn aangepast; teksten/pitches bewogen mee.
End of backlog v2 (reviewed 2026-07-02; VERSION and package.json reconciled by item 1 and bumped together by npm run release from now on). Routines: remember rule 5 — what you discover while building belongs in this file, at the priority its risk deserves.
Bron: docs/BUILD_ROADMAP.md (privé-repo) — deze pagina wordt bij elke wijziging opnieuw gegenereerd via build-docs.py.