Embed Widget¶
The embed widget puts the Atom Circuit swap UI on your own site behind an iframe. Every swap routed through the widget carries a referralId so the affiliate fee from the swap is converted to ATOM and delegated to the validator that the referralId resolves to. The SDK is open source at github.com/cosmosrescue/atom-circuit-embed-sdk and published on npm as @atom-circuit/embed-sdk.
Two audiences:
- Validators embed the widget on their own page and pass their own
referralId. The affiliate fee from every swap stakes back to them. - Non-validator sites (community sites, ecosystem aggregators, content creators) can omit
referralIdentirely - the SDK defaults to'general'and splits the fee across all participating validators. See Using the general referral.
Quick Start¶
Pick the stack you ship with, copy the snippet, replace YOUR_REFERRAL_ID with the value from your validator profile (or the literal string general, see below). Every other field on the widget is optional.
Validators find their referral ID on their validator page on atomcircuit.net, next to the referral link with a Copy button. referralId also accepts your registered validator slug or the literal string general (for non-validator sites); all three resolve correctly on the dapp side.
Vanilla HTML¶
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Atom Circuit embed</title>
</head>
<body>
<div id="atom-circuit-widget"></div>
<script src="https://unpkg.com/@atom-circuit/embed-sdk@1.2.1/dist/atom-circuit.iife.js"></script>
<script>
AtomCircuit.mount(document.getElementById('atom-circuit-widget'), {
referralId: 'YOUR_REFERRAL_ID',
});
</script>
</body>
</html>
Full example: examples/vanilla-html/full.html.
React¶
Install the package:
Mount the component:
import { AtomCircuitSwap } from '@atom-circuit/embed-sdk/react';
export default function SwapPanel() {
return <AtomCircuitSwap referralId="YOUR_REFERRAL_ID" />;
}
Full example: examples/react/full.tsx.
Next.js¶
Install the package:
Dynamic-import the component with ssr: false so the iframe-only code stays out of the server bundle:
'use client';
import dynamic from 'next/dynamic';
const AtomCircuitSwap = dynamic(
() => import('@atom-circuit/embed-sdk/react').then((m) => m.AtomCircuitSwap),
{ ssr: false }
);
export default function Page() {
return <AtomCircuitSwap referralId="YOUR_REFERRAL_ID" />;
}
Full example: examples/nextjs/full.tsx.
Using the general referral¶
If you do not represent a validator, omit referralId entirely (the SDK defaults to 'general') or pass it explicitly as referralId: 'general'. The affiliate fee from every swap is then split across all participating validators - participating meaning registered Atom Circuit validators that have received at least one prior swap attribution. The bot fans the fee out equally at sweep time; rounding remainder goes to the last validator in the set.
The widget shows "Fees split across participating validators" in the validator-attribution row instead of a single validator name. Everything else (theming, chrome, callbacks) works the same way.
Where do my fees go¶
Every swap through the widget carries the referralId you pass at mount time. The 0.5% affiliate fee on that swap is collected, routed through Skip Go to the protocol's collector wallets, converted to ATOM on Cosmos Hub, and delegated to the validator (or split across participating validators, for general) at the next sweep cycle. The full pipeline is documented in Fee Flow.
To find your referral ID, open your validator profile on atomcircuit.net. The referral ID is shown at the top of the page next to your referral link.
Sizing¶
The widget renders inside a wrapper element you do not need to style. The following MountOptions control its layout:
width- any CSS width. Default'100%'.maxWidth- any CSS max-width. Default unset.padding- applied to the wrapper element, not the iframe (iframes ignore their own padding). Default'0'.minHeight- starting iframe height before the widget reports its real content height. Default'480px'. The runtime height is managed by the SDK's resize handler and cannot be overridden.
<AtomCircuitSwap
referralId="YOUR_REFERRAL_ID"
width="100%"
maxWidth="480px"
padding="16px"
minHeight="520px"
/>
Theming¶
The optional theme object controls the widget's color palette and typography. Every field is optional. Supported keys: mode ('light', 'dark', 'auto'), accentColor, background, foreground, border (all hex), radius (px, 0-64), fontSize (px, 8-32), fontFamily (CSS-safe subset, max 200 chars).
Validation is all-or-nothing: if any single field fails its rule, the entire theme is dropped and the widget renders with its defaults. The SDK emits one console.warn describing the failure.
<AtomCircuitSwap
referralId="YOUR_REFERRAL_ID"
width="100%"
maxWidth="480px"
theme={{
mode: 'dark',
accentColor: '#7b61ff',
background: '#0d0f14',
foreground: '#f5f6fa',
border: '#1f2330',
radius: 12,
fontSize: 14,
fontFamily: 'Inter, system-ui, sans-serif',
}}
/>
The widget does not load fonts itself. Use a fontFamily already available on the host page.
Chrome Toggles¶
The widget ships with the Atom Circuit logo, the Connect Wallet button, the "Fees stake with <moniker>" badge, and the footer visible by default. Each of these can be hidden independently through the chrome object.
<AtomCircuitSwap
referralId="YOUR_REFERRAL_ID"
chrome={{
logo: false,
wallet: true,
validator: true,
footer: false,
}}
/>
A non-boolean value on any field drops the entire chrome bundle and the widget renders with all surfaces visible.
Callbacks¶
The widget emits five events on the iframe side and the SDK has one error callback for bring-up failures. All are optional.
Widget events (5):
onReady- fires once when the iframe has loaded and the SDK handshake completed; from here the widget is interactive. Payload:{ protocolVersion }.onResize- fires when the iframe content height changes; use it to reflow your surrounding page layout. Payload:{ height }in px.onSwapSubmitted- fires after the user signs and the source-chain transaction broadcasts. Payload:{ txHash, route }.onSwapSuccess- fires once the cross-chain delivery is confirmed by the indexer. Payload:{ txHash }(source-chain hash).onSwapError- fires when the swap fails inside the iframe or the wallet rejects the signature. Payload:{ code, message }with a stablecodeand a human-readablemessage.
SDK error callback:
onError- fires on widget-level bring-up problems: handshake failure, iframe load error, origin mismatch, protocol incompatibility. Separate fromonSwapError, which covers in-flow swap failures. Payload:{ code, message, cause }. Codes are stable strings:handshake_failed,iframe_load_failed,origin_mismatch,protocol_incompatible,unknown. If you do not supplyonError, the SDK logs a singleconsole.warnand continues. Nothing is thrown.
Persisting across route changes¶
React Router and most SPA routers unmount route-level components when the visitor navigates away. The default behavior is: the widget mounts on the swap page, runs through the loading spinner and handshake, then unmounts when the visitor goes to another page. Coming back remounts from scratch. The wallet session is preserved via iframe-side browser storage, but in-progress swap state (selected tokens, typed amount, fetched route) is lost.
Three patterns to handle this:
Pattern 1 - React layout hoist (recommended for React SPAs)¶
Mount <AtomCircuitSwap /> once in a top-level layout that does not unmount across route changes. Toggle CSS visibility per route:
'use client';
import { AtomCircuitSwap } from '@atom-circuit/embed-sdk/react';
import { usePathname } from 'next/navigation';
export function PersistentSwap() {
const pathname = usePathname();
return (
<div style={{ display: pathname === '/swap' ? 'block' : 'none' }}>
<AtomCircuitSwap referralId="YOUR_REFERRAL_ID" />
</div>
);
}
The widget stays mounted across navigations; only display toggles. Wallet and form state both preserved. Trade-off: the iframe stays in memory on every page.
Pattern 2 - imperative mount once¶
Use AtomCircuit.mount() directly into a persistent DOM container outside the router-managed area. Show or hide via CSS:
<div id="atom-circuit-widget" style="display: none;"></div>
<script src="https://unpkg.com/@atom-circuit/embed-sdk@1.2.1/dist/atom-circuit.iife.js"></script>
<script>
AtomCircuit.mount(document.getElementById('atom-circuit-widget'), {
referralId: 'YOUR_REFERRAL_ID',
});
function showSwap() {
document.getElementById('atom-circuit-widget').style.display = 'block';
}
function hideSwap() {
document.getElementById('atom-circuit-widget').style.display = 'none';
}
</script>
The vanilla mount() lifecycle is not tied to React. Same trade-off as Pattern 1.
Pattern 3 - accept the reload¶
Zero extra code. Re-handshake on every visit takes 1-3 seconds with the loading spinner. Appropriate when the swap page is the destination rather than a sidebar - which is how Stripe Elements, Mapbox demos, and most embedded widget previews work.
Loading State¶
While the iframe is fetching the dapp bundle and completing the handshake (typically 1-3 seconds on a warm cache), the widget renders a centered spinner inside its wrapper. The spinner fades out on the first ready event. If the handshake fails permanently the spinner is also dismissed on onError, so the host page never shows a forever-spinning state.
You do not need to wire anything for this. The behavior is automatic.
Security Model¶
The widget runs inside a sandboxed iframe served from atomcircuit.net. The cross-origin browser boundary prevents the widget from reading or writing the host page's DOM, cookies, or storage. All communication between host and iframe goes over postMessage and is origin-validated on both sides.
Sandbox attributes¶
The iframe is rendered with:
allow-same-origin is required so the Keplr extension can inject window.keplr. allow-popups and allow-popups-to-escape-sandbox let wallet popups (Keplr, Leap, Cosmostation) and tx success links open. allow-top-navigation is intentionally omitted to limit clickjacking surface.
DOM contract¶
The iframe is always wrapped in a <div data-atom-circuit-embed> element that carries position: relative. This anchors the loading overlay so it can absolutely-position over the iframe without affecting host page layout. Select the iframe with #atom-circuit-widget iframe or [data-atom-circuit-embed] iframe.
Subresource Integrity¶
For CDN consumers, pin the script with Subresource Integrity. Current hash:
<script
src="https://unpkg.com/@atom-circuit/embed-sdk@1.2.1/dist/atom-circuit.iife.js"
integrity="sha384-e0EM289L42Rs5yaVi2w+xv5Pwr6rAK9tLh5caDpIW5ADmulSQ97R3CXxC7T/R7D/"
crossorigin="anonymous"
></script>
Each release publishes a new hash on the SDK's GitHub release page. See the SDK's Security section for how to compute it yourself.
For the disclosure channel and supported versions, see SECURITY.md in the SDK repo.
Versioning and Compatibility¶
The npm package follows semver. Breaking changes to the public API of mount() and AtomCircuitSwap ship as a major version bump. Minor versions add backward-compatible options. Patch versions are fixes only. Release notes for each version live in the SDK's CHANGELOG.
The iframe protocol carries its own PROTOCOL_VERSION independent of the npm package version. The SDK and the iframe negotiate compatibility at handshake time; if the host SDK is incompatible with the deployed iframe, the widget emits onError with code protocol_incompatible instead of mounting in a broken state.
Tested in Chromium 115+, Firefox 115+, and Safari 16+. The Keplr in-app browser on mobile is supported directly. Standalone mobile browsers connect through WalletConnect; cold-start signing on iOS Safari can time out after 90 seconds (see the Integration FAQ for the surfaced behavior).
Where to Go Next¶
- npm package: npmjs.com/package/@atom-circuit/embed-sdk
- Source, issues, and security advisories: github.com/cosmosrescue/atom-circuit-embed-sdk
- For deeper integration questions, check the FAQ or open an issue on the SDK repo.