Technical referencePartner platform contract
The implementation is deliberately simple: authenticated partner owners configure partner sites, unauthenticated players use an iframe, and every funded slip joins one global round ledger.
Hookedin owns
- partner-site dashboard, embed generation, and settings validation
- order creation, deposit address allocation, and private receipts
- block scanning, payment recording, ticket assignment, and settlement
- winner selection, partner win records, payout PSBT generation, and public verification utilities
Partner owns
- its website, player account system, and any casino balance ledger
- the public player ID and private account ID passed to the iframe
- the public Bitcoin payout address configured on the partner site
- crediting its internal player after seeing a partner win record
Embed contract
<iframe
title="Partner Lotto"
src="https://hookedin.example/embed?partner=partner-slug&playerPublicIdentifier=PUBLIC_PLAYER_ID&playerPrivateIdentifier=PRIVATE_ACCOUNT_ID"
style="width:100%;max-width:560px;height:760px;border:0;"
loading="lazy"
referrerpolicy="strict-origin-when-cross-origin"
></iframe>
`playerPublicIdentifier` is display-safe public ledger attribution. `playerPrivateIdentifier` is used for partner reconciliation and can match the public value when that is acceptable. Neither value is a login token or authorization mechanism. The iframe can render public round information without them, but ticket buying stays disabled until the parent site passes both values.
Lifecycle
- Partner creates a live partner site and public payout address.
- Partner embeds /embed?partner=PARTNER_SLUG in its own product; buying tickets requires adding playerPublicIdentifier and playerPrivateIdentifier after parent-site login.
- Iframe loads current round, partner settings, ticket price, house edge, and payout disclosure.
- Player creates an order; Hookedin allocates a unique deposit address.
- Scanner records confirmed payment outpoints on the order and recalculates the ticket slip.
- The order's single slip receives a ticket range in the global shared round sequence.
- Closing block hash settles the round and selects the winning ticket.
- Settlement creates a partner_wins row only when the winning ticket is in a sold slip.
- Admin generates, signs, broadcasts, and refreshes the payout PSBT.
Page route surfaces
Page files are organized with Next route groups: app/(hookedin) for Hookedin website pages, app/(shared) for pages exposed on both surfaces, and app/(embed) for partner iframe pages. The URL paths below are generated from the same route taxonomy used by chrome, headers, and middleware.
Hookedin website only/, /account, /admin, /demo, /docs, /docs/faq, /docs/platform, /docs/technical, /docs/utilities, /hash-draw, /hash-tweak, /login, /partners, /partners/new, /partners/:partnerName, /verify
Hookedin website only. Framing denied. First-party Hookedin product, account, partner dashboard, demo, documentation, and verification utility pages.
Shared public pages, website surface/rounds, /rounds/:roundNumber, /slips/:identifier
Both surfaces. Framing denied. Public round and slip records shown inside the normal Hookedin website chrome.
Shared public pages, embedded surface/embed/rounds, /embed/rounds/:roundNumber, /embed/slips/:identifier
Both surfaces. Partner iframe allowed. The same public round and slip records rendered with partner embed context, partner frame policy, and embed navigation.
Embedded player flow only/embed, /embed/house-edge, /embed/players/:playerPublicIdentifier/rounds/:roundNumber
Embedded only. Partner iframe allowed. Partner iframe entry, price disclosure, buy-ticket form, and partner-scoped player activity.
API route surfaces
Embedded order mutationPOST /api/slips
Embedded only. Creates ticket deposit orders for the embedded buy flow. Requires parent-site player identifiers.
Shared data APIsGET /api/rounds, /api/rounds/:roundNumber/closing-block-hash, /api/rounds/:roundNumber/slips, /api/slips/:secret, /utilities/:file
Shared API. Round, slip, receipt, and utility data used by both website and embedded surfaces. Private receipt data still requires the receipt secret.
Hookedin website APIsGET, POST, PATCH /api/admin/payout-psbts/:payoutId/broadcast, /api/admin/payout-psbts/:payoutId/cancel, /api/admin/payout-psbts/:payoutId/refresh, /api/admin/rounds/:roundNumber/payout-psbt, /api/auth/logout, /api/auth/magic-link, /api/auth/sessions/revoke, /api/auth/verify, /api/health, /api/partners, /api/partners/:partnerName, /blockscan, /reset-db
Hookedin website only. Account, partner dashboard, admin, health, and local operations endpoints outside the embedded player flow.
JSON contracts
Create orderPOST /api/slips accepts partnerName, playerPublicIdentifier, playerPrivateIdentifier, and optional requestedRoundNumber. It returns a private receipt secret and order details.
Create partner sitePOST /api/partners requires a user session, slug, site title, house edge, payout address, and payout-address acknowledgement.
Update partnerPATCH /api/partners/:partnerSlug enforces ownership and keeps settings changes scoped to future orders.
Round slipsGET /api/rounds/:roundNumber/slips returns paginated public slip rows for a round.
Download utilityGET /utilities/:file serves the exact standalone script source used by the public utility docs.
House edge accounting
player_ticket_price_sats = ceil(base_ticket_price_sats * (1 + partner_house_edge))
ticket_count = floor(gross_paid_sats / player_ticket_price_sats)
jackpot_contribution_sats = ticket_count * base_ticket_price_sats
partner_balance_delta_sats = gross_paid_sats - jackpot_contribution_sats
round_jackpot_sats = total_ticket_count * base_ticket_price_sats
The order stores the partner, player, payout address, base ticket price, player ticket price, and whole-percent house edge snapshots. The slip stores the funded assignment. A 0.1 decimal house-edge rate on a 100 sat base ticket means that partner's players pay 110 sats for one ticket.
Database tables
usersEmail-only account records for partner owners.
email_magic_linksShort-lived login links stored by token hash.
user_sessionsSession tokens stored by hash with expiry and revocation.
partner_sitesOwned embed configurations with slug, site title, theme, embed origins, whole-percent house edge, status, and payout address.
ticket_ordersPrivate pre-funding orders with deposit address, receipt secret hash, and partner snapshots.
ticket_slipsOne funded ticket assignment per order with paid sats, sold-ticket base value, ticket count, range, block metadata, and status.
order_paymentsConfirmed payment outpoints credited to orders and rounds.
roundsRound window, locked ticket price, locked ticket supply, fixed jackpot, sold ticket count, closing block hash, winning ticket, and settlement status.
partner_winsDurable dashboard rows for winning partner slips and private account reconciliation.
payout_psbtsGenerated payout transactions, signing state, broadcast txid, and confirmation state.
Important schema shape
CREATE TABLE partner_sites (
id uuid PRIMARY KEY,
owner_user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
name text NOT NULL,
display_name text NOT NULL,
status text NOT NULL DEFAULT 'draft',
theme jsonb NOT NULL DEFAULT '{}'::jsonb,
embed_allowed_origins text[] NOT NULL DEFAULT '{}'::text[],
house_edge_percent smallint NOT NULL DEFAULT 0,
payout_address text NOT NULL,
payout_address_network text NOT NULL,
CHECK (status IN ('draft', 'live', 'paused')),
CHECK (house_edge_percent BETWEEN 0 AND 15)
);
CREATE UNIQUE INDEX partner_sites_normalized_name_idx
ON partner_sites ((translate(replace(lower(name), '-', ''), '0134578', 'oleastb')));
CREATE TABLE order_payments (
id uuid PRIMARY KEY,
order_id uuid NOT NULL REFERENCES ticket_orders(id) ON DELETE CASCADE,
txid text NOT NULL,
vout integer NOT NULL,
amount_sats bigint NOT NULL,
block_height bigint NOT NULL,
block_hash text NOT NULL,
credited_round_number integer REFERENCES rounds(round_number),
orphaned_at timestamptz,
UNIQUE (order_id, txid, vout)
);
CREATE TABLE partner_wins (
id uuid PRIMARY KEY,
partner_site_id uuid NOT NULL REFERENCES partner_sites(id) ON DELETE CASCADE,
round_number integer NOT NULL REFERENCES rounds(round_number),
slip_id uuid NOT NULL REFERENCES ticket_slips(id),
winning_ticket_number bigint NOT NULL CHECK (winning_ticket_number >= 0),
player_public_identifier text,
player_private_identifier text,
UNIQUE (partner_site_id, round_number, slip_id)
);Orders carry partner setting snapshots so later changes do not rewrite existing receipts, payout destinations, or house edge snapshots. Slips stay focused on the ticket assignment. UUID primary keys use UUIDv7, so creation time is derived from the ID instead of a separate creation timestamp column.
Validation rules
Partner slugLowercase URL-safe partner slug, 3 to 64 characters, with a database unique index on the normalized slug.
House edgeAPI values are decimal rates from 0 through 0.15. The database stores whole-percent integers from 0 through 15.
Payout addressRequired for live partner sites and validated against the configured Bitcoin network.
Payout warningCreating or changing a payout address requires explicit acknowledgement that the address is public.
ThemeOnly accentColor is accepted. Unknown theme keys are ignored.
Embed originsExact http or https origins are stored separately and emitted in the partner-specific /embed frame-ancestors CSP.
Player IDsOptional, normalized, capped at 120 characters, restricted to letters, numbers, spaces, periods, underscores, and hyphens, and split into public ledger display plus private partner reconciliation values.
Round numberPositive bounded integer, rejected before database use if unsafe or malformed.
Receipt secretGenerated server-side, stored only as a hash, and required to view private receipt data.
Winner selection
state = bytes(closingBlockHash)
repeat HASHES_PER_DIGIT hashes for each configured ticket digit
digit = state as a decimal number mod 10
drawDigits = digit + drawDigits
winningTicket = integer(drawDigits)
Partner metadata, house edge, payout address, and player IDs do not enter the draw. Verification only needs the closing block hash and the round's locked ticket digits. Settlement maps a sold winning ticket to the public slip whose inclusive ticket range contains it. If the ticket is unsold, the house holds that number and no player payout is created.
Settled rounds record the winner algorithm ID, draw digits, and draw calculation digests. The hash work per digit comes from the application environment.
Security rules
- Dashboard and partner settings routes require a user session.
- Partner settings updates enforce owner_user_id.
- Embedded order creation does not require a Hookedin session, but it must be a same-origin JSON request from the iframe.
- Player IDs must never authorize receipts, balances, payouts, or dashboard access.
- Private receipts are protected by unguessable receipt secrets.
- Public slip rows expose only public player IDs, never private account IDs.
- Order creation is rate-limited by client IP and configured trusted proxy header.
- Admin payout routes require admin basic auth and CSRF protection.
- Startup rejects missing required configuration values loaded through the environment.
Operational testing
- partner house edge inflates player ticket price and derives partner balance
- partner payout address is required and must match the configured Bitcoin network
- embed order creation works without a Hookedin session
- public player IDs and private account IDs are stored for partner reporting while public slip rows omit the private account ID
- multiple partner sites feed one continuous round ticket sequence
- partner win record creation is idempotent after settlement
- scanner payment recording and slip recalculation are idempotent
- admin payout generation excludes uncredited late payments
- published utility source files match the scripts in utilities/bin
- testing wallet receive indexes are reserved safely under concurrent CLI use