Predictu
Casino Integration

Iframe Embedding

The Predictu iframe app is the primary interface for casino players. It renders the complete prediction market trading experience - market discovery, event pages, trading tickets, portfolio management - all within a sandboxed <iframe> embedded in the operator's casino site. This guide covers every aspect of the embedding process.

Embed URL Format

The iframe source URL includes query parameters that activate embed mode and identify the operator:

GEThttps://app.predictu.com?embed=true&operator=<slug>
ParameterRequiredTypeDescription
embedYesbooleanMust be true. Activates embed mode, which hides the standalone navigation chrome (top bar, sidebar, footer), enables the PostMessage Bridge, and activates the Branding Injector.
operatorYesstringThe operator's URL-safe slug (e.g. example-casino). Used to look up operator configuration, branding, and allowed origins from Supabase.
Without query parameters: If loaded without ?embed=true, the app renders in standalone mode with its own navigation, login page, and default Predictu branding. This is useful for development and debugging.

Full Embed Code

The recommended embed snippet with all necessary attributes:

<!-- Predictu Casino - Prediction Markets Embed -->
<div id="predictu-container" style="position: relative; width: 100%; height: 100vh;">
  <!-- Loading skeleton (shown until iframe sends predictu:ready) -->
  <div
    id="predictu-loader"
    style="
      position: absolute; inset: 0;
      display: flex; align-items: center; justify-content: center;
      background: #0a0f0a; color: #5a6a5a; font-family: system-ui;
    "
  >
    Loading prediction markets...
  </div>

  <iframe
    id="predictu-iframe"
    src="https://app.predictu.com?embed=true&operator=example-casino"
    style="
      position: relative; z-index: 1;
      width: 100%; height: 100%; border: none;
    "
    sandbox="allow-scripts allow-same-origin allow-popups allow-forms"
    allow="clipboard-write"
    loading="lazy"
    title="Predictu Prediction Markets"
  ></iframe>
</div>

<script>
  const PREDICTU_ORIGIN = "https://app.predictu.com";
  const iframe = document.getElementById("predictu-iframe");
  const loader = document.getElementById("predictu-loader");

  // Initialize once the iframe document loads
  iframe.addEventListener("load", () => {
    iframe.contentWindow.postMessage({
      type: "predictu:init",
      user: {
        id: "player_12345",
        displayName: "John Doe",
        email: "john@example.com",
      },
      balance: 500.00,
      operatorId: "YOUR_OPERATOR_UUID",
    }, PREDICTU_ORIGIN);
  });

  // Listen for events from the iframe
  window.addEventListener("message", (event) => {
    if (event.origin !== PREDICTU_ORIGIN) return;
    if (!event.data?.type?.startsWith("predictu:")) return;

    switch (event.data.type) {
      case "predictu:ready":
        // Hide the loading skeleton
        if (loader) loader.style.display = "none";
        break;
      case "predictu:balance":
        // Sync balance to your UI
        console.log("Balance:", event.data.balance);
        break;
      case "predictu:trade":
        // Log or process trade events
        console.log("Trade:", event.data);
        break;
      case "predictu:navigate":
        // Optional: sync URL for deep linking
        console.log("Navigate:", event.data.path);
        break;
    }
  });
</script>

Sandbox Attributes

The sandbox attribute restricts what the iframe can do. Predictu requires the following sandbox tokens:

TokenRequiredPurpose
allow-scriptsYesRequired for the React application to run. Without this, the iframe renders a blank page.
allow-same-originYesRequired for Supabase authentication cookies and local storage. Without this, auth tokens cannot be persisted and every page load requires re-authentication.
allow-popupsRecommendedAllows confirmation dialogs and external link navigation (e.g. market source links). Can be omitted if you handle all navigation via PostMessage.
allow-formsRecommendedRequired for the login/register forms in standalone mode. In pure embed mode (PostMessage init), this can be omitted, but it is safer to include it.
Do not add allow-top-navigation. This would let the iframe redirect the entire parent page, which is a security risk. Predictu does not require it.

