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. Each code sits in a tier that earns 10%, 20% or 30% 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). - Registration needs approval. 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, then ask the flipper team to approve it into a tier. - Same attribute everywhere. Set
partnerto your code on the widget, embed or SDK client. An id that isn't an approved code still works for reporting, at normal odds.
import { createFlipperClient } from "@flipperdotfamily/sdk";
// your approved 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
| Path | Best for | Wallet | Start |
|---|---|---|---|
| Web component | Any site or framework, including plain HTML. One script tag or npm import. | Pass provider or walletClient | Script tag |
| Framework wrapper | React / Next.js, Vue, Svelte, Angular: typed props, framework-native events, SSR-safe React. | Props, from your wallet kit | React |
| Iframe embed | Strict CSPs, site builders, or keeping third-party code out of your page. | Over the bridge (mountFlipperIframe) | Iframe embed |
| Native | React Native, Flutter, iOS and Android apps. | Your app's wallet, over the bridge | Mobile apps |
| Headless SDK | Your own UI, bots, backends and analytics. | A viem WalletClient | Headless 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.
<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.
<script type="module">
import "https://cdn.jsdelivr.net/npm/@flipperdotfamily/widget@0/dist/cdn/flipper-widget.esm.js";
</script>Full example: examples/vanilla-html ↗
The ESM package, for bundlers. Importing it registers the element (a no-op on the server) and types document.createElement("flipper-widget"). onFlipperEvent gives typed listeners, including for ready, error and resize, whose names clash with DOM event types.
npm i @flipperdotfamily/widget # lit, viem and @flipperdotfamily/sdk come along as dependenciesimport { onFlipperEvent } from "@flipperdotfamily/widget"; // also registers <flipper-widget> (a no-op on the server)
const flipper = document.createElement("flipper-widget"); // typed as FlipperWidget
flipper.provider = window.ethereum;
flipper.partner = "acme";
flipper.theme = { mode: "dark", accent: "#ff5a1f", radius: 16 };
document.querySelector("#flip")!.append(flipper);
// typed listeners (the detail comes first); each returns an unsubscribe function
const off = onFlipperEvent(flipper, "flip-settled", (d) => console.log(d.outcome, d.payout));
onFlipperEvent(flipper, "error", (d) => console.warn(d.code, d.message));@flipperdotfamily/react is a "use client" component that renders the tag on the server and upgrades it on the client. Every widget option is a camelCase prop, and the callbacks (onReady, onConnectRequest, onFlipRequested, onFlipSettled, onPayoutResolved, onListing, onError, onResize) receive the event detail. className, style and id pass through.
npm i @flipperdotfamily/react"use client";
import { FlipperWidget } from "@flipperdotfamily/react";
import { useConnect, useWalletClient } from "wagmi";
import { injected } from "wagmi/connectors";
export function Flip() {
const { data: walletClient } = useWalletClient(); // undefined until connected
const { connect } = useConnect();
return (
<FlipperWidget
walletClient={walletClient}
theme="dark"
partner="acme"
onConnectRequest={() => connect({ connector: injected() })}
onFlipSettled={(d) => console.log(d.outcome, d.payout)}
/>
);
}FlipperConfigProvider sets defaults for every widget below it, and ref gives you the <flipper-widget> element:
import { FlipperConfigProvider } from "@flipperdotfamily/react";
// defaults for every <FlipperWidget> below it
<FlipperConfigProvider
config={{ partner: "acme", theme: { mode: "dark", accent: "#ff5a1f" } }}
>
<App />
</FlipperConfigProvider>import { useRef } from "react";
import { FlipperWidget, type FlipperWidgetElement } from "@flipperdotfamily/react";
const ref = useRef<FlipperWidgetElement>(null);
<FlipperWidget ref={ref} variant="button" buttonLabel="Flip it" />;
ref.current?.open(); // button variant: open the modal (and close())
ref.current?.refresh(); // re-read accounts, chain and balances, e.g. right after you connectUsing RainbowKit, ConnectKit or Reown AppKit instead of a bare connector? See wallet wiring.
Full example: examples/react-vite ↗
App Router: put the widget in a client component and render it from any server component. You don't need dynamic(…, { ssr: false }), since the server renders the bare tag and the client upgrades it in place. Reserve its height (the card is about 560 px) to avoid layout shift.
// app/flip/flip-widget.tsx
"use client";
import { FlipperWidget } from "@flipperdotfamily/react";
import { useConnectModal } from "@rainbow-me/rainbowkit";
import { useWalletClient } from "wagmi";
export function FlipWidget() {
const { data: walletClient } = useWalletClient();
const { openConnectModal } = useConnectModal();
return (
<FlipperWidget
walletClient={walletClient}
partner="acme"
theme="auto"
onConnectRequest={() => openConnectModal?.()}
style={{ minHeight: 560 }} // reserve the card's height: no layout shift
/>
);
}// app/flip/page.tsx (a server component)
import { FlipWidget } from "./flip-widget";
export default function Page() {
return <FlipWidget />;
}Using the bare element instead of the wrapper? Import @flipperdotfamily/widget from a "use client" file (it only registers in the browser), and set provider / theme through a ref, since objects can't be attributes.
Full example: examples/nextjs-app-router ↗
Vue 3. Props are the widget options, and events carry the detail as payload: ready, connect-request, flip-requested, flip-settled, payout-resolved, listing, error, resize. A template ref exposes el, open(), close() and refresh(). There's no onConnectRequest prop. Handle @connect-request instead, and the wrapper skips the widget's own eth_requestAccounts fallback.
npm i @flipperdotfamily/vue<script setup lang="ts">
import { FlipperWidget } from "@flipperdotfamily/vue";
const provider = window.ethereum; // or your wallet kit's EIP-1193 provider
const open = () => provider?.request({ method: "eth_requestAccounts" });
const onSettled = (d: { outcome: string; payout: string }) =>
console.log(d.outcome, d.payout);
</script>
<template>
<FlipperWidget
:provider="provider"
theme="dark"
partner="acme"
@connect-request="open"
@flip-settled="onSettled"
/>
</template>Or register it globally with defaults. A useFlipperClient(options) composable gives you the headless client.
// main.ts: register <FlipperWidget> globally, with defaults
import { createApp } from "vue";
import { FlipperPlugin } from "@flipperdotfamily/vue";
import App from "./App.vue";
createApp(App).use(FlipperPlugin, { partner: "acme", theme: "dark" }).mount("#app");Full example: examples/vue-vite ↗
Svelte 5. Options are props, and events are callback props (onReady, onConnectRequest, onFlipRequested, onFlipSettled, onPayoutResolved, onListing, onError, onResize) that receive the detail. bind:element gives you the element.
npm i @flipperdotfamily/svelte<script lang="ts">
import { FlipperWidget } from "@flipperdotfamily/svelte";
const provider = window.ethereum; // or your wallet kit's EIP-1193 provider
let element = $state<HTMLElement>(); // the <flipper-widget> itself
const open = () => provider?.request({ method: "eth_requestAccounts" });
</script>
<FlipperWidget
{provider}
theme="dark"
partner="acme"
onConnectRequest={open}
onFlipSettled={(d) => console.log(d.outcome, d.payout)}
bind:element
/>Full example: examples/svelte-vite ↗
Angular 17+. FlipperWidgetComponent is standalone with the selector flipper-widget, so no CUSTOM_ELEMENTS_SCHEMA. Inputs are the options. Outputs carry the detail and are named flipperReady, connectRequest, flipRequested, flipSettled, payoutResolved, flipperListing, flipperError and flipperResize. They differ from the DOM event names so Angular doesn't run your handler twice. @ViewChild(FlipperWidgetComponent) gives .element, .open(), .close() and .refresh().
npm i @flipperdotfamily/angularimport { Component, ViewChild } from "@angular/core";
import { FlipperWidgetComponent } from "@flipperdotfamily/angular";
import type { FlipperEventMap } from "@flipperdotfamily/widget";
@Component({
selector: "app-flip",
standalone: true,
imports: [FlipperWidgetComponent], // no CUSTOM_ELEMENTS_SCHEMA needed
template: `
<flipper-widget
[provider]="provider"
theme="dark"
partner="acme"
(connectRequest)="open()"
(flipSettled)="onSettled($event)"
></flipper-widget>
`,
})
export class FlipComponent {
provider = (window as any).ethereum; // or your wallet kit's EIP-1193 provider
@ViewChild(FlipperWidgetComponent) flipper?: FlipperWidgetComponent;
open() {
this.provider?.request({ method: "eth_requestAccounts" });
}
onSettled(d: FlipperEventMap["flip-settled"]) {
console.log(d.outcome, d.payout);
}
}Full example: examples/angular ↗
The element works anywhere custom elements do (Solid, Preact, Lit, Qwik, Alpine, jQuery). Set objects such as provider and theme as properties, not attributes, and listen on the element itself, since events don't bubble.
import "@flipperdotfamily/widget";
<flipper-widget
prop:provider={provider()}
attr:theme="dark"
attr:partner="acme"
on:flip-settled={(e) => console.log(e.detail.outcome)}
/>;
// once, in a .d.ts: let Solid's JSX know the tag
declare module "solid-js" {
namespace JSX {
interface IntrinsicElements {
"flipper-widget": HTMLAttributes<HTMLElement> & {
[k: `${"prop" | "attr" | "on"}:${string}`]: unknown;
};
}
}
}import "@flipperdotfamily/widget"; // first, so Preact sees the element's properties
<flipper-widget
provider={provider}
theme="dark"
partner="acme"
onflip-settled={(e) => console.log(e.detail.outcome)}
/>;In plain JavaScript, jQuery or Alpine, use el.provider = … and el.addEventListener("flip-settled", …) as in the script-tag sample.
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:
| Input | What | Notes |
|---|---|---|
provider | Any EIP-1193 provider | window.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. |
walletClient | A viem WalletClient | e.g. wagmi's useWalletClient().data. Replace it when the account changes (wagmi does that for you). |
onConnectRequest | or the connect-request event | A 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.
"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} />;
}"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} />;
}import "@flipperdotfamily/widget";
import { createWalletClient, custom } from "viem";
const flipper = document.querySelector("flipper-widget")!;
// simplest: the EIP-1193 provider. The widget follows accountsChanged / chainChanged itself
flipper.provider = window.ethereum;
// or a viem WalletClient (build a new one when the account changes)
const [account] = await window.ethereum.request({ method: "eth_requestAccounts" });
flipper.walletClient = createWalletClient({ account, transport: custom(window.ethereum) });Pass the underlying EIP-1193 provider (window.ethereum, or whatever you gave BrowserProvider), not the ethers object. The widget speaks EIP-1193, which ethers wraps.
import "@flipperdotfamily/widget";
import { BrowserProvider } from "ethers";
const eip1193 = window.ethereum; // the object you hand to BrowserProvider
const ethersProvider = new BrowserProvider(eip1193); // your app keeps using ethers as before
const flipper = document.querySelector("flipper-widget")!;
flipper.provider = eip1193; // NOT ethersProvider: the widget speaks EIP-1193
flipper.addEventListener("connect-request", (e) => {
e.preventDefault();
void ethersProvider.send("eth_requestAccounts", []);
});AppKit (formerly Web3Modal) exposes the connected wallet's EIP-1193 provider; open its modal on a connect request.
"use client";
import { FlipperWidget } from "@flipperdotfamily/react";
import { useAppKit, useAppKitProvider } from "@reown/appkit/react";
export function Flip() {
const { open } = useAppKit();
// EIP-1193; undefined while disconnected
const { walletProvider } = useAppKitProvider("eip155");
return <FlipperWidget provider={walletProvider} onConnectRequest={() => open()} />;
}RainbowKit is wagmi underneath: pass the wallet client, and open its connect modal.
"use client";
import { FlipperWidget } from "@flipperdotfamily/react";
import { useConnectModal } from "@rainbow-me/rainbowkit";
import { useWalletClient } from "wagmi";
export function Flip() {
const { data: walletClient } = useWalletClient(); // RainbowKit is wagmi underneath
const { openConnectModal } = useConnectModal();
return (
<FlipperWidget walletClient={walletClient} onConnectRequest={() => openConnectModal?.()} />
);
}Anything that exposes an EIP-1193 provider or a viem WalletClient works the same way.
- Privy:
await wallet.getEthereumProvider()on a connected wallet fromuseWallets()→provider; calllogin()orconnectWallet()on a connect request. - Dynamic:
await primaryWallet.getWalletClient()for an Ethereum wallet →walletClient;setShowAuthFlow(true)on a connect request. - Coinbase Wallet SDK:
createCoinbaseWalletSDK({ appName }).getProvider()→provider. - WalletConnect (
@walletconnect/ethereum-provider), MetaMask SDK, ConnectKit (wagmi), Safe apps: pass the provider or the wagmi wallet client.
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.
https://flipper.family/embed?chain=4663&theme=dark&accent=4cc2ff&partner=acme| Param | Values | Default | Meaning |
|---|---|---|---|
chain | 4663, 31337 or robinhood, local | the site's deployment | Chain to flip on. Unknown chains show "not live on this chain". |
token | token address | $FLIPPER | Token selected at start. |
tokens | comma-separated addresses | every token | Allowlist for the picker. One address means no picker. |
mode | picker / single | picker | single: one fixed token (token required), no picker. |
hidePicker | 1 / true | off | Deprecated: use mode=single. |
fit | auto / fill | auto | fill: the widget fills a fixed-size frame (no auto-height). |
size | sm / md / lg / auto | auto | Scale on top of the fluid layout. |
details | 1 / true | off | Win chance / payout / fee line under the button. |
tagline | 1 / true, or text | none | Idle headline under the coin: true = the built-in one, text = yours. |
theme | light, dark, auto | auto | Colour mode; auto follows prefers-color-scheme. |
accent | hex, # optional (4cc2ff, %23ff5a1f) | flipper sky blue | Accent colour; text on it is picked for contrast. |
radius | px, 0–40 | 24 | Card corner radius. |
branding | 0 / false | on | Removes flipper.family marks (footer link, dolphin coin faces). |
partner | [A-Za-z0-9._:-]{1,64} | none | Attribution id: echoed in every event, sent as X-Flipper-Partner. An approved partner code also attributes flips onchain (ERC-8021). |
locale | en, es | en | Built-in strings; unknown locales fall back to English. |
compact | 1 / true | off | Compact layout: small inline coin, denser spacing. |
approval | max, exact | max | Allowance to request when it's short. |
connect | event, request | event | request also sends eth_requestAccounts on Connect, for hosts that forward every RPC to a provider. |
hostOrigin | an origin | none | Iframes: pins the parent's origin. Other origins are dropped, and the embed posts only to this one. |
config | base64url JSON | none | Full 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.
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.
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:
<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.
| type | Direction | Shape |
|---|---|---|
rpc | embed → host | { id, method, params }: a wallet method only (reads use the embed's own RPC). Answer each id exactly once. |
event | embed → host | { name, data }: ready, connect-request, flip-requested, flip-settled, payout-resolved, listing, error, resize. |
rpc-result | host → embed | { id, result } |
rpc-error | host → embed | { id, error: { code, message, data? } } with EIP-1193 codes: 4001 rejected, 4100 unauthorized, 4200 unsupported, 4902 unknown chain. |
wallet | host → embed | { accounts, chainId } after ready and on every change (accounts: [] when disconnected; chainId as hex). |
config | host → embed | Any 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:
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.
| Platform | Package | Install |
|---|---|---|
| React Native / Expo | @flipperdotfamily/react-native ↗ | npx expo install @flipperdotfamily/react-native react-native-webview |
| Flutter | flipper_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.
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)}
/>
);
}FlipperWidget(
wallet: myWallet, // a FlipperWallet; null = read-only
config: const FlipperConfig(partner: 'acme'),
theme: const FlipperTheme(mode: FlipperThemeMode.dark, accent: '#ff5a1f'),
onConnectRequest: (_) => openMyConnectSheet(),
onFlipSettled: (e) => debugPrint('flip ${e.flipId}: ${e.outcome}'),
)FlipperWidgetView(
config: FlipperConfig(partner: "acme"),
theme: FlipperTheme(mode: .dark, accent: "#ff5a1f"),
wallet: wallet
)
.onFlipperConnectRequest { _ in showConnectSheet = true }
.onFlipperFlipSettled { print($0.outcome ?? "") }FlipperWidget(
wallet = wallet, // null = read-only
config = FlipperConfig(partner = "acme"),
theme = FlipperTheme(mode = FlipperThemeMode.DARK, accent = "#ff5a1f"),
onConnectRequest = { openConnect() },
onFlipSettled = { e -> Log.i("flip", "${e.flipId} ${e.outcome}") },
)Prefer a fully native UI? useFlipperHeadless() from @flipperdotfamily/react-native/headless gives you balances, previews and flip() on top of @flipperdotfamily/sdk.
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.
<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.
<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.
<!-- 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:
- CSS custom properties from your stylesheet:
flipper-widget { --flipper-accent: #ff5a1f; }. - The theme object (the
themeproperty, typedFlipperTheme), plus theaccentandradiusshorthands. ::part()for anything else.
Playground
The real widget, loaded from /embed/flipper-widget.js. Change the settings and copy the code below.
Wallet: none detected, so the widget runs read-only: prices, odds and the token list, no flips.
Events
- waiting for the first event…
Code for these settings
<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>import { FlipperWidget } from "@flipperdotfamily/react";
<FlipperWidget
provider={provider}
theme="dark"
partner="acme"
onConnectRequest={openConnectModal}
/>import { mountFlipperIframe } from "@flipperdotfamily/widget/host";
mountFlipperIframe({
container: "#flipper",
provider: window.ethereum,
params: { theme: "dark", partner: "acme" },
});The theme object
Every field is optional; unset colours keep flipper's palette for the current mode.
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.
| Property | Theme key | Dark | Light |
|---|---|---|---|
| --flipper-accent | accent | #4cc2ff | #0a6aa2 |
| --flipper-accent-text | accentText | #031a2b | #ffffff |
| --flipper-bg | background | #0a1b26 | #ffffff |
| --flipper-surface | surface | #0e2230 | #f5f9fc |
| --flipper-field | field | #13293a | #eef4f8 |
| --flipper-border | border | #cdeeff17 | #0e3a5a1c |
| --flipper-text | text | #eaf6fb | #0b2239 |
| --flipper-text-muted | textMuted | #eaf6fb99 | #0b2239a6 |
| --flipper-text-subtle | textSubtle | #eaf6fb66 | #0b223980 |
| --flipper-win | win | #f5c451 | #946200 |
| --flipper-loss | loss | #ff6b5e | #c7372c |
| --flipper-check | CSS only (follows the accent) | #4cc2ff | #0a6aa2 |
| --flipper-check-launchpad | CSS only | #d4fc50 | #6b8a00 |
| Property | Theme key | Default |
|---|---|---|
| --flipper-radius | radius | 24px (inner controls scale from it) |
| --flipper-border-width | borderWidth | 1px; 0 drops the card border |
| --flipper-shadow | shadow | a soft float; "none" drops it |
| --flipper-backdrop | CSS only | a 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-width | maxWidth | 460px; "none" fills the host |
| --flipper-font | fontFamily | Manrope when your page loads it, else the system stack; "inherit" uses your page's font |
| --flipper-font-display | displayFontFamily | Satoshi when loaded, else the UI font |
| --flipper-font-mono | monoFontFamily | JetBrains Mono when loaded, else ui-monospace |
| --flipper-coin-size | coinSize | scales with the widget's width, 84–124px (52px when compact) |
/* 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.
<!-- 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).
<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.
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
| Property / attribute | Type | Default | Notes |
|---|---|---|---|
providerproperty only | EIP-1193 provider | none | Signs transactions; reads never use it. The widget follows its accountsChanged, chainChanged and disconnect. |
walletClientproperty only | viem WalletClient | none | Instead of provider, e.g. wagmi's useWalletClient().data. Replace it when the account changes. |
onConnectRequestproperty only | (detail) => void | none | A disconnected user pressed Connect, Flip or List: open your wallet UI. |
chainIdchain-id | number (attr also robinhood, local) | 4663 | Robinhood Chain. |
rpcUrlrpc-url | string | the chain's public RPC | Reads only. |
apiUrlapi-url | string | null | the deployment's | flipper API (token list, logos). null / "none": onchain token list only. |
deploymentUrldeployment-url | string | null | flipper.family manifest | Live addresses. Not fetched when addresses has house and lens. |
addressesaddresses (JSON) | object | from the manifest | { house, lens, flipper, v4Adapter, v3Adapter, weth, … } |
tokentoken | address | "ETH" | $FLIPPER | Selected at start. |
tokenstokens (comma list) | string[] | every token | Allowlist 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-picker | boolean | false | **Deprecated**: use mode="single". Without token it falls back to $FLIPPER. |
etheth | boolean | true | Offer native ETH (flipped as WETH). |
listinglisting | boolean | true | Listing qualifying tokens (whitelisted by flipper) from the picker. |
minAmountmin-amount | decimal string | none | Minimum stake in token units ("10"). |
maxAmountmax-amount | decimal string | none | Maximum 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×). |
detailsdetails | boolean | false | Win 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 | boolean | none | A 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. |
accentaccent | colour | flipper sky blue | Shorthand for theme.accent. |
radiusradius | px, 0–40 | 24 | Shorthand for theme.radius. |
brandingbranding | boolean | true | false removes flipper marks: header dolphin, coin faces, footer. |
brandNamebrand-name | string | "flipper" | Header name. |
brandLogobrand-logo | image URL | dolphin | Header logo; also the heads face when branding is false and there's no coinImage. |
coinImagecoin-image | image URL | dolphin | Heads face (square; transparent PNG or SVG works best). |
coinImageTailscoin-image-tails | image URL | fluke | Tails face. |
buttonLabelbutton-label | string | "Flip" + the token | Trigger label for variant="button", e.g. "Flip FLIPPER". |
localelocale | "en" | "es" | "en" | Built-in string table. Regional tags fall back (es-MX → es). |
stringsproperty only | Partial<FlipperStrings> | none | Override any string (keys below). |
reducedMotionreduced-motion | boolean | OS setting | Force reduced motion on or off. |
partnerpartner | [A-Za-z0-9._:-]{1,64} | none | Attribution: echoed in every event and sent as X-Flipper-Partner on API calls. A partner code approved in the PartnerRegistry (1–32 of a-z 0-9 _ -) also attributes every flip onchain (ERC-8021), earns a share of it and can give your players better odds. |
Methods
| Member | Does |
|---|---|
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. |
client | The 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:
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).
| Field | Type | Meaning |
|---|---|---|
version | string | Widget semver. |
chainId | number | null | The chain it runs on. |
account | address | null | Connected account; null without a wallet. |
token | string | null | Selected token address, or "ETH". |
variant | card | compact | button | The rendered variant. |
partner | string | null | Your partner id. |
connect-request
A disconnected user pressed Connect, Flip or List. Cancelable: call preventDefault() when you open your own wallet UI.
| Field | Type | Meaning |
|---|---|---|
reason | connect | flip | list | What they pressed. |
partner | string | null | Your partner id. |
flip-requested
The flip transaction is mined and randomness requested. The coin is spinning.
| Field | Type | Meaning |
|---|---|---|
flipId | string | Onchain flip id (decimal). |
account | address | The player. |
token | address | The staked token (WETH for native ETH flips). |
symbol | string | Its symbol. |
decimals | number | Its decimals. |
amount | wei string | The stake. |
winChanceBps | number | Win chance in basis points (4500 = 45%). |
randomnessFee | wei string | Randomness fee paid in the native token. |
txHash | hash | The flip transaction. |
approveTxHash | hash | null | The approval, when one was needed. |
native | boolean | The stake was native ETH, wrapped to WETH. |
partner | string | null | Your 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.
| Field | Type | Meaning |
|---|---|---|
flipId | string | Onchain flip id (decimal). |
account | address | The player. |
token | address | The staked token (WETH for native ETH flips). |
symbol | string | Its symbol. |
decimals | number | Its decimals. |
amount | wei string | The stake. |
outcome | won | lost | refunded | What to tell the player. |
status | see note | Won, WonFallback (winnings paid in $FLIPPER), WinPending, Lost, LostInventory or Refunded. |
won | boolean | The flip won. |
pending | boolean | WinPending: stake returned, winnings still owed. The widget shows a Retry payout button, though flipper's payout worker usually pays within about a second. |
payout | wei string | Received in payoutToken, stake included ("0" on a loss). |
payoutToken | address | What payout is denominated in. |
flipperPaid | wei string | $FLIPPER paid on top (fallback wins). |
txHash | hash | null | The settlement transaction, if it could be looked up. |
requestTxHash | hash | The flip transaction. |
native | boolean | The stake was native ETH. |
partner | string | null | Your partner id. |
payout-resolved
A pending win's winnings were paid. Once per flip, alongside the final flip-settled.
| Field | Type | Meaning |
|---|---|---|
flipId | string | Onchain flip id (decimal). |
account | address | The player. |
token | address | The staked token (WETH for native ETH flips). |
symbol | string | Its symbol. |
decimals | number | Its decimals. |
tokenPaid | wei string | Winnings paid in token (the stake already came back at settlement). |
flipperPaid | wei string | $FLIPPER paid instead: "0" unless the token still couldn't be bought after the pending timeout. |
by | self | other | self: this widget's Retry payout paid it. other: someone else did (usually flipper's payout worker). |
native | boolean | The 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. |
txHash | hash | null | The PendingWinResolved transaction, if it could be looked up. |
partner | string | null | Your partner id. |
listing
Each stage of a listing started from the picker.
| Field | Type | Meaning |
|---|---|---|
stage | started | submitted | listed | failed | Progress. |
token | address | The token being listed. |
symbol | string | Its symbol. |
venue | v4 | v3 | null | Which route adapter lists it. |
txHash | hash | null | Once submitted. |
error | string | null | Why it failed. |
partner | string | null | Your partner id. |
error
Something failed. message is plain English and safe to show.
| Field | Type | Meaning |
|---|---|---|
code | string | user-rejected, rejected, insufficient-funds, revert, timeout, config, network, wallet or unknown. |
message | string | Plain English. |
context | string | config, wallet, preview, flip (also Retry payout) or listing. |
partner | string | null | Your partner id. |
resize
The widget's size changed (iframe and WebView hosts size themselves from it).
| Field | Type | Meaning |
|---|---|---|
width | number | CSS pixels of the border box. |
height | number | CSS pixels of the border box. |
Event names per framework
| DOM event | React, Svelte | Vue | Angular |
|---|---|---|---|
ready | onReady | @ready | (flipperReady) |
connect-request | onConnectRequest | @connect-request | (connectRequest) |
flip-requested | onFlipRequested | @flip-requested | (flipRequested) |
flip-settled | onFlipSettled | @flip-settled | (flipSettled) |
payout-resolved | onPayoutResolved | @payout-resolved | (payoutResolved) |
listing | onListing | @listing | (flipperListing) |
error | onError | @error | (flipperError) |
resize | onResize | @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.
npm i @flipperdotfamily/sdk viemimport {
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
resolveDeployment
Returns the chain's addresses, RPC and API. Explicitaddresses(withhouseandlens) win and nothing is fetched; otherwise it reads the live manifest (deploymentUrl: nullforbids that). - 2
flip()
Takes a fresh preview and throws aFlipperErrorif the house would reject. Then it approves if the allowance is short, simulates and sendsflipwith the preview's win chance as the minimum and a 5-minute deadline, and returns theflipId. The randomness fee goes with 20% headroom, which the house refunds. - 3
waitForSettlement
Resolves when the flip leaves Pending.waitForResolutionfollows aWinPendingflip until upkeep pays it, anddescribeSettlementgives 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.
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 ETHListing 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:
| Venue | Adapter | Call |
|---|---|---|
v4 | V4RouteAdapter | registerAndList(token, poolKey) |
v3 | V3RouteAdapter (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.
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.
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):
"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
| Chain | Chain id | Status | chain-id | Public RPC |
|---|---|---|---|---|
| Robinhood Chain | 4663 | Launch chain, the default | robinhood | https://rpc.mainnet.chain.robinhood.com |
| Local fork | 31337 | Development | local | http://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.
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
addressesor from the manifest over HTTPS. Pinaddresses(andrpcUrl) 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(mountFlipperIframedoes) so the embed only exchanges messages with your origin. On your side, checkevent.source === iframe.contentWindowandevent.origin === "https://flipper.family", and refuse wallet methods outside the bridge's list with4200. - Content Security Policy. The web component needs
connect-srcfor the chain's RPC and the flipper API (andflipper.familyfor the manifest, unless you pin addresses), andimg-srcfor token logos. The CDN script needsscript-src cdn.jsdelivr.net, or self-host the file. The iframe embed needs onlyframe-src flipper.family(andscript-srcforflipper-host.jsif you load it from a URL). - Native apps. Keep the WebView on
flipper.family/embedand 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:
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.