2 August 2026. The developer's document. Tables, schemas and enums. Synced to the 2 Aug spec revision: phase-one community layer, 4.7 evidence ladder, and the founding-corpus seeding model.
This document is the how. The specification is the what and the why. Where a rule needs a reason, this document points to a chapter in the specification (written spec 4.3) rather than repeating it. Where a launch number is needed, it is here. Where this document and the specification prose disagree, this document and the specification's chapter 14 contract win together, and chapter 14 wins over this document.
Build order and the architecture principles are in spec 12.
A slice is one casino, one payment method, one net-position bucket, one amount band, one VIP flag. Every threshold, median, count, cap and trend is per slice (spec 14.1). The 5-distinct-member gate lives in spec 14.2.
slice_key = (casino_id, method, net_position, amount_band, vip_at_time)
Amount band and VIP are part of the key but open progressively: a figure shows only once its full slice clears the gate, and coarser splits (method, net position) show first while finer ones fill.
The Overdue benchmark is the one exception: computed on (casino_id, method) only, so a live record can be judged before its finest slice fills.
The store is organised around entities, not tasks. An entity gets its own table if you can ask a useful question of it without joining to something else (spec 12).
| Entity | Owns | Children |
|---|---|---|
| Member | A person. Auth, verification tier, level. | Account links, sessions |
| Casino | A brand. Provisional or confirmed. | Passport fields, terms snapshots, records |
| Operator | The company behind one or more casinos. | Casino claims |
| Record | One logged withdrawal. | Payment legs, evidence, endings, written account, flags |
| Ledger | Tester payments. Append-only. | Payout requests |
| Comment | A community post or reply. Screened like any player text. Never touches a number. | Votes |
| Vote | One up/down per member per comment. Verified-player only. |
Casino and Operator are separate tables, joined by a dated claim. The measurement lives on the brand and must never touch a commercial account (spec 6).
Exactly one state per record, persisted. Payment states (WAITING, PART_PAID, PAID) are recomputed from the legs when a leg changes. The other states are set by their transition event or by the sweep in 12b, and are stored, never re-derived from legs.
| State | Live/Terminal | Counter | Counts as unpaid | In payout median | In denominator |
|---|---|---|---|---|---|
DRAFT |
Live | none | no | no | no |
WAITING |
Live | Still waiting | yes | no | no |
PART_PAID |
Live | Part paid | yes, outstanding part | only when complete | when complete |
PAID |
Terminal | Paid | no | yes | yes |
METHOD_CHANGED |
Terminal | Method changed | no | no method median | yes |
REFUSED |
Terminal | Refused | no, counted separately | no | yes |
CANCELLED |
Terminal | Cancelled | no | no | yes |
ACCOUNT_CLOSED |
Terminal | Account closed | yes, separate | no | yes |
CLOSED_SHORT |
Terminal | Closed short | yes, refused part | no | yes |
ABANDONED_UNPAID |
Terminal | Gave up waiting | yes | no | yes |
UNCONFIRMED |
Terminal | Unconfirmed | no | no | yes |
Every record sits in exactly one counter. PART_PAID is never split between Paid and an outstanding bucket (spec 4.1).
Flags sit on top of the state. disputed and removed are the two editorial flags. overdue is a third boolean, but it is sweep-computed from the clock, not set by a person (spec 4.1).
| Flag | Effect on state | Effect on numbers |
|---|---|---|
overdue |
none, it is a property of a live record | the record shows as overdue; sets ever_overdue once, which the silence rules read |
disputed |
none | none, the flag is shown |
removed |
none, state retained underneath | drops out of every count and median |
Overdue is a property, not a state. Any live record (WAITING or PART_PAID) whose elapsed time passes 1.5x its benchmark carries overdue = true, computed each sweep. It never competes with PART_PAID. ever_overdue latches true the first time and is what distinguishes Abandoned from Unconfirmed on silence.
| From | To | Trigger | Guard |
|---|---|---|---|
DRAFT |
WAITING |
player confirms details | evidence present or fields typed |
WAITING |
PART_PAID |
a leg arrives, balance still positive | |
WAITING/PART_PAID |
PAID |
player confirms arrival, balance zero | |
PART_PAID |
CLOSED_SHORT |
casino refuses the remainder | refusal artifact |
PAID |
WAITING |
payment bounced or clawed back after being marked paid | reversal leg recorded |
WAITING |
REFUSED |
casino declines before any leg, artifact recorded | refusal artifact. A PART_PAID refusal goes to CLOSED_SHORT. |
| any live | CANCELLED |
player reverses the withdrawal | |
| any live | ACCOUNT_CLOSED |
player locked out, withdrawal outstanding | own outcome only if evidence artifact attached, member verified, and link predates the withdrawal; otherwise the state sets but the record is buried and excluded from aggregates (spec 4.8) |
| any live | METHOD_CHANGED |
balance reaches zero via a leg whose method differs from the requested method | derivation compares leg method to requested method; same method to zero is PAID |
WAITING/PART_PAID |
UNCONFIRMED |
21d silence and ever_overdue = false and never said outstanding |
|
WAITING/PART_PAID |
ABANDONED_UNPAID |
21d silence and (ever_overdue = true or said outstanding) |
|
REFUSED |
PAID |
escalation, casino pays | payment artifact |
Overdue is not a transition. The sweep sets overdue and latches ever_overdue on any live record past 1.5x its benchmark (§12b).
Silence transitions use "last contact" = last player reply, not last reminder (§7.3).
On publish, remove, dispute-clear and correct: recompute the affected counters, the affected medians, the member's tier, and the not-paid line. All of it lands or none of it does (spec 12). Removal by direct action and removal by upheld flag produce identical effects.
Same format as the record machine. Every transition writes the audit row from section 6.
Casino (status)
| From | To | Trigger | Guard |
|---|---|---|---|
| none | provisional |
first record names a brand no fuzzy match resolves | no confident match |
| none | listed |
staff seed a known brand pre-launch | passport fields present |
provisional |
listed |
person confirms via the provisional queue | link.verify; real operator, not a mirror, licence plausible, domain matches; page indexable |
listed |
suspended |
staff freeze under a legal demand or fictitious brand | link.verify; reason set, aggregates frozen. The page renders a visible suspension notice, never a silent hide (spec 11.10). |
suspended |
listed |
staff reverse it | reversible |
A provisional confirmed as a mirror is merged not transitioned: records move, old URL returns 410. A rejected provisional keeps its row, noindex, never hard-deleted.
Dispute (outcome)
| From | To | Trigger | Guard |
|---|---|---|---|
| none | open |
POST /operator/dispute | domain-verified operator; min claim length; attempt <= 2; open disputes < 5; throttle clear |
open |
correction_applied |
a factual error is fixed | flag.adjudicate; fact-fix never a ruling; recompute per 2.4 |
open |
no_correction |
no factual error shown | flag.adjudicate; 72h target |
The disputed flag is independent of the outcome and moves no number.
Operator reply (moderation state)
| From | To | Trigger | Guard |
|---|---|---|---|
| none | submitted |
POST /operator/reply | domain-verified operator; no existing reply (409); throttled |
submitted |
screened |
automatic screen runs | before publication, no manual step |
screened |
published |
screen raises no flag | publishes in seconds |
screened |
removed |
flag upheld by a person | record.review; author told; visible marker |
published |
removed |
removal rule matched | record.review; visible marker, never silent |
No transition here touches a record state or number.
Account link
| From | To | Trigger | Guard |
|---|---|---|---|
| none | pending |
POST /record/{id}/link with proof | asked after the first record |
pending |
linked |
name matches, or no-KYC casino has no name | proof_tier set by the evidence |
pending |
mismatch_held |
name on proof differs | 202, queued |
mismatch_held |
linked |
person resolves it, once | link.verify; 48h target |
On linked the member becomes distinct for every gate and the record leaves the unlinked tier.
Payout (payout_state)
| From | To | Trigger | Guard |
|---|---|---|---|
| none | requested |
member requests a payout | verified tester; amount >= minimum; <= tier max; <= balance |
requested |
review |
compatibility checks pass | coin, network, address compatible |
requested |
denied |
incompatible | hard reject, no person |
review |
approved |
person approves | payout.approve; balance re-checked; ledger debited now |
review |
denied |
person denies with reason | payout.approve; reason in audit row |
approved |
paid |
crypto sent | tx_hash recorded at this step, not at approval. Approval records intent and debits the ledger; paid records the completed transfer. |
paid and denied are terminal. A denied request never touched the ledger.
| Field | Type | Notes |
|---|---|---|
id |
uuid | |
member_id |
uuid | |
casino_id |
uuid | may be a provisional casino |
account_link_id |
uuid, nullable | null until linked, see §5 |
state |
enum §2.1 | |
disputed |
bool | §2.2 |
removed |
bool | §2.2 |
removal_reason, removed_at |
string, ts, nullable | visible marker, append-only public log (spec 11.10) |
method |
enum §8 | read from screenshot, confirmed |
chain |
enum, nullable | crypto only |
amount_original |
decimal | |
currency |
iso 4217 | |
amount_usd |
decimal | converted at request-date rate (§9) |
amount_band |
enum §8 | derived from amount_usd |
net_position |
enum: winnings / own_money / unknown | the net-position bucket, the 3.4 answer, part of the slice key (spec 3.4) |
net_position_source |
enum: verified / stated / unknown | how we know it: verified from history, player-stated, or unknown (spec 3.5) |
vip_at_time |
bool, self-reported | |
requested_at |
timestamp | |
last_player_reply |
timestamp | drives the silence clock; seeded from requested_at at creation, updated on every reply including confirm-details |
last_reminder_at |
timestamp | |
ever_overdue |
bool | set once, gates Abandoned vs Unconfirmed |
said_outstanding |
bool | player said still waiting at least once |
published_at |
timestamp, nullable | null while DRAFT |
closed_at |
timestamp, nullable | drives 90-day image destruction |
benchmark_used |
enum: casino_method / sitewide_method / published_window / none | which source applied |
benchmark_value_minutes |
int, nullable | the actual value applied at the time, frozen so a later median change never rewrites this record's overdue status |
evidence_tier |
enum §8 | bad-ending ladder only (spec 4.7) |
arrival_proof |
enum: bank_or_wallet_arrival / on_chain / none | paid records only, separate from evidence_tier, never a Tier number (spec 7.1) |
is_tester |
bool | separate bucket, never fills an organic gate |
is_control |
bool | withheld until resolved (§7.5) |
acquisition_source |
enum: organic / shared_link / operator_prompt / community | anti-selection signal (spec 9) |
| Field | Type | Notes |
|---|---|---|
id, record_id |
uuid | |
amount |
decimal | negative for a reversal |
method |
enum §8 | |
arrived_at |
timestamp | |
reversed |
bool |
Outstanding balance = requested amount minus sum of leg amounts. state derives from it (spec 4.5). The payout median uses the final positive leg's timestamp.
| Field | Type | Notes |
|---|---|---|
id, member_id, casino_id |
uuid | |
proof_tier |
enum: registered / deposited / withdrew | spec 5 |
name_match |
enum: matched / mismatch_pending / no_name | no-KYC casinos have no name |
created_at |
timestamp |
| Field | Type | Notes |
|---|---|---|
id |
uuid | |
email |
string | the only required field at first record |
identity_verified |
bool | KYC through provider, token only, no documents |
points |
int | sum of point_events, cached |
level |
int 1-99 | derived |
tier |
enum: registered/verified/established/senior/top | derived from points plus gates (spec 7.1) |
false_statements |
int | gates Senior |
last_evidenced_at |
timestamp | drives the senior-privilege pulse |
| Field | Type | Notes |
|---|---|---|
id |
uuid | |
name, primary_domain |
string | |
status |
enum: provisional / listed / suspended | provisional is noindex (§10) |
claimed_by |
uuid, nullable | current operator, denormalised from the latest claim row |
claim_history |
table | dated claims (operator_id, claimed_at), so ownership history survives a change |
passport |
json | licence, entity, ownership, sibling brands, one dated terms field (spec 6.1, spec 6.2) |
suspended_reason, suspended_at |
string, ts, nullable | freeze-and-hide, aggregates frozen, reversible |
| Field | Type | Notes |
|---|---|---|
id |
uuid | |
legal_name |
string | |
verified_domain |
string, nullable | domain-control magic link (spec 11.1b) |
paid_profile |
bool | presentation only, never read by any calculation |
profile_fields |
json | operator-stated, rendered beside sourced fields, never overwriting them |
| Field | Type | Notes |
|---|---|---|
id, record_id, operator_id |
uuid | |
claim_text |
string | min length enforced |
evidence_uri |
string, nullable | |
attempt |
int | max 2 per record (spec 14.12) |
outcome |
enum: open / correction_applied / no_correction | correction is a fact-fix, never a ruling (spec 6) |
resolved_by, resolved_at |
uuid, ts |
| Field | Type | Notes |
|---|---|---|
id, record_id, operator_id |
uuid | |
body |
text | append-only, screened like player text |
created_at |
timestamp | one per record, throttled |
Replies annotate, never change a state or a number (spec 11.1b).
| Field | Type | Notes |
|---|---|---|
id, member_id |
uuid | |
type |
enum: test_earned / withdrawal | append-only, balance = sum |
amount |
decimal | |
payout_state |
enum: requested / review / approved / paid / denied | separate payout.approve permission |
tx_hash |
string, nullable | required on every approved crypto payout |
dest_address, coin, network |
string | validated for compatibility, wrong network is a hard reject |
| Rule | Value |
|---|---|
| Earned per approved contribution | Paid through the public loyalty engine (same as every player, no closed batch, no pay-per-verdict): ~$8 loyalty row, plus 20 points |
| Evidence bonus | +$2 loyalty on any approved contribution backed by evidence (account history or transaction files). Pays for the verification work, never the verdict. Applies to everyone (spec 7.2) |
| Available balance | sum of ledger rows |
| Minimum payout | $20 (tune via config.thresholds) |
| Tier maximum per payout | Verified $250 / Established $500 / Senior $1,000 / Top $2,500 |
| Ledger debit | one negative withdrawal row at approval, never at request |
| Compatibility | coin / network / address validated at request, wrong network hard rejects |
tx_hash |
required on every approved crypto payout before paid |
| Permission | payout.approve only, separate from content permissions |
The launch corpus is roughly 2,000 measured withdrawals from founding members: veteran real players (for example experienced reviewers from the public paid-review contests) logging casinos where they already have real accounts and history. Same log-and-write flow as any player, same quality approval (positive or negative is irrelevant), paid through the same public loyalty engine as everyone, plus the evidence bonus. No closed batch, no pay-per-verdict.
Founding records stay in their own separate bucket, labelled, never merged into organic figures, and never fill an organic gate (spec 7.2 / 3.12). Legal posture: incentivized-and-disclosed contributions from genuine users, not fabricated reviews from non-users; disclosure is intrinsic because the loyalty engine is a public feature applied to every contribution.
Per file, server-enforced on every read. Default masked.
| Viewer | Sees |
|---|---|
| public | masked version if published, else nothing |
| author | their own masked and original |
staff with evidence.unmask |
unmasked original, logged |
| operator | nothing, ever |
| Field | Type | Notes |
|---|---|---|
id, record_id |
uuid | |
masked_uri |
string | what persists |
original_uri |
string, nullable | destroyed 90d after record close (§7.4) |
ocr_confidence |
float | auto-fill at >= 0.85, else typed |
scanned |
bool | malware scan before storage |
Testimony beside a record, never a measurement. Standalone accounts touch no number (spec 5).
| Field | Type | Notes |
|---|---|---|
id |
uuid | |
record_id |
uuid, nullable | null exactly when standalone |
member_id |
uuid | |
body |
text | free text, never a dropdown, grammar untouched |
is_standalone |
bool | |
moderation_state |
enum: published / flagged_held / redacted / removed | |
created_at |
timestamp |
Updates are child rows, never edits. The author never rewrites published text. Moderation is our action, never silent.
| Field | Type | Notes |
|---|---|---|
id, account_id |
uuid | |
body |
text | screened like the parent, dated thread |
created_at |
timestamp |
Ending questions, asked at every terminal state:
| Field | Type | Notes |
|---|---|---|
deposit_again |
enum: yes / no, nullable | never on standalone; one per member per casino, latest wins; published as a range behind the 5-member gate, no 8-record floor |
documents_asked |
enum: no / once / more_than_once | every ending |
Standalone deposit-proof tier, from the account link proof_tier:
| Tier | Proof | Lands |
|---|---|---|
| registered | account page screenshot | buried bucket |
| deposited | transaction history showing a deposit | main standalone section |
| withdrew | full evidenced withdrawal | attached, feeds the numbers |
Routes: POST /record/{id}/account (attached), POST /casino/{id}/account (standalone, tier guard), POST /account/{id}/append (author only).
spec 4.7, LOCKED order): tier1_casino_words, tier2_transaction_history, tier3_balance, none_buried. Applies to proving a bad ending only.bank_or_wallet_arrival, on_chain, none. A bank or wallet screenshot, or the on-chain transaction, ranks a paid record above a word-only confirmation. Nothing here depends on a casino email (spec 13.1). Never mapped onto the Tier 1/2/3 numbers.visible, buried, removed (spec 15).spec 7.1.payout.approve).spec 2.3). account_link_id is null. Record publishes at the unlinked tier, feeds no median or counter.deposited tier needs the transaction history to show a completed deposit. withdrew needs a full evidenced withdrawal.link.verify queue, resolved by a person once. No-KYC casinos have no name to match and link at the lower tier.Added in the 2 August spec revision: comments and up/down voting are phase one. Two entities, one hard guard, one hard invariant.
{id, member_id, casino_id nullable, record_id nullable, parent_comment_id nullable, body, moderation_state, created_at}. casino_id null means a General discussion; parent_comment_id set means a reply. Body screened like any player text (redaction, no personal data). moderation_state in {visible, buried, removed}: unproven posts are buried (shown, ranked below evidenced), never hidden.{member_id, comment_id, dir in {+1,-1}}, one per member per comment.verified_player = true (at least one confirmed, screenshot-backed withdrawal, spec 7.1 / spec 15). Enforced on the write path (POST /comment, POST /comment/{id}/vote), not only the UI. Logging a first cashout never needs this (spec 2.1).spec 3 / spec 15).Grantable units. Roles are bundles. One person holds all at launch; the audit log is the integrity control until a second person exists (spec 12).
| Permission | Action | Constraint |
|---|---|---|
record.review |
resolve a flagged or held record | never on an operator you have a commercial relationship with |
flag.adjudicate |
fact-check a disputed record | not "who was right", only factual error (spec 6) |
link.verify |
resolve a name mismatch or confirm a brand | |
evidence.unmask |
view an original image | writes to the audit log every time |
anomaly.review |
work the outlier queue | the only defence against selection (spec 9) |
control.assign |
assign records to the held sample | separate person as soon as a second exists |
terms.confirm |
confirm a terms diff is real (PHASE TWO) | |
commercial.manage |
manage the commercial page | cannot also hold record.review/flag.adjudicate for that operator |
config.thresholds |
change a launch default | writes to the audit log |
staff.manage |
grant and revoke permissions |
Roles are bundles over the units above. payout.approve is an eleventh unit and never enters a content bundle.
| Role | Bundle |
|---|---|
| Founder | all eleven |
| Moderator | record.review, flag.adjudicate, link.verify, anomaly.review |
| Payments | payout.approve |
| Commercial | commercial.manage |
| Control | control.assign |
| Admin | staff.manage, config.thresholds |
A user may hold several roles. One person holds all at launch. Two separations from spec 12: Commercial for an operator cannot also review or adjudicate it, and Control moves to a second person the moment one exists.
Audit log row, on every state change and every privileged action: {actor, permission_used, entity_type, entity_id, from_state, to_state, reason, timestamp}. Immutable.
def slice_median(records): # records already filtered to one slice
by_member = group_by(records, member_id)
member_medians = []
eligible = []
for member, recs in by_member:
recs = most_recent(recs, 2) # cap 2 per member per slice FIRST (spec 14.2)
eligible += recs
member_medians.append(median(elapsed_time(r) for r in recs))
if distinct_members(eligible) < 5: return None # gate (spec 3.13 / 14.2)
if len(eligible) < 8: return None # 8-record gate on CAPPED records, not raw
return median(member_medians) # median across members, not across records
Payout median: only PAID and completed PART_PAID, one method, within the 180-day window.
Time to resolution: every terminal state plus live records at current elapsed time, same slice.
def benchmark(casino, method):
candidates = [casino_median(casino, method),
sitewide_median(method),
published_window(casino, method)] # method-aware; business days -> minutes via casino tz + holidays; None if terms not held
candidates = [c for c in candidates if c is not None]
return min(candidates) if candidates else None
def is_overdue(record):
b = benchmark(record.casino, record.method)
if b is None: return False # stays WAITING, elapsed only
return elapsed(record) > 1.5 * b
Deposit-again is per CASINO, not per slice (spec 5.2): the member's latest answer per casino, published as a range once 5 distinct members have answered, with no 8-record floor. The refusal and cancellation rates below are per slice.
Denominator = records in the slice that reached an ending, one per member, within the window (§2.1 denominator column). Gate: 5 distinct members. "One per member" = the member's most recent ending in the slice.
Original image destroyed 90 days after record close. Masked version kept while published. On member deletion of images, record keeps its tier marked "evidence since destroyed".
20 percent of each founding batch, random, assigned at logging before the outcome is known. Visible only to control.assign. Publishes on resolution, marked as a released control record. If tracked withdrawals resolve faster than untracked at the same operator/method/window, publish the gap (spec 3).
All from spec 14, repeated here as the single source for a developer. spec 14 overrides if they ever diverge.
| Parameter | Value |
|---|---|
| Records before a slice shows a median | 8 |
| Distinct members for that slice | 5 |
| Distinct members before a headline counter | 5 |
| Per-member cap per slice | 2 |
| Rolling window | 180 days (trend is exempt) |
| Records per member per casino per week | 5 |
| Overdue at | 1.5x benchmark |
| First arrival prompt | day 2, then days 4, 7, 14, then weekly |
| Silence to Unconfirmed / Abandoned | 21 days after last player reply |
| Draft expiry | 14 days, one warning |
| Dispute response target | 72 hours |
| Max evidence file | 12 MB, JPEG/PNG/HEIC/PDF |
| OCR auto-fill confidence | >= 0.85 |
| Unmasked retention | 90 days after close |
| Dedup key | casino + member + amount within 1% + request date within 1 day |
| Open disputes per casino | 5 |
| Re-dispute limit | 2 attempts ever |
| Dispute throttle | correction rate under 30% after >= 5 disputes |
| Held control share | 20% |
| Trend window | last 10 vs previous 10, min 20 in slice, suppress under 3 members |
Saved queries over the one store, not separate tables. Same transition guards fire on every path (spec 12).
| Queue | Source | Permission | Target |
|---|---|---|---|
| Anomaly review | outlier clusters | anomaly.review |
daily |
| Flagged content | screen_flag = true |
record.review |
24h |
| Name mismatches | name_match = mismatch_pending |
link.verify |
48h |
| Provisional casinos | casino.status = provisional |
link.verify |
before index |
| Disputed records | disputed = true |
flag.adjudicate |
72h |
| Terms diffs (PHASE TWO, not launch) | new snapshot differs | terms.confirm |
weekly |
noindex, no stable URL, record visible only by direct link.spec 11). Records move to the right casino or are held unresolved.An operator has no login. To set a disputed flag or use the phase-two channel, it verifies by domain-control magic link to an address on the brand's primary domain, generic providers rejected (spec 6). Guardrails in spec 14.12.
Full curve and reasoning in spec 7.1, LOCKED. The schema-relevant facts:
member.points int, member.level int 1-99, member.tier enum.spec 7.1, LOCKED): Registered L1-9 at 0-99 points; Verified L10-29 at 100+ and KYC; Established L30-59 at 400+; Senior L60-89 at 1,200+; Top L90-99 at 3,000+ and a manual invite. Level interpolates on points within a band. The L62 / 1,240-point sample is Senior just above the 1,200 floor.false_statements = 0; Top is a manual flag.{member_id, source, points, casino_id, at}.casino_id is clamped at 150 at read time.penalty record.points. Senior/Top privilege check is live: active = last_evidenced_or_corroborated_at within 180 days.| Source | Points | Cap |
|---|---|---|
| kyc | 50 | once |
| evidenced_withdrawal | 25 | |
| arrival_proof | 10 | |
| casino_link | 15 | once per casino |
| distinct_casino_deposit | 20 | 8 casinos. OFF until the link-graph protection ships (spec 7.1). Order cannot change. |
| written_account | 5 | |
| reminder_answer | 2 | 10/month |
| corroboration_held | 15 | Senior only |
| paid_test | 20 |
The product is a clock. A third of the state machine only moves on timers. This is how that work runs, because two developers would otherwise each invent it differently.
One sweep, not lazy-on-read. A scheduled job runs every 5 minutes over live records (DRAFT, WAITING, PART_PAID). Derived state is never computed only at read, so a record that has gone overdue is overdue for everyone at once, not just the next viewer.
The sweep is idempotent and ordered. Each record is evaluated once per tick. Transitions are checked in a fixed order so two cannot fire in one tick: overdue before silence, silence before nothing. Overdue is a flag, so it never competes with a state. The silence clock reads ever_overdue, which latches on an earlier tick, so a record cannot be flagged overdue and abandoned in the same tick.
| Timer | Fires when | Set by the sweep |
|---|---|---|
| Overdue | elapsed > 1.5 * benchmark and benchmark exists |
set overdue = true, latch ever_overdue, freeze benchmark_value_minutes. No state change. |
| Reminder | day 2, then 4, 7, 14, then weekly from request | enqueue a push or email, record last_reminder_at |
| Unconfirmed | 21 days since last_player_reply, ever_overdue = false, never said outstanding |
live to UNCONFIRMED |
| Abandoned | 21 days since last_player_reply, ever_overdue = true or said outstanding |
live to ABANDONED_UNPAID |
| Draft expiry | 14 days in DRAFT |
one warning at day 13, delete at 14 |
| Image destruction | 90 days after record close | delete original_uri, keep masked |
benchmark_value_minutes is frozen per record at the moment it goes overdue, and never recomputed. A later shift in the casino or site-wide median changes the benchmark for records not yet overdue, never for one already judged. This is why the value is stored, not just the source. A WAITING record has not frozen a value yet and is judged against the live benchmark on each tick until it crosses.
Reminder timers key off last_player_reply, not last_reminder_at. A reminder we send does not reset the silence clock, only a reply does, or the silence transitions would never fire.
Sweeps are single-writer per record. The job takes a row lock, so a player action and a sweep transition cannot race. A player confirming arrival mid-sweep wins; the sweep skips a record whose state changed since it was read.
Everything in spec 14.13, plus, specific to this document:
spec 14.9), only the FX source is open.spec 12).Public read API, no auth. Every response carries slice, count and computed_at so a number never travels without its denominator (spec 3).
Returns the passport, the per-method medians, the not-paid line, the counters, the trend.
200 { casino, passport, methods: [ { method, net_position, median_minutes, count,
distinct_members, window_days, trend } ], not_paid_line, counters, records_url }
404 casino not found
409 casino is provisional (noindex), body carries the single record only
Cursor-paginated. Filter by method, net_position, band, state, tier. Sort by requested_at or elapsed.
200 { records: [ record ], next_cursor, total }
total is a true count, not a page count. Aggregates compute over the full filtered set, not the page.
Casinos ranked on winnings payout time for one method, distinct-member weighted.
200 { method, positions: [ { rank, casino, median_minutes, count } ] }
A casino appears only if its slice clears the gate (spec 14.2).
Create a record from a screenshot. No auth beyond an email for the clock. Lands in DRAFT.
201 { record_id, state: DRAFT, read_fields }
422 unreadable, body lists fields the player must type
Player confirms what we read. DRAFT to WAITING, the clock starts.
200 { state: WAITING, benchmark_used }
One-tap arrival confirmation. Token is single-action, single-record, expiring. Never grants account access.
200 { state } 410 token expired
masked_uri is public on the record. The unmasked original_uri is never returned by the API and is reachable only with evidence.unmask, every access logged.noindex, no aggregate, direct link only.Attach the casino account link after the first record. Proof screenshot.
200 { proof_tier } 202 name mismatch, queued for a person
Domain-verified operator disputes a record. Fact-check only.
201 { dispute_id, state: open } 429 dispute cap reached
One public reply per record. Screened, throttled.
201 { reply_id } 409 reply already exists
POST /staff/record/{id}/resolve, /flag/{id}/adjudicate, /link/{id}/verify, /casino/{id}/confirm, /casino/{id}/suspend, /terms/{id}/confirm, /control/assign. Each requires the matching permission from §6 and writes the audit row.
JSON { error, code, detail }. Codes are stable strings, not HTTP status alone.
The calls that are settled, why, and what they replaced. A developer who wants to change one reads here first (spec appendix has the full history).
| # | Decision | Why | Replaces |
|---|---|---|---|
| D1 | Payment stops the clock, never a response | any response metric is captured by whoever answers cheapest | operator response time |
| D2 | No composite score, only records and named medians | a formula is an attack surface and a thing to defend | 5-star Bayesian average |
| D3 | We never contact the operator (one narrow phase-two exception) | avoids the data-protection wall and the fraud-signal that slows the player's money | operator notification loop |
| D4 | Silence inherits the last known state | resetting to neutral rewarded operators for making people give up | silence to neutral |
| D5 | Publish immediately, gate the statistics | a submission that vanishes into a queue never brings anyone back | pre-publication approval queue |
| D6 | Account link is asked for after the first record | the ten minutes of motivation should buy a record, not setup | link before upload |
| D7 | We do the redaction, not the player | every task moved off the player is a record kept | player crops their own shot |
| D8 | Overdue at 1.5x the lowest of three benchmarks | an ordered ladder let a slow casino grade its own homework | casino-own-median benchmark |
| D9 | Part paid enters no median until the balance is zero | otherwise pay $500 of $16k fast and post a good number | partial credit |
| D10 | Disputes are fact-checks, never rulings on who was right | the whole product rests on not adjudicating | dispute as verdict |