Feature Policies (allow attribute)

The allow attribute controls which browser features the iframe can access:

PolicyRequiredPurpose
clipboard-writeRecommendedAllows the trading UI to copy share links, referral codes, and market URLs to the clipboard. Without this, "Copy Link" buttons will silently fail.

Features the iframe does not require: camera, microphone, geolocation, payment, fullscreen. Do not add these unless you have a specific reason.

Responsive Sizing

The iframe app is fully responsive and adapts to its container dimensions. Here are recommended sizing strategies:

Full Viewport (Recommended)

The simplest approach - the iframe fills the entire content area of your page:

<iframe
  src="..."
  style="width: 100%; height: 100vh; border: none;"
></iframe>

Fixed Height Panel

If the prediction markets are one section of a larger page:

<iframe
  src="..."
  style="width: 100%; height: 800px; border: none; border-radius: 12px;"
></iframe>

Dynamic Height (Auto-resize)

Listen for the predictu:navigate message and adjust the iframe height based on the route. Market list pages need more height than individual market pages:

const HEIGHT_MAP = {
  "/discover": "100vh",
  "/watchlist": "80vh",
  "/portfolio": "70vh",
};

window.addEventListener("message", (event) => {
  if (event.origin !== PREDICTU_ORIGIN) return;
  if (event.data?.type === "predictu:navigate") {
    const height = HEIGHT_MAP[event.data.path] || "100vh";
    document.getElementById("predictu-iframe").style.height = height;
  }
});

Mobile Considerations

On mobile devices, the iframe app renders a mobile-optimized layout with a bottom navigation bar. For the best mobile experience:

  • Set width: 100% and avoid fixed pixel widths below 360px.
  • Use height: 100dvh (dynamic viewport height) instead of 100vh to account for mobile browser chrome.
  • Avoid wrapping the iframe in a scrollable container - the iframe manages its own scroll internally.
  • Ensure your parent page does not set overflow: hidden on the body when the iframe has focus, as this can interfere with touch scrolling.
/* Mobile-friendly iframe container */
.predictu-container {
  width: 100%;
  height: 100dvh;
  height: 100vh; /* fallback for older browsers */
  position: relative;
  overflow: hidden;
 -webkit-overflow-scrolling: touch;
}

Loading States

The iframe takes 1–3 seconds to load depending on network conditions. Implement a loading state to prevent a blank flash:

Skeleton Pattern

Show a loading skeleton behind the iframe, then hide it when you receive predictu:ready:

<!-- Position skeleton behind iframe -->
<div style="position: relative;">
  <div id="skeleton" style="
    position: absolute; inset: 0; z-index: 0;
    background: #0a0f0a;
    display: flex; align-items: center; justify-content: center;
    font-family: system-ui; color: #5a6a5a;
  ">
    <div>
      <div style="
        width: 40px; height: 40px; border: 3px solid #1a2a1a;
        border-top-color: #22c55e; border-radius: 50%;
        animation: spin 0.8s linear infinite;
      "></div>
      <style>@keyframes spin { to { transform: rotate(360deg); } }</style>
    </div>
  </div>

  <iframe id="predictu-iframe" ... style="position: relative; z-index: 1;" />
</div>

<script>
  window.addEventListener("message", (event) => {
    if (event.data?.type === "predictu:ready") {
      document.getElementById("skeleton").style.display = "none";
    }
  });
</script>

Timeout Fallback

Add a timeout to handle cases where the iframe fails to load or send the ready signal:

let ready = false;

window.addEventListener("message", (event) => {
  if (event.data?.type === "predictu:ready") {
    ready = true;
    document.getElementById("skeleton").style.display = "none";
  }
});

