Partner 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.
System boundary
Read FAQHookedin 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/private user identifiers 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="Demo Partner Lotto" src="https://hookedin.example/embed?partner=demo-partner&endUserPublicIdentifier=PUBLIC_PLAYER_ID&endUserPrivateIdentifier=PRIVATE_PLAYER_ID" style="width:100%;max-width:560px;height:760px;border:0;" loading="lazy" referrerpolicy="strict-origin-when-cross-origin" ></iframe>
`endUserPublicIdentifier` is display-safe user ledger attribution. `endUserPrivateIdentifier` is used for partner reconciliation and can match the public value when that is acceptable. Neither value is a login token or authorization mechanism.
Lifecycle
- Partner creates a live partner site with a public payout address.
- Partner embeds /embed?partner=PARTNER_NAME&endUserPublicIdentifier=PUBLIC_PLAYER_ID&endUserPrivateIdentifier=PRIVATE_PLAYER_ID in its own authenticated product.
- Iframe loads current round, partner settings, ticket price, commission, and payout disclosure.
- Player creates an order; Hookedin allocates a unique deposit address.
- Scanner records confirmed payment outpoints and recalculates the ticket slip.
- Slip receives 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 if the winning slip belongs to a partner site.
- Admin generates, signs, broadcasts, and refreshes the payout PSBT.
Routes and APIs
JSON contracts
Commission payout split
ticket_count = floor(gross_paid_sats / ticket_price_sats) prize_contribution_sats = gross_paid_sats commission_sats = floor(prize_pool_sats * commission) player_receives_sats = prize_pool_sats - commission_sats hookedin_commission_sats = floor(commission_sats / 2) partner_commission_sats = commission_sats - hookedin_commission_sats partner_receives_sats = player_receives_sats + partner_commission_sats
The slip stores gross paid sats, prize contribution, ticket count, ticket price, and the partner commission snapshot. A 0.1 commission is shown as 10%: players are shown 90% of the prize pool, Hookedin keeps 5%, and the partner payout is 95% before any Bitcoin network fee.
Database tables
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[],
commission double precision NOT NULL DEFAULT 0,
payout_address text NOT NULL,
payout_address_network text NOT NULL,
CHECK (status IN ('draft', 'live', 'paused')),
CHECK (commission >= 0 AND commission <= 1)
);
CREATE UNIQUE INDEX partner_sites_normalized_name_idx
ON partner_sites ((translate(replace(lower(name), '-', ''), '0134578', 'oleastb')));
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),
end_user_public_identifier text,
end_user_private_identifier text,
UNIQUE (partner_site_id, round_number, slip_id)
);Orders and slips carry snapshots so later partner setting changes do not rewrite existing receipts, ticket ranges, payout destinations, or commission snapshots. UUID primary keys use UUIDv7, so creation time is derived from the ID instead of a separate creation timestamp column.
Validation rules
Winner selection
SHA256("hookedin:v3:{roundNumber}:{closingBlockHash}")
winningTicket = (digestInteger % soldTicketCount) + 1Partner metadata, commission, payout address, and user identifiers do not enter the draw. The winning slip is the public slip whose inclusive ticket range contains the winning ticket.
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.
- User identifiers must never authorize receipts, balances, payouts, or dashboard access.
- Private receipts are protected by unguessable receipt secrets.
- Public verification JSON exposes only public user identifiers, never private identifiers.
- 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
- commission payout split shows player, partner, and Hookedin shares
- partner payout address is required and must match the configured Bitcoin network
- embed order creation works without a Hookedin session
- public/private user identifiers are stored for partner reporting while public verification omits the private identifier
- 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