For the complete documentation index, see llms.txt. This page is also available as Markdown.

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.


Contents


Prerequisites

Your app must be a wagmi v2 app (or become one). The widget ships these as peer dependencies — you install them:

Peer dependency
Range
Why

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:

  1. A WagmiProvider with at least one connector.

  2. A QueryClientProvider (wagmi v2 requires it).

  3. 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 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 when network="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_KEY on the positions service, or they return 503.

  • Earn defaults allowNetworkToggle to true (pay/swap default to false).

  • Miden funding (testnet, optional): pass an earnMiden adapter 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:

  1. Withdraw the position → underlying, on the position's source chain.

  2. 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 same EpochIntentWidget config that renders Earn already exposes it.

  • EVM → Miden (testnet only): pass the same earnMiden adapter 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.

  1. Earn → Withdraw tab → pick a position.

  2. Flip Smart Withdraw.

  3. 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)").

  4. Review the quote — You receive ≈ X · Minimum Y · 1% slippage.

  5. Confirm in the wallet. The CTA walks the stages: Quoting withdrawal… → Quoting swap route… → Preparing… → Confirm in your wallet… → Submitting…, then onSuccess fires 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 earnMiden adapter. 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.

  • onQuote fires with the receive amount as the user changes destination; do user-facing success work in onSuccess.

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 onClose without a preceding onSuccess as "outcome unknown," not "failed" — an issued intent can still settle server-side. Persist the nonce from onIntentSent to 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.css is 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.ts types.

  • 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:

  1. pnpm add the widget + peers (wagmi, viem, @tanstack/react-query, lucide-react).

  2. Wrap your app: WagmiProviderQueryClientProvider (→ RainbowKit, optional).

  3. Import @epoch-protocol/epoch-intent-widget/styles.css once at root.

  4. Render EpochIntentWidget with isOpen/onClose + api.baseUrl + flat pay props.

  5. React in onSuccess.

Earn:

  1. Set api.positionsBaseUrl for live data (else pass earnMarketsSource).

  2. Ensure ONEDELTA_API_KEY is set on the positions service.

  3. Scope with earnChainIds / earnLenderFilter as needed.

  4. Hide the Earn entry point on testnet.

  5. (Optional) Pass an earnMiden adapter for Miden-funded testnet deposits and to offer Miden as a Smart Withdraw destination.

  6. 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:

  1. Project your theme to :root with themeToCssVars.

  2. Alias --epoch-* into your CSS framework's color tokens.

  3. Pass the same theme prop to the widget for consistency.

Last updated