Referral credits without invented money logic.
The ticket asks for a referral feature. The hardened packet spots stored value, ownership, erasure, an undefined trust score, and an audit trail that cannot prove itself.
One paragraph. Five systems.
Add a referral program to the app.
When a user refers a friend, both of them get 500 credits. Credits work like our normal balance so people can spend them on anything. Show a trust score on the referrer's dashboard so we can spot abuse—people making fake accounts to farm credits. If the trust score is low, hold the credits.
Users can erase their account at any time, and referral history has to stay intact for the audit trail so we can prove what happened if someone challenges a payout. Log everything.
Credits should only be released once the friend actually pays for something, so we do not get farmed.
Acceptance: referral program works, abuse checks are in place, audit trail exists. Need it this week.
142 words. Reads like one task. It is five.
Read this before anything runs.
CROWN. This ticket reaches stored value. Credits spend like currency, which makes them a liability on the books. It also reaches ownership and erasure.
No packet below auto-runs. The agent writes nothing until the owner approves the split. Money-adjacent packets carry a Money Truth assertion: the ledger must reconcile against the balance the user sees.
A PROGRAM WEARING A TASK'S CLOTHING
Two new stores, one scoring surface, one audit log, one dashboard surface, one release rule, and one erasure path cannot share one red test. Split the work before an agent silently chooses the order.
| # | Packet | Exposure | Needs | Why here |
|---|---|---|---|---|
| A | Append-only credit ledger | Crown · money | None | Nothing else is safe on a mutable balance column. |
| B | Referral record + ownership | Crown · ownership | A | Needs a ledger to point at. |
| C | Release after first cleared payment | Crown · money | A, B | Resolves grant versus release. |
| D | Trust score | None | B | Advisory only after money is already held. |
| E | Erasure against retention | Crown · ownership | A, B | Separates identity from retained events. |
This packet covers A only. B through E follow one at a time, in that order.
Facts the agent is not allowed to invent.
| # | Fill from live files | Why it matters |
|---|---|---|
| 01 | ORM / query layer | Sets the real implementation shape. |
| 02 | Migration tool + exact command | Stops an invented production command. |
| 03 | Test runner + exact command | The red test needs a command that runs. |
| 04 | Existing balance table and column | Names today's source of truth. |
| 05 | Every current balance writer | A second writer breaks the invariant. |
| 06 | Auth middleware path | Identity comes from the session. |
| 07 | Ownership helper path and signature | Blocks client-supplied ownership. |
| 08 | Credit precision and unit | Money math never rests on a float guess. |
| 09 | Runtime database role and grants | Append-only must hold at the database. |
| 10 | CI configuration path | The named test must run on every push. |
Ten fills. Every one marks a place where an agent would otherwise invent something plausible and move on.
Four failures that survive a normal read.
3.1 · Self-contradiction — two found
“Both users get 500 credits” conflicts with “release credits only after payment.” Resolve it as two events: grant held credits at referral, release them only after a cleared payment. Held and spendable remain separate facts.
“Erase the account” conflicts with “keep referral history.” Store a pseudonymous account reference in the ledger and keep identity in a separate map. Erasure severs the map while events remain provable.
3.2 · Undefined number — found
“Trust score” has no inputs, range, threshold, formula, or zero case. Packet D must return the full formula or show the underlying signals with no invented score. A number without a formula is an agent's opinion wearing a decimal point.
3.3 · Unfalsifiable criteria — three found
| Original | Named test | Goes red when |
|---|---|---|
| Audit trail exists | ledger.append_only.spec | The runtime role can update or remove a ledger row. |
| Referral program works | ledger.reconciles.spec | Event sum differs from the balance shown. |
| Abuse checks are in place | referral.hold_release.spec | Held credit becomes spendable without a cleared payment. |
3.4 · Self-reported control — found
An ordinary audit table is not proof. If the application can write it, the application can rewrite it. Make the ledger append-only at the database level. Grant INSERT and SELECT to the runtime role; revoke UPDATE and DELETE. Hash-chain each event to its predecessor so a removed row leaves a visible break.
The sweep catches what the ticket never names.
| Smell | Present | Resolution |
|---|---|---|
| Client-supplied identity | Yes | The referrer comes from the authenticated session, never the request body. |
| Mutable audit log | Yes | Database grants and a hash chain enforce append-only history. |
| Free-string owner | Likely | Use a foreign key to the pseudonymous account reference. |
| Constant copied per row | Possible | Keep program-wide constants outside event rows. |
| Orphan interface | Yes | Ship the dashboard score with packet D, never before its truth exists. |
The work, stated without the hidden program.
Every change to an account's spendable amount becomes an immutable event. Compute the balance from events. Never store it as the only mutable truth.
Event types: GRANT_HELD · RELEASE · SPEND · REVERSE · ADJUST
Each row carries a pseudonymous account reference, signed amount in the approved unit, event type, cause reference, timestamp, and previous-event hash.
ledger.append_only.spec
GIVEN a ledger row written by the runtime role WHEN that role attempts UPDATE on the row THEN the database refuses the statement AND when that role attempts DELETE THEN the database refuses the statement AND when a stored previous-hash changes out of band THEN chain verification reports the first broken row
Money Truth assertion
GIVEN an account with at least one ledger event WHEN signed event amounts are summed THEN the sum equals the spendable balance shown to that account AND held value remains separate from spendable value
The loop cannot go green while those facts drift. If either test passes before the work exists, the test is wrong—fix the test first.
Written before work starts.
- Turn
ledger.enabledoff. Reads return to the existing balance source named by fill 04. - Keep the ledger table. A rollback never destroys evidence.
- Reverse the new runtime grants with the reviewed migration.
- Stop dual-write at the flag so the prior balance remains authoritative.
- Trigger: any production reconciliation mismatch. One row is enough.
READINESS — 6 / 10
Ten named fills remain for the owner to answer from live files.
| Section | Score | Evidence |
|---|---|---|
| Exposure classified | 1 / 1 | Crown—money and ownership. |
| Size and order | 1 / 1 | Five packets with prerequisites. |
| Fills named | 1 / 1 | Ten, each addressed to the owner. |
| Fills answered | 0 / 2 | Live repository pass still required. |
| Failure classes resolved | 1 / 2 | Two resolutions rest on live fills. |
| Smells swept | 1 / 1 | Five present, each routed. |
| Red test runnable | 0 / 1 | Needs fill 03's real command. |
| Rollback written | 1 / 1 | Trigger included. |