// Fallback: hide skeleton after 10 seconds even if ready wasn't received
setTimeout(() => {
  if (!ready) {
    document.getElementById("skeleton").innerHTML =
      '<p style="color: #ef4444;">Failed to load. Please refresh.</p>';
  }
}, 10000);

Origin Whitelist Configuration

The PostMessage bridge validates the event.origin of every incoming message against the operator's registered origins. This is a critical security mechanism that prevents unauthorized sites from sending commands to the iframe.

How Origins Work

An origin is the combination of protocol + hostname + port:

URLOrigin
https://www.example-casino.com/casino/marketshttps://www.example-casino.com
https://example-casino.comhttps://example-casino.com
http://localhost:3002/testhttp://localhost:3002
https://staging.example-casino.com:8443https://staging.example-casino.com:8443
www vs non-www matters. https://example-casino.com and https://www.example-casino.com are different origins. Add both if your site is accessible at either.

Managing Origins

Origins are managed through the Operator Dashboard or the iframe-origins API:

GET/api/iframe-origins

Returns the list of allowed origins for your operator.

POST/api/iframe-origins

Add a new origin. Body: { "origin": "https://www.example-casino.com" }

DELETE/api/iframe-origins?origin=https://www.example-casino.com

Remove an origin from the whitelist.

Development Mode

In development (NODE_ENV=development), the PostMessage bridge allows all localhost origins regardless of the whitelist. This makes local development easier, but never rely on this in production. The allowed check logic:

function isOriginAllowed(origin: string): boolean {
  // Exact match against whitelist
  if (allowedParentOrigin && origin === allowedParentOrigin) return true;
  // Dev convenience: allow localhost
  if (isDevelopment() && isLocalhostOrigin(origin)) return true;
  return false;
}

Embed Mode Behavior Changes

When ?embed=true is set, the iframe app makes several behavioral adjustments:

FeatureStandalone ModeEmbed Mode
Navigation chromeFull top bar, sidebar, footerHidden - only the content area renders
AuthenticationLogin/register forms via Supabase AuthPostMessage predictu:init from parent
Balance sourceOperator’s wallet via S2S callbacksInitial balance from predictu:init, updates via Wallet Adapter
BrandingDefault Predictu themeOperator's custom branding via BrandingInjector
External linksNavigate directlyOpen in new tab (respects allow-popups sandbox)
PostMessage BridgeInactiveActive - sends ready, balance, trade, navigate events

Cross-Origin Considerations

Content Security Policy

If your casino site uses a strict Content Security Policy, you need to allow the Predictu origin in the frame-src directive:

Content-Security-Policy: frame-src https://app.predictu.com;

X-Frame-Options

The Predictu iframe app sets X-Frame-Options: ALLOWALL to permit embedding. Your parent page should not set X-Frame-Options: DENY orSAMEORIGIN on the page that contains the iframe - those headers apply to the page itself being framed, not to iframes within it.

Some browsers (Safari, Brave) restrict third-party cookies by default. Since the iframe is on a different origin than the parent page, Supabase auth cookies may not persist. The PostMessage-based predictu:init authentication flow is designed to work without cookies - user identity comes from the parent, not from stored credentials.

Troubleshooting

SymptomCauseFix
Blank iframeMissing allow-scripts in sandboxAdd allow-scripts to the sandbox attribute
Iframe shows login page?embed=true missing or predictu:init not sentCheck URL params and verify PostMessage init fires after iframe load
Default Predictu brandingOperator slug incorrect or branding not configuredVerify the operator param matches your slug in the Operator Dashboard
PostMessage events not receivedOrigin not in whitelistAdd your exact origin (including protocol and port) to the Operator Dashboard
Auth cookies lost on refreshThird-party cookie blockingThis is expected in embed mode. Re-send predictu:init on each page load.
"Copy Link" button does nothingMissing clipboard-write in allow attributeAdd allow="clipboard-write" to the iframe element
iframe content not scrollable on iOSParent container has overflow: hiddenRemove overflow restriction or add -webkit-overflow-scrolling: touch