flipper

Docs / Integrate

Build on flipper

Add provably fair coin flips to your product with a drop-in widget for any framework, a hosted embed for iframes and native apps, or a headless SDK. Your users keep their wallet, and you keep your brand.

Hand this to your AI

The flipper-sdk skill

An Agent Skills folder (SKILL.md plus reference files) that teaches a coding assistant everything on this page. Unzip it into .claude/skills/ (or your agent's skills directory) and ask it to “add a flipper widget”, or paste SKILL.md into any coding assistant.

Humans: the package READMEs are the long form. @flipperdotfamily/widget · BRIDGE.md · @flipperdotfamily/sdk · CDN flipper-widget.js

Overview

flipper is a coin flip on majors, Robinhood stock tokens and a curated set of Robinhood Chain tokens, against a house bankroll held in $FLIPPER. At launch a flip has a 45% win chance and pays 2× in the token you staked (2.05× on $FLIPPER flips), improving to 47.5% at 2× as the protocol grows. Each flip settles in one transaction. The main docs page explains the odds, randomness and settlement.

Drop-in widget@flipperdotfamily/widget

<flipper-widget> is the whole flip UI (token picker, amount, 3D coin, results, listing, native ETH) as a web component, with typed wrappers for React, Vue, Svelte and Angular.

Hosted embedflipper.family/embed

The same widget on its own page, for an <iframe>. It borrows your wallet over a postMessage bridge.

MobileRN · Flutter · iOS · Android

Native packages that load the embed in a WebView and lend it your app's wallet.

Headless SDK@flipperdotfamily/sdk

viem-based TypeScript: deployments, previews, flips, native ETH, listing and settlement, for your own UI or a backend.

Your partner id

Give your integration a partner id (letters, digits and . _ : -, up to 64 characters). The widget, embed and SDK API client echo it in every event payload and send it as the X-Flipper-Partner header on flipper API calls, so activity through your integration is attributed to you. It isn't a secret. acme in the samples is a placeholder.

Partners (ERC-8021)

A registered partner code also attributes your players' flips onchain. The widget and SDK add it to each flip as an ERC-8021 data suffix, and the house credits you on every attributed flip.

  • Your cut. Every code earns 20% of the flip's expected edge. Keep it as revenue, give it to players as better odds, or set a split.
  • How it's paid. Your share accrues onchain on losing flips (the only ones that pay the house), sized to match your cut on average. Anyone can pay it out to your payout address with claimPartner(id).
  • Anyone can register. A code is 1–32 characters of a-z 0-9 _ -. Register it with a payout address and the share you give back as odds, and it earns from the next flip: no approval. Codes are first come, first served.
  • Same attribute everywhere. Set partner to your code on the widget, embed or SDK client. An id that isn't an approved code still works for reporting, at normal odds.
Partner flips with the SDK
import { createFlipperClient } from "@flipperdotfamily/sdk";

// your partner code: flips from this client carry it onchain (an ERC-8021 suffix)
const flipper = createFlipperClient({ publicClient, walletClient, addresses, partner: "your-code" });

const pv = await flipper.preview(token, amount); // the odds your players get, discount included
await flipper.flip({ token, amount });           // attributed to you onchain

// your share accrues on losing flips; anyone can pay it out to your payout address
const id = await flipper.partnerIdOfCode("your-code");
const { amount: paid } = await flipper.claimPartner(id);

Choose your path

Integration paths
PathBest forWalletStart
Web componentAny site or framework, including plain HTML. One script tag or npm import.Pass provider or walletClientScript tag
Framework wrapperReact / Next.js, Vue, Svelte, Angular: typed props, framework-native events, SSR-safe React.Props, from your wallet kitReact
Iframe embedStrict CSPs, site builders, or keeping third-party code out of your page.Over the bridge (mountFlipperIframe)Iframe embed
NativeReact Native, Flutter, iOS and Android apps.Your app's wallet, over the bridgeMobile apps
Headless SDKYour own UI, bots, backends and analytics.A viem WalletClientHeadless SDK

Quickstart

Each snippet renders the widget and wires a wallet. Without a wallet the widget runs read-only (live prices, odds and the token list) and its button reads “Connect wallet”.

No build step. The CDN file registers <flipper-widget>, exposes the global FlipperWidget and bundles viem, lit and the SDK.

index.html
<script src="https://cdn.jsdelivr.net/npm/@flipperdotfamily/widget@0/dist/cdn/flipper-widget.js"></script>

<flipper-widget theme="dark" partner="acme"></flipper-widget>

<script>
  const flipper = document.querySelector("flipper-widget");
  flipper.provider = window.ethereum; // any EIP-1193 provider; leave it unset for read-only

  // the user pressed Connect (or Flip) while disconnected: open your wallet UI
  flipper.addEventListener("connect-request", (e) => {
    e.preventDefault(); // you handle it
    window.ethereum
      ?.request({ method: "eth_requestAccounts" })
      .then(() => flipper.refresh()); // re-read accounts, chain and balances
  });

  flipper.addEventListener("flip-settled", (e) => {
    // "won" | "lost" | "refunded", and the payout in wei
    console.log(e.detail.outcome, e.detail.payout);
  });
</script>

For ES modules, use flipper-widget.esm.js next to it. To self-host, the same file is served at https://flipper.family/embed/flipper-widget.js.

HTML
<script type="module">
  import "https://cdn.jsdelivr.net/npm/@flipperdotfamily/widget@0/dist/cdn/flipper-widget.esm.js";
</script>

Full example: examples/vanilla-html ↗

Wallet wiring

The host owns the wallet. The widget has no wallet modal and never holds keys. It reads the chain through its own RPC and uses your wallet only to send transactions the user confirms. Give it one of:

Wallet inputs
InputWhatNotes
providerAny EIP-1193 providerwindow.ethereum, a wagmi connector's getProvider(), WalletConnect, AppKit, Privy, Dynamic, Coinbase… The widget follows its accountsChanged, chainChanged and disconnect. Call el.refresh() right after your app connects it.
walletClientA viem WalletCliente.g. wagmi's useWalletClient().data. Replace it when the account changes (wagmi does that for you).
onConnectRequestor the connect-request eventA disconnected user pressed Connect, Flip or List, so open your wallet UI. The event is cancelable. With no callback and no preventDefault(), a provider without accounts is asked for eth_requestAccounts.

On the wrong chain the button reads “Switch to Robinhood Chain” and sends wallet_switchEthereumChain, adding the chain with wallet_addEthereumChain when the wallet answers 4902.

Pass wagmi's WalletClient or the active connector's EIP-1193 provider, and open your connect UI from onConnectRequest.

walletClient
"use client";
import { FlipperWidget } from "@flipperdotfamily/react";
import { useWalletClient } from "wagmi";

export function Flip({ openConnect }: { openConnect: () => void }) {
  // wagmi hands you a new client when the account or chain changes
  const { data: walletClient } = useWalletClient();
  return <FlipperWidget walletClient={walletClient} onConnectRequest={openConnect} />;
}
connector.getProvider()
"use client";
import { useEffect, useState } from "react";
import { FlipperWidget } from "@flipperdotfamily/react";
import { useAccount } from "wagmi";

export function Flip({ openConnect }: { openConnect: () => void }) {
  const { connector, isConnected } = useAccount();
  const [provider, setProvider] = useState<unknown>(null);

  useEffect(() => {
    if (!isConnected || !connector) return setProvider(null);
    let live = true;
    // the connector's EIP-1193 provider
    void connector.getProvider().then((p) => live && setProvider(p));
    return () => {
      live = false;
    };
  }, [connector, isConnected]);

  return <FlipperWidget provider={provider} onConnectRequest={openConnect} />;
}

Iframe embed

The hosted embed at https://flipper.family/embed is the same <flipper-widget> on a full-bleed page with no wallet of its own. Use it to keep the widget's code out of your page (a strict CSP, framework isolation, a site builder). It borrows your wallet over a postMessage bridge and reports its events and height back to you.

URL parameters

Every parameter is optional.

Text
https://flipper.family/embed?chain=4663&theme=dark&accent=4cc2ff&partner=acme
Embed URL parameters
ParamValuesDefaultMeaning
chain4663, 31337 or robinhood, localthe site's deploymentChain to flip on. Unknown chains show "not live on this chain".
tokentoken address$FLIPPERToken selected at start.
tokenscomma-separated addressesevery tokenAllowlist for the picker. One address means no picker.
modepicker / singlepickersingle: one fixed token (token required), no picker.
hidePicker1 / trueoffDeprecated: use mode=single.
fitauto / fillautofill: the widget fills a fixed-size frame (no auto-height).
sizesm / md / lg / autoautoScale on top of the fluid layout.
details1 / trueoffWin chance / payout / fee line under the button.
tagline1 / true, or textnoneIdle headline under the coin: true = the built-in one, text = yours.
themelight, dark, autoautoColour mode; auto follows prefers-color-scheme.
accenthex, # optional (4cc2ff, %23ff5a1f)flipper sky blueAccent colour; text on it is picked for contrast.
radiuspx, 0–4024Card corner radius.
branding0 / falseonRemoves flipper.family marks (footer link, dolphin coin faces).
partner[A-Za-z0-9._:-]{1,64}noneAttribution id: echoed in every event, sent as X-Flipper-Partner. A registered partner code also attributes flips onchain (ERC-8021).
localeen, esenBuilt-in strings; unknown locales fall back to English.
compact1 / trueoffCompact layout: small inline coin, denser spacing.
approvalmax, exactmaxAllowance to request when it's short.
connectevent, requesteventrequest also sends eth_requestAccounts on Connect, for hosts that forward every RPC to a provider.
hostOriginan originnoneIframes: pins the parent's origin. Other origins are dropped, and the embed posts only to this one.
configbase64url JSONnoneFull FlipperEmbedConfig (brandName, brandLogo, coinImage, strings, min/maxAmount, rpcUrl, addresses…). Wins over the params.

mountFlipperIframe

@flipperdotfamily/widget/host creates the iframe and runs the host side of the bridge. It pins hostOrigin to your origin, checks every message's source and origin, forwards wallet requests to your provider (refusing methods outside the bridge's list with 4200), pushes account and chain changes, and sizes the iframe from the widget's resize events.

