Technical reference

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 FAQ

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/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

  1. Partner creates a live partner site with a public payout address.
  2. Partner embeds /embed?partner=PARTNER_NAME&endUserPublicIdentifier=PUBLIC_PLAYER_ID&endUserPrivateIdentifier=PRIVATE_PLAYER_ID in its own authenticated product.
  3. Iframe loads current round, partner settings, ticket price, commission, and payout disclosure.
  4. Player creates an order; Hookedin allocates a unique deposit address.
  5. Scanner records confirmed payment outpoints and recalculates the ticket slip.
  6. Slip receives ticket range in the global shared round sequence.
  7. Closing block hash settles the round and selects the winning ticket.
  8. Settlement creates a partner_wins row if the winning slip belongs to a partner site.
  9. Admin generates, signs, broadcasts, and refreshes the payout PSBT.

Routes and APIs

/embed?partner=:partnerNameUnauthenticated player iframe for a live partner site.
/p/:partnerNamePublic partner page with iframe preview and public round history.
/p/:partnerName/rounds/:roundNumberPublic partner-specific round ledger.
/partners/newAuthenticated partner-site creation form.
/partners/:partnerNameAuthenticated partner dashboard, iframe snippet, demo, settings, stats, and wins.
/api/partnersAuthenticated JSON API for creating partner sites.
/api/partners/:partnerNameAuthenticated JSON API for updating owned partner sites.
/api/slipsUnauthenticated order creation endpoint used by the iframe.
/api/slips/:secretPrivate receipt API for the receipt secret.
/api/roundsPublic round list.
/api/rounds/:roundNumber/verificationPublic verification JSON for utilities and browser verification.
/utilities/:filePublic source for standalone verification scripts.

JSON contracts

Create orderPOST /api/slips accepts partnerName, endUserPublicIdentifier, endUserPrivateIdentifier, and optional requestedRoundNumber. It returns a private receipt secret and order details.
Create partnerPOST /api/partners requires a user session, name, display name, commission, payout address, and payout-address acknowledgement.
Update partnerPATCH /api/partners/:partnerName enforces ownership and keeps settings changes scoped to future orders.
Verify roundGET /api/rounds/:roundNumber/verification returns round data, public slips, winning slip data, and payout status when available.
Download utilityGET /utilities/:file serves the exact standalone script source used by the public utility docs.

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

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 name, display name, theme, embed origins, commission, status, and public payout address.
ticket_ordersPrivate pre-funding orders with deposit address, receipt secret hash, and partner snapshots.
ticket_slipsFunded ticket assignments with immutable partner, user, payout, commission, and ticket-range data.
slip_paymentsConfirmed payment outpoints credited to slips and prize-pool rounds.
roundsRound window, sold ticket count, prize pool, closing block hash, winning ticket, and settlement status.
partner_winsDurable dashboard rows for winning partner slips and private user 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[],
  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

NameLowercase URL-safe partner name, 3 to 64 characters, with a database unique index on the normalized name.
CommissionNumber from 0 through 1. 0.1 is shown as 10%.
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.
User identifiersOptional, 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

SHA256("hookedin:v3:{roundNumber}:{closingBlockHash}")
winningTicket = (digestInteger % soldTicketCount) + 1

Partner 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