Why this exists
Sideout is a charity beach volleyball app. It is the easy half, and it is the part you can see. The half underneath it is Purse: an immutable double-entry ledger, a contest and settlement engine, versioned eligibility rules, an embeddable SDK, a webhook dispatcher, an operator console and a fiat rail with custody reconciliation.
That is, on purpose, the same shape as the infrastructure Lucra sells: one SDK a brand drops into an app it already has, with payments, compliance and settlement handled underneath. I built it because the only way to have an opinion about that problem is to solve it, and because the interesting parts of it are invisible from the outside.
Ledger invariants
9
Reconciled every 15 minutes. A failure makes the platform answer 503 and page an operator.
Custody, reconciled
$300.00
What the rail moved and what the ledger owes, agreeing exactly. Follow a dollar.
Separate databases
2
The app and the platform never share one. A test proves each process refuses to load the other’s connection string.
Every row names a real module. The claim is not that this resembles their product; it is that the module exists, is tested, and can be read.
Lucra calls it
One SDK, embedded in the partner app
Here
@purse/sdk and a cross-origin iframe
The partner mounts an iframe on Purse’s origin and talks to it over typed postMessage with an exact origin and a nonce. The secret key never reaches a browser, and the build greps its own client bundle for one and fails if it finds it.
packages/purse-sdk, apps/purse-embed
Lucra calls it
Merchant of record for real-money flows
Here
The treasury: custody, deposits, withdrawals, rake
Dollars sit in custody at a payment provider; the ledger records each player’s claim in a closed-loop asset at one unit to one cent. An invariant reconciles the two every fifteen minutes and fails the health check if they diverge.
apps/purse/src/treasury
Lucra calls it
KYC and identity verification
Here
The identity seam and its state machine
The state machine is real and enforced by a database trigger, so a user cannot jump from unstarted to verified. The check itself is an interface standing in for Persona or Socure; no document or image has anywhere to be stored.
apps/purse/src/providers, apps/purse/src/users/verification.ts
Lucra calls it
Geolocation and state availability
Here
The geolocation seam and permitted regions
Every eligibility decision records the region it was made under and the version of the ruleset in force, so an auditor can be shown which rules applied when. GeoComply is the vendor named behind the interface.
apps/purse/src/providers/dev/geo.ts, apps/purse/src/eligibility
Lucra calls it
Fraud monitoring
Here
The risk seam, velocity limits and operator flags
Rolling stake limits and duplicate-identity fingerprints raise operator flags rather than blocking silently. Sardine is the vendor named behind the interface.
apps/purse/src/providers/dev/risk.ts, apps/purse/src/eligibility/velocity.ts
Lucra calls it
Dispute resolution and settlement
Here
Score consensus, the dispute queue and a frozen payout preview
The partner owns the outcome and Purse owns the settlement. A close must present the hash of the preview it was shown, and a contest whose inputs moved since then is refused rather than quietly paying something else.
apps/sideout/src/server/consensus.ts, apps/purse/src/contests/settlement.ts
Lucra calls it
Free-to-play, real money, or rewards
Here
Two assets and a per-contest rake
Free-to-play contests run on POINTS with no rake. Cash contests run on the custodied asset with a rake frozen on the contest at creation, so the fee entrants agreed to cannot change underneath them.
apps/purse/src/db/schema.ts, apps/purse/src/treasury/money.ts
Lucra calls it
Responsible gaming: limits and self-exclusion
Here
Restrictions honoured before entry and before money moves
A self-exclusion stops a deposit as firmly as it stops an entry. Elsewhere the platform flags rather than blocks; this is the deliberate exception, because being stopped is the thing the user asked for.
apps/purse/src/users/restrictions.ts
Lucra calls it
Tournaments, peer-to-peer, mini games
Here
Three contest kinds on one engine
Tournament, head-to-head and pool share a lifecycle, an escrow model and a settlement engine. A second tenant, an office ping-pong ladder, runs on the same platform to prove the engine is not Sideout-shaped.
apps/purse/src/contests
Lucra calls it
Operator-controlled configuration
Here
The operator console and a versioned ruleset
Its own origin and its own sessions, no database access, talking to the API over the same boundary a partner does. Rulesets are versioned and pinned to every decision made under them.
apps/purse-console, apps/purse/src/eligibility/rulesets.ts
Before replacing it, I integrated it. There is a second, standalone version of Sideout that talks to Lucra’s documented REST API and Web SDK, with a faithful in-process mock so every flow runs end to end without credentials.
Porting their user-score-by-metadata matcher turned up something worth reporting: two of the five worked examples in the published documentation contradict the prose specification directly above them — example three’s scoring of a fully equal array, and example five’s partial score for summer-league against summer-tournament. Rather than guess, the mock implements both readings behind a switch, /health reports which one is active, and a test asserts every row of the comparison table.
That build lives at github.com/Mohith26/sideout; this one, the platform rebuild, at github.com/Mohith26/sideout-purse.