Phoenix Prediction Docs

Parent Bridge

The raw postMessage wire protocol between the iframe and the operator page

This is the low-level postMessage contract between the Phoenix Prediction iframe and your page. Most integrations should use the Host SDK - it implements everything here, including the security checks. Use this page if you cannot add the dependency, are integrating from a non-JS stack, or want to understand exactly what crosses the boundary.

The iframe does not own login, registration, cashier, or deposit. When it needs one of those, it emits an event; your page runs the flow.

Envelope

Every message - both directions - is a single namespaced, versioned object. The receiver's first job is to drop anything that is not a pmm message of the version it speaks.

{
  v: 1,            // protocol version
  ns: 'pmm',       // namespace - ignore anything else
  kind: 'event' | 'command' | 'hello' | 'ready' | 'reply',
  name?: string,   // event/command name (see tables below)
  id?: string,     // optional correlation id (command ⇄ reply)
  payload?: object,
}

kind is the discriminator:

  • hello - embed → host, the handshake announcement (version + capabilities).
  • ready - host → embed, the handshake acknowledgement.
  • event - embed → host, fire-and-forget (name + payload).
  • command - host → embed (name + payload, optional id).
  • reply - embed → host, answers a command that carried an id.

Handshake

On load the embed sends hello (retrying briefly so a late listener still catches it). Reply with ready. Events stream regardless, but the handshake lets each side feature-detect.

// embed → host
{ v: 1, ns: 'pmm', kind: 'hello', protocolVersion: 1, embedVersion: '0.1.7',
  operator: 'acme', capabilities: ['nav.events', 'theme.runtime', 'ticket.open', ...] }

// host → embed
{ v: 1, ns: 'pmm', kind: 'ready', protocolVersion: 1 }

Events: iframe → host

{ v: 1, ns: 'pmm', kind: 'event', name, payload }

namePayloadParent behavior
navigation{ view, path, title }Optional: mirror your URL / <title> / analytics.
resize{ height }Set the iframe height (auto sizing).
auth.requested{ reason }Open login/register, mint a player token, send setToken.
deposit.requested{ reason, currency }Open your cashier.
profile.requested{ reason }Open your account/profile UI.
order.placed{ marketId, orderId }Optional analytics / refresh your balance. UI hint only.
token.expiring{ secondsRemaining }Mint and setToken a fresh launch token.
error{ message, code }Log; show a support path if needed.

Commands: host → iframe

{ v: 1, ns: 'pmm', kind: 'command', name, payload }

namePayloadPurpose
navigate{ view }Drive the embed to a view ({ name: 'feed' | 'portfolio' | 'leaderboard' | 'activity' } or { name: 'listing', slug }).
openMarket{ slug }Open a market by listing slug.
openTicket{ slug, side }Open the order ticket for a market, preselecting a side.
setToken{ token }Swap the session JWT in place (post-login, or refresh).
setTheme{ mode?, accent?, secondary?, radius?, neutralHue?, neutralChroma?, colorblind?, yes?, no?, font?, fontUrl? }Re-theme at runtime, no reload. The embed derives a contrast-safe palette from these seeds (only the hue is taken from yes/no).
setBranding{ logoUrl?, logoDarkUrl?, logoHeight?, wordmark? }Set the header logo / wordmark, no reload. Logo URLs must be HTTPS; a load failure falls back to the wordmark.
refresh{}Re-run the current view's queries.
balanceUpdate{ amount, asOf }Push the player's new balance after it changes anywhere on your side (a deposit, a withdrawal, a bet on another of your products, ...). amount is the new total (decimal string); asOf is your wallet's processed_at for it. The embed merges by asOf.
back / forward{}Step the embed's own browser history.
getState{} (+ id)Ask for the current view; answered with a reply.

Correlated request (getState)

Send a command with an id; the embed answers with a reply carrying the same id.

// host → embed
{ v: 1, ns: 'pmm', kind: 'command', id: 'abc', name: 'getState', payload: {} }
// embed → host
{ v: 1, ns: 'pmm', kind: 'reply', id: 'abc', ok: true, result: { view: { name: 'feed' }, path: '/' } }

Visitor → player flow

1. Iframe starts with a visitor JWT.
2. Visitor taps Buy → iframe emits `auth.requested`.
3. Your site logs the user in; your backend mints a player JWT.
4. Host sends a `setToken` command.
5. Iframe reconnects as the player.

Example listener (raw)

const iframe = document.querySelector('#phoenix-prediction-embed')
const phoenixOrigin = 'https://embed.prediction.phoenixverse.io'

const send = (kind, name, payload) =>
  iframe?.contentWindow?.postMessage({ v: 1, ns: 'pmm', kind, name, payload }, phoenixOrigin)

window.addEventListener('message', async (event) => {
  // 1) exact origin, 2) only the embed's window, 3) the pmm envelope of our version
  if (event.origin !== phoenixOrigin) return
  if (event.source !== iframe?.contentWindow) return
  const m = event.data
  if (!m || m.ns !== 'pmm' || m.v !== 1) return

  switch (m.kind) {
    case 'hello':
      send('ready', undefined, undefined) // ack the handshake
      break
    case 'event':
      if (m.name === 'auth.requested') {
        const token = await loginAndMintToken()
        send('command', 'setToken', { token })
      } else if (m.name === 'deposit.requested') {
        openCashier()
      } else if (m.name === 'resize') {
        iframe.style.height = `${m.payload.height}px`
      }
      break
  }
})

Security requirements

Three rules are non-negotiable. The Host SDK enforces all of them for you.

  • Check event.origin with exact equality (not startsWith) before trusting any message, and confirm event.source is the embed's contentWindow.
  • Send to the exact Phoenix origin, never *.
  • Validate the envelope (ns === 'pmm' and v === 1) before reading kind/payload.
  • Register allowed origins in your Phoenix operator config - the embed rejects commands from any other origin.
  • Money truth is server-side. Bridge events coordinate UI only; never credit, debit, or unlock on a postMessage alone.

Iframe sandbox & permissions

If you build the iframe yourself (rather than via the SDK), use these attributes:

<iframe
  src="https://embed.prediction.phoenixverse.io/o/acme?token=JWT&parent_origin=https%3A%2F%2Fwww.acme.example"
  sandbox="allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox allow-forms"
  allow="clipboard-write; fullscreen"
  style="width: 100%; min-height: 720px; border: 0"
></iframe>

allow-same-origin is required (the embed needs its own origin for session storage and socket auth) and is safe because the embed is a different origin than your page.

Common questions