Commit Graph

7 Commits

Author SHA1 Message Date
drjones
d7fa097eab feat(ux): scan-to-cash-out, first-run walkthrough, plain language
Cashing out was the least approachable thing here: open your wallet,
create an invoice for exactly the right amount, copy it, come back, paste
it. That is the step people abandon, leaving sats behind.

LNURL-withdraw replaces it with a scan. The arcade shows a code, the
wallet pulls the funds, and the player never handles an invoice or types
an amount. The paste path is kept for wallets without LNURL support, but
folded away.

The withdraw token is a bearer instrument, so it is random, single-use,
bound to one account and one amount, and expires in five minutes. Sixteen
goroutines racing one code yield exactly one payment. Funds are debited
when the code is issued — otherwise a player could cash out and bet the
same sats before the wallet claimed them — and a sweep refunds any code
that is never scanned.

bech32 is verified against the BIP-173 vectors, including the invalid
ones. Getting this wrong produces codes that silently fail to scan with
no useful error for the player.

Adds a three-card first-run walkthrough, an explanation of what a
multiplier target means, and a one-time confirmation before a player's
first real-money action — the interface is deliberately frictionless, and
that is the one place a moment of friction is worth it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 01:05:52 +00:00
drjones
c250ed2f80 feat(wallet): Lightning deposit and withdrawal in the client
Deposit shows a scannable QR, because nobody types a 400-character
invoice at a party. The QR encoder is written rather than imported: a CDN
script is a dependency on the outside world, and this box has to work on
a network with no internet.

Its tests caught a real bug — the format-information loop wrote eight
cells down column 8, but the eighth is the dark module, which is fixed.
Overwriting it produces symbols some readers reject.

The deposit and withdraw cards stay hidden unless the server reports a
node, so a play-money deployment does not advertise a deposit it cannot
honour. Settlement is polled and confirmed server-side against the node,
so the client cannot claim a payment that never arrived.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 04:18:57 +00:00
drjones
b0f07f63ff feat(pqsign): WASM signer for browser-side post-quantum identity
WebCrypto has Ed25519 but no ML-DSA, so the post-quantum half is
compiled from the same pkg/pqid the server verifies with. One
implementation of the scheme in the project means a client and server
cannot disagree about signing.

The Ed25519 half is stored as its 32-byte seed rather than the expanded
key, since the seed cannot encode an inconsistent pair, and the public
key is derived rather than stored so a client cannot present one that
does not match what it signs with.

Verified end to end in a JS runtime: 1984-byte public key, 3373-byte
signature, derived key matches, malformed input returns an error rather
than crashing the module. 3.4MB, 0.9MB gzipped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 04:10:51 +00:00
drjones
038550b6ff perf: single-statement postings, marshal-once broadcast, client interpolation
Measured, then fixed, the three things that made a crowd impossible.

Ledger: Post issued three round trips per posting, so settlement scaled
in network latency rather than work. It is now two statements regardless
of leg count — settling 1000 winners went 844ms to 220ms. The lock and
the balance read must stay separate statements: a single statement, even
one whose CTE does FOR UPDATE, evaluates against a snapshot taken before
the locks are held, so concurrent transactions read stale balances and
money disappears. The conservation tests caught exactly that.

Broadcast: every connection marshalled its own copy, ~355us each. At any
real crowd that exceeds the tick interval by orders of magnitude. Frames
are now serialised once per broadcast and shared.

Feed: the player list is capped at 24 and carries no public keys, and
running rounds broadcast at 5Hz instead of 60Hz. Clients compute the
multiplier locally from the round start time, which the deterministic
curve makes exact. Frame size fell from 3.6KB to 1.8KB.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 22:36:30 +00:00
drjones
5c3393b989 feat(ui): 3D scenes, mobile-first layout, portfolio charts
Three WebGL scenes on one renderer and one context, since a phone should
not allocate a context per game: a rocket straining against gravity, a
decaying orbit, and a tower that sways harder the higher it stacks. The
loop stops when the tab is hidden.

Navigation moves to the bottom, where a thumb already is. New Stats view
with balance history, cash-out rate, and a distribution chart that plots
observed crash points against what the published maths predicts — the
honest version of a hot-numbers board.

Charts are hand-built SVG, ~300 lines, rather than a library that would
cost more to load than the 3D engine.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 22:26:35 +00:00
drjones
48a9120fe4 feat: auto cash-out targets, 1% house edge, terminal aesthetic
Auto cash-out closes a position at exactly the chosen target rather than
the next tick's multiplier, and fires whenever the target is at or below
the crash point. This is the feature that makes the game playable over a
network, where manual timing is at the mercy of latency.

House edge drops from 2% to 1% across crash and scratch. Scratch prize
tables retuned so the published 99% RTP is exact.

Adds docs/API.md: the client uses no private endpoints, so anyone can
write a bot against the same API.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 16:24:31 +00:00
drjones
2a2a1db8de feat: playable arcade — rooms, identity, client, deployment
Round length is now bounded: the multiplier follows a hyperbolic curve
diverging at 60s, replacing an exponential one where a 275x crash point
produced a two-and-a-half minute round.

Fixes seed reveal, which silently failed every round because pgx cannot
encode a fixed-size byte array as bytea.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 15:34:52 +00:00