Widget Integration Guide
How to embed EpochIntentWidget in your React app. You render one component and it owns the full flow: wallet-connect → token pick → quote → sign → settle.
For runtime behavior, states, and caveats, see BEHAVIOR.md.
For the full prop table, see the README.
Contents
Prerequisites
Your app must be a wagmi v2 app (or become one). The widget ships these as peer dependencies — you install them:
react, react-dom
^18
It's a React component
wagmi
^2
Account, wallet client, connectors
viem
^2
Signing, RPC reads, unit math
@tanstack/react-query
^5
Required by wagmi v2
lucide-react
^1.14
Icons inside the widget
You also need an Epoch allocator endpoint for api.baseUrl, and — for live Earn data only — a 1delta positions proxy for api.positionsBaseUrl.
Step 1 — Install
A wallet-connect UI library (RainbowKit / Web3Modal) is optional. The widget renders its own connector list from whatever connectors your wagmi config registers; if you already use RainbowKit, those connectors show up automatically.
Step 2 — Providers & CSS
Three things must be in place before you render the widget:
A
WagmiProviderwith at least one connector.A
QueryClientProvider(wagmi v2 requires it).The widget stylesheet imported once at your app root — without it the widget renders unstyled.
Theme tokens & portals
The widget renders its modal into a document.body portal. If you only style the widget via the theme prop, you're covered. If you build chrome around the widget against the same --epoch-* design tokens, project them onto :root so portalled modal children resolve the variables:
Step 3 — Mount the widget
The whole integration: render the component, control isOpen/onClose, pass api.
The widget returns null when isOpen is false, so keeping it mounted is cheap. Each closed → open starts a fresh session — state does not carry across opens.
If you drive several configurations from one place, keep a single instance and swap the props you pass it:
Step 4 — The api prop
api propapi is the only required prop besides isOpen/onClose.
baseUrl— your allocator endpoint.rpcUrls— optional. Defaults to built-in public RPCs for balance reads. Override per chain where the public RPC is flaky (a flaky read shows balance as—, not an error).positionsBaseUrl— Earn live data only. Omit → Earn uses bundled static configs.testnetBaseUrl/testnetPositionsBaseUrl— used only whennetwork="testnet"; default to localhost.
Earn needs the positions URL; pay/swap don't:
Step 5 — Pick a mode
Pay
mode="pay" (default). You fix what's received and where; the user pays from any token they hold. Two ways to describe it.
Flat props — simplest "send X to an address":
Nested intent — full control for protocol interactions (mint, buy ticket, deposit, etc.). requiredAmount is a bigint in atomic units:
By default the destination is pinned. Set lockDestinationToken={false} to let the user re-pick it. Scope the source side with sourceChainIds, defaultSourceChainId, defaultSourceTokenAddress, sourceTokenFilter.
Routing & liquidity — restrict which solver paths are quoted (pay, swap, and earn):
Presets: any, filler-single-transaction (one user transaction with seamless end-to-end execution — protocol interactions handled behind the scenes), external-multi-transactions, custom (with solvers: [\0x…`]`). Use the same value for quote and submit — the widget forwards it to the SDK automatically.
Swap
mode="swap" — classic exchange UX; the user picks both sides. Provide an initial destination intent (lockDestinationToken is forced off internally so the user can always change what they receive):
protocol: 'swap' (or 'transfer'/'bridge') routes a pure token-in → token-out path; any other protocol string is treated as a custom protocol interaction.
Earn
mode="earn" renders deposit/withdraw over lending markets (1delta). Two data sources.
A — Static (zero backend), good for getting started:
B — Live data: set api.positionsBaseUrl and scope with the earn props:
Earn-specific notes:
Earn is mainnet-only — the 1delta upstream doesn't index testnet pools. Hide the Earn entry point when your app is in testnet mode.
The live routes need
ONEDELTA_API_KEYon the positions service, or they return503.Earn defaults
allowNetworkToggletotrue(pay/swap default tofalse).Miden funding (testnet, optional): pass an
earnMidenadapter to add an EVM/Miden source toggle. Keep@miden-sdk/*in your app — the widget only consumes the adapter shape (connect,createP2IDNote, asset list,reclaimHeight).
See BEHAVIOR.md → Earn for every earn case and caveat.
Smart Withdraw
What it is. In the withdraw tab, the Smart Withdraw toggle ("Auto-bridge & swap to any chain") lets the user redeem a lending position and receive the proceeds on a different chain and/or token — or on a Miden account — instead of the underlying on its native chain. Toggle off = the classic behaviour (receive the underlying where the position lives).
Under the hood it's a 2-leg intent, not a new contract:
Withdraw the position → underlying, on the position's source chain.
Swap / bridge that underlying to the chosen destination chain + token (or bridge to Miden).
Both legs are fused into a single EIP-5792 wallet_sendCalls batch — [withdraw, approve, depositERC20AndRegister] into The Compact — then submitted as one allocation. So it's 1 wallet prompt on a smart-account / EIP-7702 wallet, or 2 sequential prompts on a plain EOA (withdraw, then deposit — no separate approve). The widget capability-detects and falls back automatically — see Transaction Batching & EIP-7702 for the batching internals.
Integrator setup.
EVM → EVM: nothing extra. The toggle ships inside
mode="earn"'s withdraw tab — the sameEpochIntentWidgetconfig that renders Earn already exposes it.EVM → Miden (testnet only): pass the same
earnMidenadapter you use for Miden-funded deposits. The connected Miden account becomes the withdrawal recipient; the receive-token list is the Miden faucet set from the Epoch graph.
End-user flow.
Earn → Withdraw tab → pick a position.
Flip Smart Withdraw.
Pick Destination Chain + Receive Token. For Miden, pick the Miden chain option and a Miden token — a Miden wallet must be connected (its account shows under "Recipient (Miden)").
Review the quote —
You receive ≈ X · Minimum Y · 1% slippage.Confirm in the wallet. The CTA walks the stages: Quoting withdrawal… → Quoting swap route… → Preparing… → Confirm in your wallet… → Submitting…, then
onSuccessfires on settlement.
Caveats.
Degenerate route. Destination = the position's own chain and token is an in==out swap → no quote. The CTA disables with "Select a different chain or token." Pick a different chain or token.
Miden destination is testnet-only and requires a connected Miden wallet + the
earnMidenadapter. Delivery also depends on the solver network supporting EVM→Miden for the chosen faucet.1% slippage floor on the swap leg; the summary shows the guaranteed minimum. There is no settlement timeout — add your own if your UX needs one.
onQuotefires with the receive amount as the user changes destination; do user-facing success work inonSuccess.
Headless / SDK. Smart Withdraw is a composition, not a single intent — a withdraw (ProtocolInteraction) + a swap/bridge (GetTokenOut) fused into one EIP-5792 batch over a Compact resource lock. To build it without the widget, see SDK Reference → Smart Withdraw (headless) for the exact function sequence.
Theming
Three layers, least to most effort:
Slots: overlay, container, header, body, footer, receiveCard, receiveAmount, receiveLabel, payCard, payAmount, payLabel, button, chainSelector, tokenSelector, banner, progress.
Override CTA copy without touching styles via ctaLabels — see BEHAVIOR.md → CTA labels.
Wiring callbacks
None are required — the widget works with none wired. Wire the ones your app reacts to:
Rules of thumb (full detail in BEHAVIOR.md):
Put user-facing success work in
onSuccess— the modal auto-closes shortly after, so don't depend on it staying open.Treat
onClosewithout a precedingonSuccessas "outcome unknown," not "failed" — an issued intent can still settle server-side. Persist the nonce fromonIntentSentto reconcile.There is no settlement timeout; add your own if your UX needs a ceiling.
Testnet
In testnet mode the widget uses the Sepolia chain/token registries, reads intent.config.destinationTestnetChainId, and resolves api.testnetBaseUrl / api.testnetPositionsBaseUrl (both default to localhost). Base Sepolia USDC for testing: 0x036CbD53842c5426634e7929541eC2318f3dCF7e.
Caveats: Earn has no real testnet (1delta doesn't index testnet pools — dummy-lending markets only), and Miden funding is testnet-only. See BEHAVIOR.md → Caveats.
Packaging notes
The published package ships
dist/only. The CSS at@epoch-protocol/epoch-intent-widget/styles.cssis a separate export — importing the JS does not pull in styles. Import the CSS explicitly (Step 2).The package is dual-format (ESM + CJS) with bundled
.d.tstypes.React, wagmi, viem, and the Epoch SDKs are externalized — your app provides them.
Bundler notes
Dedupe singletons. Ensure a single copy of
react,react-dom,wagmi,viem,@tanstack/react-query. Duplicate copies break wagmi's hooks/context once the widget and your wallet UI both pull them in. In Vite:resolve.dedupe: ['react','react-dom','wagmi','viem','@tanstack/react-query'].Portalled modal + theme. The modal mounts into
document.body; project--epoch-*onto:root(Step 2) if your own chrome shares those tokens.Miden earn only. Miden funding needs WASM + top-level-await support from your bundler (e.g.
vite-plugin-wasm+vite-plugin-top-level-await) and the Miden SDK excluded from dep pre-bundling. Skip all of it unless you integrate Miden — it's not needed for EVM pay/swap/earn.
Checklists
Minimal Pay:
pnpm addthe widget + peers (wagmi,viem,@tanstack/react-query,lucide-react).Wrap your app:
WagmiProvider→QueryClientProvider(→ RainbowKit, optional).Import
@epoch-protocol/epoch-intent-widget/styles.cssonce at root.Render
EpochIntentWidgetwithisOpen/onClose+api.baseUrl+ flat pay props.React in
onSuccess.
Earn:
Set
api.positionsBaseUrlfor live data (else passearnMarketsSource).Ensure
ONEDELTA_API_KEYis set on the positions service.Scope with
earnChainIds/earnLenderFilteras needed.Hide the Earn entry point on testnet.
(Optional) Pass an
earnMidenadapter for Miden-funded testnet deposits and to offer Miden as a Smart Withdraw destination.Smart Withdraw (redeem → any chain/token) needs no extra props for EVM→EVM — it ships in the withdraw tab.
Theming chrome on the same tokens:
Project your theme to
:rootwiththemeToCssVars.Alias
--epoch-*into your CSS framework's color tokens.Pass the same
themeprop to the widget for consistency.
Last updated