@hypermid/checkout
v0.3.0
Published
Embeddable crypto checkout for Hypermid — pay with any token, the merchant receives the token they asked for. Drop in with a single script tag or npm import.
Maintainers
Readme
@hypermid/checkout
Embeddable crypto checkout for Hypermid — pay with any token, the merchant receives the token they asked for.
The package ships three entry points so you can choose the integration depth that fits your stack:
| Entry | Use when | Integration cost |
|---|---|---|
| @hypermid/checkout | Zero-JS — drop a script tag or mount an iframe | One line |
| @hypermid/checkout/headless | You already have a UI and only need the payment engine | ~20 lines |
| @hypermid/checkout/react | Flagship. In-page React component with our UI, using your existing wallet | <HypermidWidget /> |
The destination — amount, token, chain, recipient — is bound to the checkout session server-side when you create it. The only thing the payer chooses is which token to pay with; the destination can never be overridden from the client.
How it works
Server-side, create a checkout with your secret key:
curl -X POST https://api.hypermid.io/v1/checkout \ -H "Authorization: Bearer sk_live_…" \ -H "Content-Type: application/json" \ -d '{ "amount": "100000000", "token": "0x833…913", "chain": 8453, "recipient": "0xYourTreasury", "orderId": "order-123" }' # → { "data": { "id": "co_…", … } }Sessions must be created server-side with your secret key (
sk_live_…). Never create sessions in client-side JavaScript — the secret key authenticates your merchant account and must not be exposed to browsers.Client-side, embed the returned
checkoutIdusing any of the three entries below.
React component (in-page) — @hypermid/checkout/react
The recommended integration for React apps. The widget renders directly inside your page (no iframe), consumes your already-connected wallet, and presents the full Hypermid checkout UI: funding-source picker, quote, review, status, and receipt.
Install
npm install @hypermid/checkoutPeer dependencies (required):
npm install react react-domQuickstart — Privy (signer-injected)
The most common case: your app already uses Privy for authentication and wallet management. Pass the connected signer directly to <HypermidWidget> — no wallet-connect step inside the widget.
import { HypermidWidget } from "@hypermid/checkout/react";
import {
hypermidSignerFromPrivy,
hypermidSignerFromPrivySmart,
} from "@hypermid/checkout/adapters";
import { usePrivy, useWallets, useSmartWallets } from "@privy-io/react-auth";
function CheckoutButton({ checkoutId }: { checkoutId: string }) {
const { ready, authenticated } = usePrivy();
const { wallets } = useWallets();
const { client: smartClient } = useSmartWallets();
if (!ready || !authenticated) return <div>Log in to continue</div>;
// Prefer Smart Wallet (gasless) if available; fall back to embedded EOA.
const wallet = wallets[0];
const signer = smartClient
? hypermidSignerFromPrivySmart({ client: smartClient })
: wallet
? hypermidSignerFromPrivy(wallet)
: undefined;
if (!signer) return <div>No wallet connected</div>;
return (
<HypermidWidget
sessionId={checkoutId}
signer={signer}
theme="dark"
onSuccess={({ checkoutId, paidAmount, txHash }) => {
console.log("Paid:", paidAmount, "tx:", txHash);
fulfillOrder(checkoutId);
}}
onError={({ reason }) => {
console.error("Checkout failed:", reason);
}}
/>
);
}Gas sponsorship — When using Privy Smart Wallet, gas is sponsored automatically if your Privy dashboard has Smart Wallets enabled and a funded paymaster configured. There is no per-call sponsor flag; sponsorship is ambient to the client.
<HypermidWidget> props
| Prop | Type | Required | Description |
|---|---|---|---|
| sessionId | string | Yes | Checkout session id (co_…) from POST /v1/checkout. |
| signer | HypermidWallet | No | Your already-connected wallet signer. When provided, the widget will render the picker/quote UI directly with no wallet-connect step (once the UI extraction is complete). When absent, the widget will lazy-load a standalone wallet connection gate (Phase 2). |
| amount | string | No | For open-amount sessions (deposit/withdrawal). Whole units of the destination token. Mutually exclusive with fixed-amount sessions. |
| payToken | string | No | Pre-selected pay token (ERC-20 address or native sentinel). |
| payChain | number | No | Pre-selected pay chain id. |
| apiBase | string | No | Override the API base URL. Default: https://api.hypermid.io. |
| theme | "dark" \| "light" \| Partial<Theme> | No | Theming. Default "dark". See Theming below. |
| maxSlippage | number | No | Maximum acceptable slippage between the displayed quote and the binding quote, as a decimal (e.g. 0.01 = 1%). See Quote re-confirm guard below. |
| onSuccess | (payload) => void | No | Backend on-chain-verified completion. The only trustworthy "paid" signal. Payload: { checkoutId, status: "completed", paidAmount?, txHash? }. |
| onError | (payload) => void | No | Payment failed. Payload: { checkoutId, reason? }. |
| onClose | () => void | No | Payer dismissed the checkout. |
| onStatus | (status) => void | No | Coarse lifecycle status for spinners: "idle" \| "quoting" \| "review" \| "paying" \| "completed" \| "failed". |
| onEvent | (event, properties) => void | No | Funnel events for your own analytics (PostHog, Segment, GA, etc.). Stripped of PII. |
Quote re-confirm guard
The widget displays a quote to the payer before they sign. When pay() fetches the binding quote, the required input may have moved due to market volatility. If the binding quote's requiredInput exceeds the displayed figure by more than maxSlippage, the widget surfaces the new figure for re-confirmation rather than signing silently.
// Default — 1%
<HypermidWidget sessionId={id} signer={signer} />
// Tighten for high-value transactions
<HypermidWidget sessionId={id} signer={signer} maxSlippage={0.005} />- Default:
0.01(1%) - Hard ceiling:
0.03(3%) — merchants may tighten (e.g. 0.5%) but cannot loosen past 3%. The payer must never pay materially more than the figure they agreed to. - Re-confirm return: When the guard fires,
onSuccess/onErrorare not called. The widget surfaces the newrequiredInputinside its own UI. If you need to react programmatically, inspectonStatusfor"reconfirm".
Theming
Pass a preset string or override individual tokens:
// Preset
<HypermidWidget theme="light" … />
// Granular override
<HypermidWidget
theme={{
accent: "#ff4400",
textPrimary: "#111827",
radiusButton: 8,
}}
/>Full Theme shape:
interface Theme {
bgPage: string;
bgCard: string;
border: string;
textPrimary: string;
textMuted: string;
textFaint: string;
accent: string;
accentText: string;
font: string;
radiusCard: number;
radiusButton: number;
}Phase 1 scope — what's in now, what's later
In now (CR-287 Phase 1):
- Signer-injected mode: pass an existing wallet, the widget renders the method-selection UI on top of it.
- Method selection → headless payment → status/receipt.
- All EVM chains and tokens supported by the Hypermid quote engine.
Coming later (not in Phase 1):
- Standalone wallet connect — when no
signeris passed, the widget connects its own wallet. Currently lazy-loads a gate component; full connect flow is Phase 2. - On-ramp tab — cash/card funding via on-ramp provider (CR-282). Will appear as a tab inside the widget surface.
- Non-EVM chains — Solana, etc.
Other providers
The signer prop accepts any HypermidEvmSigner. Adapters are provided for:
import {
hypermidSignerFromPrivy, // Privy embedded EOA — live-verified
hypermidSignerFromPrivySmart, // Privy Smart Wallet (4337, gasless) — live-verified
hypermidSignerFromDynamic, // Dynamic.xyz — implemented, not yet live-verified
hypermidSignerFromCoinbaseEoa, // Coinbase Wallet (extension/mobile) — implemented, not yet live-verified
hypermidSignerFromCoinbaseSmart,// Coinbase Smart Wallet (EIP-5792) — implemented, dormant; 5792 connector not registered
hypermidSignerFromTurnkey, // Turnkey — implemented, not yet live-verified
} from "@hypermid/checkout/adapters";Each adapter is structurally typed — no provider SDK is imported by the package, so your preferred provider version is never pinned by us.
Production readiness: Only the Privy adapters (EOA and Smart Wallet) have been exercised end-to-end against real payments on mainnet — deposits, batching, and gas sponsorship verified. The others exist in source and pass unit tests, but have not yet been run against a live settlement. If you need Coinbase, Dynamic, or Turnkey before we validate them, open an issue and we'll prioritise the live test.
Headless engine — @hypermid/checkout/headless
Use this when you have your own UI and only need the payment logic: quotes, session binding, drift guard, batching, and settlement.
import { HypermidCheckout } from "@hypermid/checkout/headless";
const result = await HypermidCheckout.pay({
checkoutId: "co_…",
provider: signer, // HypermidWallet — same signer adapters as above
amount: "25", // for open-amount sessions
onStatus: (s) => setStatus(s),
});
// result.status === "completed" | "failed"
// result.txHash, result.paidAmountSee src/headless.ts for the full HypermidPayConfig shape and event semantics.
Iframe embed — @hypermid/checkout
Zero-JS integration: mount a sandboxed iframe that loads the live checkout app from Hypermid's CDN. Best for static sites or when you want zero maintenance.
import { HypermidCheckout } from "@hypermid/checkout";
const checkout = HypermidCheckout.init({
containerId: "pay",
checkoutId: "co_…",
theme: "dark",
onSuccess: ({ checkoutId }) => {
// fulfill the order
},
onError: ({ reason }) => {
// show a retry
},
});Script tag (no build step)
<div id="hypermid-checkout"></div>
<script src="https://hypermid.io/checkout/v1/embed.js"></script>
<script>
HypermidCheckout.init({
containerId: "hypermid-checkout",
checkoutId: "co_…",
theme: "dark",
onSuccess: function (p) { /* fulfill order */ },
});
</script>Or auto-init from a data attribute:
<div data-hypermid-checkout data-checkout-id="co_…" data-theme="light" id="hypermid-checkout"></div>
<script src="https://hypermid.io/checkout/v1/embed.js"></script>Iframe config
| Option | Type | Notes |
|---|---|---|
| containerId | string | Required. Element id to mount into. |
| checkoutId | string | Required. Session id (co_…) from POST /v1/checkout. |
| theme | "dark" \| "light" | Default "dark". |
| label | string | Action label, e.g. "Pay" / "Deposit". |
| accent, bgPage, bgCard, border, textPrimary, textMuted | string (hex) | Full theme-token overrides. |
| fontFamily | string | Font family for the embed. |
| width, height, borderRadius | string (CSS) | height is a floor — the frame grows to fit taller content. |
Iframe events
The embed relays the checkout lifecycle to your callbacks over postMessage. Only messages from a Hypermid origin and this embed's own iframe are accepted.
| Callback | Fires when | Payload |
|---|---|---|
| onReady() | the iframe has mounted | — |
| onResize(height) | content height changed (grow-only floor) | number |
| onSuccess(p) | the payment is backend on-chain-verified complete | { checkoutId, status, paidAmount?, txHash? } |
| onError(p) | the payment failed | { checkoutId, reason? } |
| onClose() | the payer dismissed the checkout | — |
onSuccessis the only trustworthy "paid" signal — it fires on the backend's on-chain settlement verification (delivered amount ≥ the requested amount), never merely because a transaction mined. Fulfill orders ononSuccess.
Iframe methods
checkout.update({ checkoutId: "co_next…" }); // swap in a new session + reload
checkout.destroy(); // remove the embed + its listenerSecurity
- The destination is server-bound to the session — query params can only set cosmetics / the pay-token, never money.
onSuccessis gated on backend on-chain verification, not on a mined tx.- The React component runs in your app's JS context and signs only what the headless engine hands it — the same server-authoritative calldata returned by
GET /v1/quote.
License
MIT