TypeScript
import { mountFlipperIframe } from "@flipperdotfamily/widget/host";

const embed = mountFlipperIframe({
  container: "#flipper", // element or selector
  // any EIP-1193 provider; null = read-only until setProvider()
  provider: window.ethereum,
  // the URL params below
  params: { theme: "dark", accent: "#ff5a1f", partner: "acme" },
  // anything else in FlipperEmbedConfig, sent as ?config=
  config: { brandName: "Acme", tagline: "Double or nothing on Acme" },
  // default: provider.request({ method: "eth_requestAccounts" })
  onConnectRequest: () => openMyWalletModal(),
  onEvent: (name, data) => console.log(name, data), // every widget event
});

// later
embed.setProvider(nextProvider);     // after your user connects or switches wallets
embed.setConfig({ theme: "light" }); // live config
embed.destroy();

For a fixed-size frame (a sidebar, dashboard tile or full-screen panel), pass fit: "fill". The iframe fills its container, which needs a height, and the widget lays out inside it from about 240×360 up.

TypeScript
mountFlipperIframe({ container: "#tile", provider, fit: "fill", params: { mode: "single", token: "0x…your token" } });
// #tile { width: 360px; height: 480px }

Without a bundler, load https://flipper.family/embed/flipper-host.js (also on jsDelivr as https://cdn.jsdelivr.net/npm/@flipperdotfamily/widget@0/dist/cdn/flipper-host.js) and call the global:

HTML
<div id="flipper"></div>
<script src="https://flipper.family/embed/flipper-host.js"></script>
<!-- or https://cdn.jsdelivr.net/npm/@flipperdotfamily/widget@0/dist/cdn/flipper-host.js -->
<script>
  FlipperEmbedHost.mountFlipperIframe({
    container: "#flipper",
    provider: window.ethereum,
    params: { theme: "auto", partner: "acme" },
  });
</script>

The raw protocol

Writing your own host? Every message is a JSON object with v: 1 and a source: "flipper" from the embed, "flipper-host" from you. Ignore anything else.

Bridge messages
typeDirectionShape
rpcembed → host{ id, method, params }: a wallet method only (reads use the embed's own RPC). Answer each id exactly once.
eventembed → host{ name, data }: ready, connect-request, flip-requested, flip-settled, payout-resolved, listing, error, resize.
rpc-resulthost → embed{ id, result }
rpc-errorhost → embed{ id, error: { code, message, data? } } with EIP-1193 codes: 4001 rejected, 4100 unauthorized, 4200 unsupported, 4902 unknown chain.
wallethost → embed{ accounts, chainId } after ready and on every change (accounts: [] when disconnected; chainId as hex).
confighost → embedAny FlipperEmbedConfig fields at the top level, merged live (sync dark mode, preselect a token).

The embed may send these wallet methods, and refuses anything else before it reaches you:

  • eth_accounts
  • eth_chainId
  • eth_requestAccounts
  • eth_sendTransaction
  • wallet_switchEthereumChain
  • wallet_addEthereumChain
  • wallet_watchAsset
  • wallet_getCapabilities
  • wallet_sendCalls
  • wallet_getCallsStatus

The three EIP-5792 methods are optional. Answer 4200 and the widget falls back to sequential transactions. Transactions arrive fully specified (gas, EIP-1559 fees) and already simulated, so forward them unchanged. A minimal host:

JavaScript
const EMBED = "https://flipper.family";
// <iframe id="flipper" src="https://flipper.family/embed?partner=acme&hostOrigin=…">
const iframe = document.querySelector("iframe#flipper");
const provider = window.ethereum;
const send = (m) =>
  iframe.contentWindow.postMessage({ v: 1, source: "flipper-host", ...m }, EMBED);
const pushWallet = async () =>
  send({
    type: "wallet",
    accounts: await provider.request({ method: "eth_accounts" }),
    chainId: await provider.request({ method: "eth_chainId" }),
  });

window.addEventListener("message", async (e) => {
  if (e.source !== iframe.contentWindow || e.origin !== EMBED) return; // only our iframe
  const m = typeof e.data === "string" ? JSON.parse(e.data) : e.data;
  if (m?.v !== 1 || m.source !== "flipper") return;

  if (m.type === "rpc") {
    try {
      const result = await provider.request({ method: m.method, params: m.params });
      send({ type: "rpc-result", id: m.id, result });
    } catch (err) {
      const error = { code: err.code ?? -32603, message: err.message ?? "Request failed" };
      send({ type: "rpc-error", id: m.id, error });
    }
  } else if (m.type === "event") {
    if (m.name === "ready") pushWallet();
    if (m.name === "connect-request") openYourConnectModal(); // then pushWallet()
    if (m.name === "resize") iframe.style.height = m.data.height + "px";
  }
});
provider.on("accountsChanged", pushWallet);
provider.on("chainChanged", pushWallet);

Lifecycle, error codes, native transports and the full security model are in BRIDGE.md.

Mobile apps

Each mobile SDK loads the hosted embed (https://flipper.family/embed) in the platform's WebView and hands every wallet request to your app's wallet (WalletConnect / Reown AppKit, an embedded wallet or your own signer). The page never sees keys, and your wallet shows its own confirmation for every transaction.

Mobile packages
PlatformPackageInstall
React Native / Expo@flipperdotfamily/react-native ↗npx expo install @flipperdotfamily/react-native react-native-webview
Flutterflipper_family ↗flutter pub add flipper_family
iOS (SwiftUI / UIKit)FlipperWidget ↗SPM https://github.com/flipperdotfamily/FlipperWidget-iOS, or pod 'FlipperWidget'
Android (Compose / Views)family.flipper:widget ↗implementation("family.flipper:widget:0.1.0")

Every SDK takes the same options as the embed URL (chain, token, tokens, theme, accent, radius, branding, partner, locale, compact, mode, fit, details, tagline), plus the full config object for white-labelling (brandName, brandLogo, coinImage, strings, …). Changes apply live, the widget sizes itself to its content, and it reports the same typed events as the web component.

React Native + Reown AppKit
import { FlipperWidget } from "@flipperdotfamily/react-native";
import { useAccount, useAppKit, useProvider } from "@reown/appkit-react-native";

export function Flip() {
  const { provider, providerType } = useProvider();
  const { address, chainId, isConnected } = useAccount();
  const { open } = useAppKit();

  return (
    <FlipperWidget
      wallet={isConnected && providerType === "eip155" ? provider : null}
      address={address ?? null}
      walletChainId={chainId ?? null} // number or CAIP-2 ("eip155:4663")
      theme="dark"
      accent="#ff5a1f"
      partner="acme"
      onConnectRequest={() => open()}
      onFlipSettled={(f) => console.log(f.outcome)}
    />
  );
}

Prefer a fully native UI? useFlipperHeadless() from @flipperdotfamily/react-native/headless gives you balances, previews and flip() on top of @flipperdotfamily/sdk.

Security. The SDKs load only the https embed (http localhost in debug builds), keep navigation on the embed, open other links in the system browser, accept bridge messages only from the embed's main frame, and by default forward only seven wallet methods (plus the batch-call ones if you enable them), which you can narrow. Never auto-approve bridge requests.

The protocol is in BRIDGE.md, and each SDK's README covers wallet wiring, theming, events and troubleshooting. For a host of your own (Capacitor, Tauri, a custom WebView), implement the host side of the bridge described there.

Theming and white-label

Picker or single token

mode="picker" (the default) lets the user choose among listed tokens, and tokens narrows the list. mode="single" fixes one token and drops the picker, leaving a plain amount input with the token's logo and symbol beside it. Single mode needs token (an address or "ETH"), or the widget shows a configuration error rather than guessing. hidePicker still works as a deprecated alias.

HTML
<flipper-widget mode="single" token="0x…your token"></flipper-widget>

Headline and details

By default the widget shows only the coin, the amount with a small balance line, and the button. When a token's swap fees trim a flip's odds below usual, a quiet ↓ Odds 0.9 pts below usual appears beside the balance (Odds −0.9 pts when narrow). It shows the deviation, never the odds. Turn on details for win chance, payout and fee under the button. Set tagline for a headline under the coin: true for the built-in one, or your own text.

HTML
<flipper-widget details tagline="Double or nothing on Acme"></flipper-widget>

Any size

The widget lays out from its own box (container queries), not the viewport, scaling type, spacing and the coin fluidly. It tightens below about 300 px, and from 640 px the coin moves beside the form (the compact variant becomes a one-line bar from 960 px). Height follows the content, reported by resize events. fit="fill" takes the element's height too, for fixed-size cards, sidebars and full-bleed panels; the coin scales with the height, and extras drop away on short boxes. size (sm / md / lg) scales everything. The token picker is a sheet inside the widget, so it always fits.

HTML
<!-- a fixed-size card -->
<flipper-widget fit="fill" style="width: 320px; height: 520px"></flipper-widget>

<!-- fill a sidebar -->
<aside style="height: 100vh">
  <flipper-widget fit="fill" mode="single" token="ETH"></flipper-widget>
</aside>

Theme

Three layers, and later ones win:

  1. CSS custom properties from your stylesheet: flipper-widget { --flipper-accent: #ff5a1f; }.
  2. The theme object (the theme property, typed FlipperTheme), plus the accent and radius shorthands.
  3. ::part() for anything else.

Playground

The real widget, loaded from /embed/flipper-widget.js. Change the settings and copy the code below.

Loading the widget…
Mode
Accent

default

24px
Variant
Tokens
Density
Detailswin chance, payout, fee
flipper branding
Locale

Wallet: none detected, so the widget runs read-only: prices, odds and the token list, no flips.

Events

  1. waiting for the first event…

Code for these settings

HTML
<script src="https://cdn.jsdelivr.net/npm/@flipperdotfamily/widget@0/dist/cdn/flipper-widget.js"></script>

<flipper-widget
  theme="dark"
  partner="acme"
></flipper-widget>

<script>
  const flipper = document.querySelector("flipper-widget");
  flipper.provider = window.ethereum; // or your wallet kit's EIP-1193 provider
</script>

The theme object

Every field is optional; unset colours keep flipper's palette for the current mode.

TypeScript
flipper.theme = {
  mode: "auto",                  // "light" | "dark" | "auto" (follows prefers-color-scheme)
  accent: "#ff5a1f",             // text on it is picked for contrast (or set accentText)
  background: "#fff8f2", surface: "#fff", field: "#fbefe6", border: "#0000001a",
  text: "#1b1109", textMuted: "#1b110999", textSubtle: "#1b110966",
  win: "#c77700", loss: "#d23a2e",
  radius: 16,                    // px or any CSS length; inner controls scale from it
  fontFamily: "inherit",         // use the host page's font
  displayFontFamily: "Georgia, serif", monoFontFamily: "ui-monospace, monospace",
  density: "compact",            // "compact" | "comfortable" | "spacious"
  coinSize: 96, shadow: "none", borderWidth: 0, maxWidth: "none",
  dark: { background: "#140c06" }, // per-mode overrides (also: light)
};

CSS custom properties

Set them on the element from your CSS. Colours default to flipper's palettes, and accentText is computed for contrast when you set only accent. The --flipper-check* properties colour the picker's checkmarks and have no theme key. --flipper-check is for tokens flipper whitelists (it follows the accent), and --flipper-check-launchpad for verified launchpad launches.

PropertyTheme keyDarkLight
--flipper-accentaccent#4cc2ff#0a6aa2
--flipper-accent-textaccentText#031a2b#ffffff
--flipper-bgbackground#0a1b26#ffffff
--flipper-surfacesurface#0e2230#f5f9fc
--flipper-fieldfield#13293a#eef4f8
--flipper-borderborder#cdeeff17#0e3a5a1c
--flipper-texttext#eaf6fb#0b2239
--flipper-text-mutedtextMuted#eaf6fb99#0b2239a6
--flipper-text-subtletextSubtle#eaf6fb66#0b223980
--flipper-winwin#f5c451#946200
--flipper-lossloss#ff6b5e#c7372c
--flipper-checkCSS only (follows the accent)#4cc2ff#0a6aa2
--flipper-check-launchpadCSS only#d4fc50#6b8a00
Layout and type custom properties
PropertyTheme keyDefault
--flipper-radiusradius24px (inner controls scale from it)
--flipper-border-widthborderWidth1px; 0 drops the card border
--flipper-shadowshadowa soft float; "none" drops it
--flipper-backdropCSS onlya faint light from the top, on the default colours only (setting --flipper-bg turns it off); "none" drops it, any background-image replaces it
--flipper-max-widthmaxWidth460px; "none" fills the host
--flipper-fontfontFamilyManrope when your page loads it, else the system stack; "inherit" uses your page's font
--flipper-font-displaydisplayFontFamilySatoshi when loaded, else the UI font
--flipper-font-monomonoFontFamilyJetBrains Mono when loaded, else ui-monospace
--flipper-coin-sizecoinSizescales with the widget's width, 84–124px (52px when compact)
CSS
/* layer 1: custom properties from your stylesheet */
flipper-widget {
  --flipper-accent: #ff5a1f;
  --flipper-radius: 12px;
  --flipper-font: "Inter", system-ui, sans-serif;
}

/* layer 3: ::part() for anything else */
flipper-widget::part(cta) { text-transform: uppercase; letter-spacing: 0.04em; }
flipper-widget::part(card) { border: 2px solid #1b1109; box-shadow: 6px 6px 0 #1b1109; }

::part() hooks

Style anything the theme doesn't cover with flipper-widget::part(name):

  • root
  • card
  • header
  • brand
  • account
  • coin
  • status
  • result
  • payout
  • field
  • token-button
  • amount-input
  • max-button
  • balance
  • odds
  • note
  • cta
  • details
  • footer
  • picker
  • picker-search
  • picker-row
  • check
  • check-launchpad
  • trigger
  • modal

Variants and density

card (the default) has a header, the 3D coin and the form. compact puts a small coin inline with denser spacing, for sidebars and feeds. button renders a trigger that opens the card in a native <dialog> (el.open() / el.close() work too). Separately, theme.density sets the spacing scale: compact, comfortable (default) or spacious. The layout follows the widget's own width down to 280 px.

HTML
<!-- default: header, 3D coin, form -->
<flipper-widget variant="card"></flipper-widget>

<!-- small inline coin, denser -->
<flipper-widget variant="compact"></flipper-widget>

<!-- a trigger that opens the card in a modal -->
<flipper-widget variant="button" button-label="Flip FLIPPER"></flipper-widget>

Fonts

The widget doesn't download fonts. It asks for Manrope (UI), Satoshi (headlines) and JetBrains Mono (numbers), and falls back to system stacks if your page hasn't loaded them. fontFamily: "inherit" uses your page's font, and displayFontFamily and monoFontFamily set the other two.

White-label

branding="false" removes flipper's marks (the header dolphin, the dolphin and fluke coin faces, and the “Powered by” footer). Then add your name (brandName), logo (brandLogo, square, 64 px or more), coin faces (coinImage, coinImageTails; with branding off, the logo doubles as heads), accent and copy (strings).

HTML
<flipper-widget
  branding="false"
  brand-name="Acme"
  brand-logo="https://acme.example/logo.svg"
  coin-image="https://acme.example/coin-heads.png"
  coin-image-tails="https://acme.example/coin-tails.png"
  accent="#ff5a1f"
  tagline="Double or nothing on Acme"
  partner="acme"
></flipper-widget>

Strings and locales

locale picks a built-in table (en, es); strings overrides any key on top of it. {placeholders} are filled in at render time.

TypeScript
flipper.locale = "es"; // built-in: "en" (default), "es"; "es-MX" falls back to "es"
flipper.strings = {
  flip: "Flip {amount} {symbol}!",      // {placeholders} are filled in at render time
  won: "Nice.",
  tagline: "Double or nothing on Acme", // shown with tagline={true} (off by default)
};
Every string key, with its English default (121)
brand
flipper
connect
Connect wallet
connectShort
Connect (the compact header's connect button)
connecting
Connecting…
switchChain
Switch to {chain}
switching
Switching…
wrongNetwork
Wrong network
tagline
Double or nothing
taglineSub
Heads pays {multiple} in {symbol}. Provably fair.
loading
Loading…
notLive
Not live on this network yet
unreachable
Can't reach {chain}
paused
Flipping is paused
locked
Flips are paused (the button while the drawdown breaker has the protocol locked)
lockedNote
Flips are paused while the treasury is protected. In-flight flips settle after it reopens.
enterAmount
Enter an amount
belowMin
Minimum is {amount} {symbol}
aboveMax
Maximum is {amount} {symbol}
insufficient
Not enough {symbol}
needFee
Need {native} for the randomness fee
pricing
Pricing…
cantPrice
Couldn't price this flip
flip
Flip {amount} {symbol}
flipAgain
Flip again
selectToken
Select a token
tokenNotSet
No token configured
singleNeedsToken
Single-token mode needs a token: set the `token` option to a token address or "ETH".
notFlippable
{symbol} can't be flipped
stepPreviewing
Checking the odds…
stepApproving
Approve {symbol} in your wallet
stepApproveSent
Approving {symbol}…
stepSigning
Confirm the flip in your wallet
stepFlipSent
Flipping…
stepWrapping
Confirm wrapping ETH in your wallet
stepWrapSent
Wrapping ETH…
stepBatch
Confirm in your wallet
drawing
Drawing…
stillDrawing
Still drawing…
waitingRandomness
Waiting for randomness
slowRandomness
Randomness is slow right now. Your flip is safe: it settles as soon as it arrives.
landing
Landing…
heads
Heads.
tails
Tails.
won
You won.
lost
Not this time.
refunded
Refunded.
resultWon
{amount} {symbol} was sent to your wallet.
resultWonClaim
{amount} {symbol} is ready to claim on flipper.family (this flip settled in safe mode).
resultWinPending
Your {stake} stake is back. Your winnings are being settled and will arrive shortly.
resultWonFallback
Your {stake} stake is back, and your winnings were paid as {bonus}.
resultLost
Your {stake} stake went to the house.
resultRefunded
The randomness never arrived, so the flip was cancelled and your {stake} stake was returned.
settling
settling winnings…
wethPayout
Winnings arrive as WETH.
payoutSettling
Payout being settled
payoutOwed
{amount} {symbol} owed
retryPayout
Retry payout
deferredTitle
Settling after pause (a flip whose randomness arrived while the protocol was locked)
deferredBody
The coin landed while flips were paused. Your stake is safe: this flip settles once the treasury reopens.
deferredCta
Waiting for the treasury to reopen
settleNow
Settle now
settlingNow
Settling…
settleReady
Settle this flip now (anyone can)
settleWaiting
It can be settled once the treasury reopens.
payingOut
Paying out…
payoutReady
Pay out the winnings now (anyone can)
payoutChecking
Checking whether the payout can go through…
payoutNotYet
Can't be bought right now.
payoutAuto
Automatic payout by {time} (in {duration})
payoutDue
Automatic payout is due now
payoutAlreadyPaid
Already paid out.
payoutPaid
Paid out: {amount} {symbol}
pendingWinsOne
A win is being paid out
pendingWinsMany
{count} wins are being paid out
amountLabel
Amount of {symbol} to flip
balance
Balance
max
MAX
winChance
Win chance
pays
Pays
fee
Fee
oddsBelow
Odds {points} pts below usual
oddsBelowShort
Odds −{points} pts
oddsBelowHelp
This token's swap fees are high, so the house trims the win chance instead of the payout: {points} percentage points below the usual odds. Smaller stakes are trimmed less.
chooseToken
Choose a token
searchTokens
Search name, ticker or address
sectionListed
Flippable
sectionEligible
Listable
sectionUnsupported
Not supported
verifiedBy
Verified by {sources} (deprecated, no longer shown)
checkListed
Whitelisted by flipper (tooltip of a picker row's accent checkmark)
checkLaunchpad
Verified launch (tooltip of the launchpad checkmark)
nativeEth
Native ETH, flipped as WETH (tooltip of the ETH row)
houseToken
House token · {multiple} payout (tooltip of the $FLIPPER row ({multiple}: the live $FLIPPER payout, e.g. 2.05×))
noResults
No tokens match “{query}”
loadingTokens
Loading tokens…
tokensError
Couldn't load the token list.
retry
Retry
close
Close
listTitle
{symbol} isn't listed yet
listBody
Anyone can list it: one transaction, and it's flippable for everyone.
listButton
List {symbol}
listChecking
Checking its liquidity…
listConfirm
Confirm the listing in your wallet
listSending
Listing {symbol}…
listDone
{symbol} is listed. Flip away.
listBlocked
{symbol} can't be listed: {reason}
unsupported
{symbol} can't be flipped: {reason}
listingOff
{symbol} isn't listed on flipper yet.
unwrapNote
You have {amount} WETH.
unwrap
Unwrap to ETH
unwrapping
Unwrapping…
unwrapped
Unwrapped to ETH.
poweredBy
Powered by
viewTx
View transaction
addToWallet
Add {symbol} to wallet
openWidget
Flip
coinIdle
Coin
coinSpinning
Coin flipping, waiting for randomness
coinHeads
Coin landed heads: you won
coinTails
Coin landed tails: you lost
dialogLabel
Coin flip

Source: packages/widget/src/strings.ts

Config and events

Properties take typed values; attributes are kebab-case strings. Every option is optional. The same names are the framework wrappers' props and the fields of the embed's config.

Options

Widget options
Property / attributeTypeDefaultNotes
providerproperty onlyEIP-1193 providernoneSigns transactions; reads never use it. The widget follows its accountsChanged, chainChanged and disconnect.
walletClientproperty onlyviem WalletClientnoneInstead of provider, e.g. wagmi's useWalletClient().data. Replace it when the account changes.
onConnectRequestproperty only(detail) => voidnoneA disconnected user pressed Connect, Flip or List: open your wallet UI.
chainIdchain-idnumber (attr also robinhood, local)4663Robinhood Chain.
rpcUrlrpc-urlstringthe chain's public RPCReads only.
apiUrlapi-urlstring | nullthe deployment'sflipper API (token list, logos). null / "none": onchain token list only.
deploymentUrldeployment-urlstring | nullflipper.family manifestLive addresses. Not fetched when addresses has house and lens.
addressesaddresses (JSON)objectfrom the manifest{ house, lens, flipper, v4Adapter, v3Adapter, weth, … }
tokentokenaddress | "ETH"$FLIPPERSelected at start.
tokenstokens (comma list)string[]every tokenAllowlist of addresses (and/or "ETH"). One entry means no picker.
modemode"picker" | "single""picker"picker: the user chooses the token (tokens narrows the list). single: one fixed token, shown as a label on the amount row, with no picker. Needs token, or a configuration error shows.
hidePickerhide-pickerbooleanfalse**Deprecated**: use mode="single". Without token it falls back to $FLIPPER.
ethethbooleantrueOffer native ETH (flipped as WETH).
listinglistingbooleantrueListing qualifying tokens (whitelisted by flipper) from the picker.
minAmountmin-amountdecimal stringnoneMinimum stake in token units ("10").
maxAmountmax-amountdecimal stringnoneMaximum stake in token units.
approvalapproval"max" | "exact""max"Allowance requested when it's short. max lets later flips skip the approval.
variantvariant"card" | "compact" | "button""card"button renders a trigger that opens the card in a modal.
fitfit"auto" | "fill""auto"auto: width from the container, height from the content. fill: the element's full width and height (give it a height), for fixed-size cards, sidebars and full-bleed panels.
sizesize"sm" | "md" | "lg" | "auto""auto"A scale on top of the fluid layout (0.88×, 1×, 1.14×).
detailsdetailsbooleanfalseWin chance, payout and randomness fee under the button. When off, the widget only flags odds that fees trim below usual ("↓ Odds 0.9 pts below usual"), never the odds themselves.
taglinetagline (bare = true)string | booleannoneA headline under the coin while idle. true: the built-in one ("Double or nothing" and its payout line, from strings.tagline / strings.taglineSub); a string: your own line.
themetheme (mode or JSON)mode | FlipperTheme"auto"Colour mode, or the full theme object.
accentaccentcolourflipper sky blueShorthand for theme.accent.
radiusradiuspx, 0–4024Shorthand for theme.radius.
brandingbrandingbooleantruefalse removes flipper marks: header dolphin, coin faces, footer.
brandNamebrand-namestring"flipper"Header name.
brandLogobrand-logoimage URLdolphinHeader logo; also the heads face when branding is false and there's no coinImage.
coinImagecoin-imageimage URLdolphinHeads face (square; transparent PNG or SVG works best).
coinImageTailscoin-image-tailsimage URLflukeTails face.
buttonLabelbutton-labelstring"Flip" + the tokenTrigger label for variant="button", e.g. "Flip FLIPPER".
localelocale"en" | "es""en"Built-in string table. Regional tags fall back (es-MX → es).
stringsproperty onlyPartial<FlipperStrings>noneOverride any string (keys below).
reducedMotionreduced-motionbooleanOS settingForce reduced motion on or off.
partnerpartner[A-Za-z0-9._:-]{1,64}noneAttribution: echoed in every event and sent as X-Flipper-Partner on API calls. A partner code registered in the PartnerRegistry (1–32 of a-z 0-9 _ -, open to anyone) also attributes every flip onchain (ERC-8021), earns a share of it and can give your players better odds.

Methods

Element members
MemberDoes
open() / close()Button variant: open or close the modal.
refresh()Re-read the wallet's accounts, chain and balances. Call it right after your app connects the provider or moves the user's funds.
clientThe underlying @flipperdotfamily/sdk client (read-only use; recreated when the chain or wallet changes).

Events

CustomEvents dispatched on the element. They don't bubble, so listen on the element itself. Every detail is JSON-safe (amounts are wei as decimal strings), and every payload except resize carries your partner. The embed bridge sends the same payloads. In TypeScript, onFlipperEvent from @flipperdotfamily/widget types the detail and avoids the clash between ready / error / resize and DOM event types:

TypeScript
import { onFlipperEvent } from "@flipperdotfamily/widget";

const seen = new Set<string>();
const off = onFlipperEvent(flipper, "flip-settled", (d) => {
  if (d.pending) return; // WinPending: a final flip-settled for this flipId follows
  if (seen.has(d.flipId)) return;
  seen.add(d.flipId);
  track(d.outcome, d.payout, d.partner);
});
// later: off();

ready

Once, when the widget has loaded its deployment (or failed to).

ready payload
FieldTypeMeaning
versionstringWidget semver.
chainIdnumber | nullThe chain it runs on.
accountaddress | nullConnected account; null without a wallet.
tokenstring | nullSelected token address, or "ETH".
variantcard | compact | buttonThe rendered variant.
partnerstring | nullYour partner id.

connect-request

A disconnected user pressed Connect, Flip or List. Cancelable: call preventDefault() when you open your own wallet UI.

connect-request payload
FieldTypeMeaning
reasonconnect | flip | listWhat they pressed.
partnerstring | nullYour partner id.

flip-requested

The flip transaction is mined and randomness requested. The coin is spinning.

flip-requested payload
FieldTypeMeaning
flipIdstringOnchain flip id (decimal).
accountaddressThe player.
tokenaddressThe staked token (WETH for native ETH flips).
symbolstringIts symbol.
decimalsnumberIts decimals.
amountwei stringThe stake.
winChanceBpsnumberWin chance in basis points (4500 = 45%).
randomnessFeewei stringRandomness fee paid in the native token.
txHashhashThe flip transaction.
approveTxHashhash | nullThe approval, when one was needed.
nativebooleanThe stake was native ETH, wrapped to WETH.
partnerstring | nullYour partner id.

flip-settled

The coin landed. With pending: true, a second flip-settled for the same flipId follows when the winnings are paid (alongside payout-resolved). De-duplicate by flipId; the last one is final. A flip whose randomness arrived while the drawdown breaker had the protocol paused gets its flip-settled only once it settles after the reopen. Until then the widget shows it as settling after the pause.

flip-settled payload
FieldTypeMeaning
flipIdstringOnchain flip id (decimal).
accountaddressThe player.
tokenaddressThe staked token (WETH for native ETH flips).
symbolstringIts symbol.
decimalsnumberIts decimals.
amountwei stringThe stake.
outcomewon | lost | refundedWhat to tell the player.
statussee noteWon, WonFallback (winnings paid in $FLIPPER), WinPending, Lost, LostInventory or Refunded.
wonbooleanThe flip won.
pendingbooleanWinPending: stake returned, winnings still owed. The widget shows a Retry payout button, though flipper's payout worker usually pays within about a second.
payoutwei stringReceived in payoutToken, stake included ("0" on a loss).
payoutTokenaddressWhat payout is denominated in.
flipperPaidwei string$FLIPPER paid on top (fallback wins).
txHashhash | nullThe settlement transaction, if it could be looked up.
requestTxHashhashThe flip transaction.
nativebooleanThe stake was native ETH.
partnerstring | nullYour partner id.

payout-resolved

A pending win's winnings were paid. Once per flip, alongside the final flip-settled.

payout-resolved payload
FieldTypeMeaning
flipIdstringOnchain flip id (decimal).
accountaddressThe player.
tokenaddressThe staked token (WETH for native ETH flips).
symbolstringIts symbol.
decimalsnumberIts decimals.
tokenPaidwei stringWinnings paid in token (the stake already came back at settlement).
flipperPaidwei string$FLIPPER paid instead: "0" unless the token still couldn't be bought after the pending timeout.
byself | otherself: this widget's Retry payout paid it. other: someone else did (usually flipper's payout worker).
nativebooleanThe stake was native ETH: token is WETH (the winnings are paid in WETH) and symbol is "ETH". false, with "WETH", for a pending win the widget only learned about from an earlier session.
txHashhash | nullThe PendingWinResolved transaction, if it could be looked up.
partnerstring | nullYour partner id.

listing

Each stage of a listing started from the picker.

listing payload
FieldTypeMeaning
stagestarted | submitted | listed | failedProgress.
tokenaddressThe token being listed.
symbolstringIts symbol.
venuev4 | v3 | nullWhich route adapter lists it.
txHashhash | nullOnce submitted.
errorstring | nullWhy it failed.
partnerstring | nullYour partner id.

error

Something failed. message is plain English and safe to show.

error payload
FieldTypeMeaning
codestringuser-rejected, rejected, insufficient-funds, revert, timeout, config, network, wallet or unknown.
messagestringPlain English.
contextstringconfig, wallet, preview, flip (also Retry payout) or listing.
partnerstring | nullYour partner id.

resize

The widget's size changed (iframe and WebView hosts size themselves from it).

resize payload
FieldTypeMeaning
widthnumberCSS pixels of the border box.
heightnumberCSS pixels of the border box.

Event names per framework

Event names per framework
DOM eventReact, SvelteVueAngular
readyonReady@ready(flipperReady)
connect-requestonConnectRequest@connect-request(connectRequest)
flip-requestedonFlipRequested@flip-requested(flipRequested)
flip-settledonFlipSettled@flip-settled(flipSettled)
payout-resolvedonPayoutResolved@payout-resolved(payoutResolved)
listingonListing@listing(flipperListing)
erroronError@error(flipperError)
resizeonResize@resize(flipperResize)

Headless SDK

@flipperdotfamily/sdk is what the widget runs on: a framework-agnostic viem client for deployments, previews, flips, native ETH, listing and settlement. Use it for your own UI, a script or a backend. Its only peer dependency is viem@^2.

Shell
npm i @flipperdotfamily/sdk viem
flip.ts
import {
  createFlipperClient, describeSettlement, flipperChain,
  resolveDeployment, walletClientFromProvider,
} from "@flipperdotfamily/sdk";
import { createPublicClient, http, parseUnits } from "viem";

const deployment = await resolveDeployment({ chainId: 4663 }); // Robinhood Chain: addresses, RPC, API
const chain = flipperChain(deployment);
const publicClient = createPublicClient({ chain, transport: http(deployment.rpcUrl) });
const [account] = await window.ethereum.request({ method: "eth_requestAccounts" });
const walletClient = walletClientFromProvider(window.ethereum, chain, account);

const flipper = createFlipperClient({
  publicClient,
  walletClient,
  addresses: deployment.addresses,
});

const house = await flipper.house();
const { flipId, receipt } = await flipper.flip({
  token: house.flipper,          // $FLIPPER; any listed token works
  amount: parseUnits("100", 18),
  approve: "max",                // default "exact"
  // previewing → approving → approve-sent → signing → flip-sent → requested
  onStep: (s) => console.log(s.step),
});

const settled = await flipper.waitForSettlement(flipId, { fromBlock: receipt.blockNumber });
const copy = describeSettlement(settled, { symbol: "FLIPPER", decimals: 18 });
console.log(copy.headline, copy.detail); // "You won." …
  1. 1

    resolveDeployment

    Returns the chain's addresses, RPC and API. Explicit addresses (with house and lens) win and nothing is fetched; otherwise it reads the live manifest (deploymentUrl: null forbids that).
  2. 2

    flip()

    Takes a fresh preview and throws a FlipperError if the house would reject. Then it approves if the allowance is short, simulates and sends flip with the preview's win chance as the minimum and a 5-minute deadline, and returns the flipId. The randomness fee goes with 20% headroom, which the house refunds.
  3. 3

    waitForSettlement

    Resolves when the flip leaves Pending. waitForResolution follows a WinPending flip until upkeep pays it, and describeSettlement gives player-facing copy for every status.

Native ETH

The house flips WETH. flipEth wraps and flips in one call. If the wallet supports atomic batching (EIP-5792), wrap, approve and flip go out as one wallet_sendCalls with one confirmation. Otherwise they're sequential (batch: "never" forces that). If WETH isn't listed yet it throws with details.errorName === "WethNotListed", and anyone can list it.

TypeScript
import { parseEther } from "viem";

const res = await flipper.flipEth({
  amount: parseEther("0.01"),
  approve: "max",
  // previewing → (batch-signing → batch-sent) or
  // (wrapping → wrap-sent → approving → approve-sent → signing → flip-sent) → requested
  onStep: (s) => console.log(s.step),
});
console.log(res.batched); // true: wrap + approve + flip went out as one EIP-5792 batch

const { flipId, receipt } = res;
const settled = await flipper.waitForSettlement(flipId, { fromBlock: receipt.blockNumber });
// winnings arrive as WETH: flipper.unwrapWeth(amount) turns them back into ETH

Listing a token

Only qualifying tokens can be listed: tokens and pools flipper whitelists, or a launchpad's launches vouched for by its own onchain verifier. Each venue has its own route adapter:

Listing venues
VenueAdapterCall
v4V4RouteAdapterregisterAndList(token, poolKey)
v3V3RouteAdapter (bridged into v4 by the V3BridgeHook)registerAndListV3(token, v3Pool)

You don't call the adapters directly. The flipper API knows each token's best pool, and checkListing / list pick the venue.

TypeScript
import { createFlipperApi, listingTargetFromApi } from "@flipperdotfamily/sdk";

// partner is sent as the X-Flipper-Partner header
const api = createFlipperApi({ url: deployment.apiUrl!, partner: "acme" });
const page = await api.tokens({ q: "tsla", limit: 40 }); // { tokens, sections, total, next }
const { token } = await api.token(page.tokens[0].address); // one token, every eligible pool

// { venue: "v4" | "v3", … } from the token's best pool; null without one
const target = listingTargetFromApi(token);
if (target) {
  // the adapter's check(), then a dry run; never throws
  const check = await flipper.checkListing(target);
  if (check.ok) await flipper.list(target, { onSent: (hash) => console.log("sent", hash) });
  else console.log(check.reason); // plain English; also check.code, check.routeCostBps
}

createFlipperApi makes only public, CORS-open reads with no credentials. They're rate-limited to 20 per second per end-user IP, so debounce searches.

Building your own UI

The widget's own building blocks: previews (odds, fee and whether the house accepts the stake), the odds-shift note, plain-English reject reasons, the displayed randomness fee and the largest stake at full odds. Every write rethrows a FlipperError with a plain-English message, and describeError(err) does the same for any viem error.

TypeScript
import { formatBps, oddsShift, rejectReason } from "@flipperdotfamily/sdk";

// eth_call: odds, fee, and whether the house accepts this stake
const pv = await flipper.preview(token, amount);
if (pv.code !== 0) showError(rejectReason(pv.code, { symbol })?.message);

// costly routes trim the odds, never the payout (from ~8% at the launch edge)
// terms: the base odds and payout now (the edge comes down as the protocol grows)
const { terms } = await flipper.house();
const shift = oddsShift(pv, terms);
showOdds(formatBps(pv.winChanceBps), shift.shifted ? shift.message : null);

// the fee to display; flip() sends it padded and the house refunds the rest
const fee = await flipper.displayRandomnessFee(token);
// the largest stake that still gets the base odds
const { amount: fullOdds } = await flipper.maxStake(token, balance, true);

In React, useFlipperClient from @flipperdotfamily/react builds the same client from a provider or wallet client (provider, walletClient, chainId, rpcUrl, addresses, deploymentUrl, all optional):

TSX
"use client";
import { useFlipperClient } from "@flipperdotfamily/react";

const { client, deployment, chain, loading, error } = useFlipperClient({
  provider,
  chainId: 4663, // Robinhood Chain
});

Everything else (holder rewards, claims, token discovery, gas-aware fees, ABIs) is in the SDK README.

Chains and contracts

Chains
ChainChain idStatuschain-idPublic RPC
Robinhood Chain4663Launch chain, the defaultrobinhoodhttps://rpc.mainnet.chain.robinhood.com
Local fork31337Developmentlocalhttp://127.0.0.1:8545

On any other chain the widget says it isn't live there yet. Addresses, RPCs and API URLs come from the deployment manifest at flipper.family/embed/deployment.json, which the widget and resolveDeployment() read unless you pin addresses.

This server has no deployment configured, so its manifest is empty.

Security

  • The wallet stays with you. The widget and the embed never hold keys and never ask for message signatures. Every transaction goes through your wallet's own confirmation.
  • Simulated first. Every transaction is simulated before it's sent, and flips carry a minimum win chance and a deadline, so a flip can't land at worse odds than shown.
  • Pin what you trust. Addresses come from addresses or from the manifest over HTTPS. Pin addresses (and rpcUrl) if you'd rather not trust the manifest at runtime.
  • No credentials. API calls send no cookies, keys or tokens, only X-Flipper-Partner. Your partner id is public by design.
  • Iframes. Pass hostOrigin (mountFlipperIframe does) so the embed only exchanges messages with your origin. On your side, check event.source === iframe.contentWindow and event.origin === "https://flipper.family", and refuse wallet methods outside the bridge's list with 4200.
  • Content Security Policy. The web component needs connect-src for the chain's RPC and the flipper API (and flipper.family for the manifest, unless you pin addresses), and img-src for token logos. The CDN script needs script-src cdn.jsdelivr.net, or self-host the file. The iframe embed needs only frame-src flipper.family (and script-src for flipper-host.js if you load it from a URL).
  • Native apps. Keep the WebView on flipper.family/embed and open other links in the system browser; the mobile packages do this for you.

FAQ

Does it work with server-side rendering?

Yes. @flipperdotfamily/react renders the tag on the server and upgrades it on the client, so Next.js server components need no dynamic import. Importing @flipperdotfamily/widget is a no-op on the server; the element registers in the browser.

How big is it?

The single-file CDN build is about 113 KB gzipped (95 KB brotli), with viem, lit and the SDK bundled. The npm package is ESM and shares viem and lit with your app, so with a bundler it adds less.

Which chains are supported?

Robinhood Chain (4663) is the launch chain and the default; 31337 is a local fork for development. See Chains and contracts.

How do the odds work?

At launch, a 45% win chance paying 2× in the staked token, or 2.05× on $FLIPPER flips. The house edge falls from 10% to 5% as the protocol grows (47.5% at 2× on every flip), and each flip keeps the terms it was made at. Very expensive swap routes get slightly lower odds instead of a lower payout, and the widget says so before the user signs. The full model is on the main docs page.

ETH or WETH?

The house flips WETH. With eth on (the default) the picker offers native ETH: the widget wraps it, approves and flips, in one confirmation when the wallet supports EIP-5792 batching. Winnings arrive as WETH, and the widget offers to unwrap them.

Can my users list new tokens?

Qualifying tokens, yes. Tokens flipper whitelists that aren't enabled yet show a “List” button in the picker, but arbitrary tokens can't be listed. Set listing to false to hide the button, or restrict the picker with tokens (one entry fixes the token).

Can I use my own tag name?

Import the class from @flipperdotfamily/widget/element (it doesn't register anything) and define it under any name:

TypeScript
import { FlipperWidget } from "@flipperdotfamily/widget/element"; // the class, not registered

customElements.define("acme-flip", class extends FlipperWidget {});
// <acme-flip partner="acme"></acme-flip>
Can I put several widgets on one page?

Yes. Each element has its own configuration, wallet and events; load the script once. Events don't bubble, so listen on each element.

What does my Content Security Policy need?

connect-src for the RPC and flipper API, img-src for token logos, and script-src cdn.jsdelivr.net for the CDN script (details under Security). Can't loosen your CSP? The iframe embed only needs frame-src.

Does it respect reduced motion?

Yes. With prefers-reduced-motion the toss becomes a fade; reducedMotion forces it on or off. Every control is keyboard-reachable, and results are announced through a live region.

How do I test locally?

Point the widget at a local deployment: chain-id="local" (31337) with deployment-url="http://localhost:3000/embed/deployment.json", or pin addresses and rpc-url="http://127.0.0.1:8545". The SDK takes the same options in resolveDeployment().

What happens when the wallet is on the wrong chain?

The button reads “Switch to Robinhood Chain” and asks the wallet to switch, adding the chain first if the wallet doesn't know it. Iframe and native hosts forward those two requests and then send a wallet message with the new chain.