Build

Mount the widget

The widget is a React component that quotes, routes, signs and follows a deposit, and does the same for a withdrawal back to native USDC on the chains the user chooses. It runs on wagmi, so it drops into the stack a protocol already has.

Install

pnpm add @inletkit/widget @inletkit/sdk

Three lines

app.tsx
import {
  InletProvider,
  DepositWidget,
  erc4626Destination,
} from "@inletkit/widget";
import "@inletkit/widget/styles.css";
 
const vault = erc4626Destination({
  id: "my-vault",
  name: "My USDC Vault",
  destinationDomain: 6,
  receiver: "0x30695D945039FbBc0C36F595a8B5B54d83a945De",
  vault: "0xYourVaultOnBaseSepolia",
});
 
<InletProvider privyAppId={PRIVY_APP_ID} relayerUrl={RELAYER_URL}>
  <DepositWidget destinations={[vault]} />
</InletProvider>

DepositWidget is the deposit form on its own. InletWidget puts deposit and withdraw under one header with a toggle, and takes the same destinations:

app.tsx
import { InletProvider, InletWidget, testnetDestinations } from "@inletkit/widget";
 
<InletProvider privyAppId={PRIVY_APP_ID} relayerUrl={RELAYER_URL}>
  <InletWidget destinations={testnetDestinations} />
</InletProvider>

Any ERC 4626 vault over USDC on a chain with a receiver needs no contract work. For Aave V3, Compound III and Uniswap v4 pools with USDC on one side, the presets in @inletkit/widget already point at the deployed adapters. Anything else needs one adapter with one function.

Inside an app that already runs wagmi

InletProvider brings Privy login and wagmi for apps that have neither. An app that already runs wagmi renders InletWidget or DepositWidget inside its own provider and skips InletProvider. For EVM sources the widget only uses wagmi hooks and the relayerUrl prop, and it falls back to the first wagmi connector for the connect button. Solana Devnet needs InletProvider, because Phantom is connected through Privy; without it the From list shows the EVM chains only.

Props

InletWidget

PropTypeMeaning
destinationsDestination[]Positions a user can deposit into. The ones that carry an exit entry are also offered for withdrawal
relayerUrlstringOverrides the relayer from InletProvider
sourcesSourceChain[]Chains a deposit can start from
defaultDestinationIdstringWhich destination starts selected
defaultMode"deposit" | "withdraw"Which form shows first. Defaults to deposit
onModeChange(mode) => voidCalled when the user flips the toggle
onRecord(record: IntentRecord) => voidEvery update of a deposit record
onExit(record: ExitRecord) => voidEvery update of a withdrawal record

DepositWidget

PropTypeMeaning
destinationsDestination[]The positions a user can deposit into. The first one is selected unless defaultDestinationId says otherwise
relayerUrlstringOverrides the relayer from InletProvider
sourcesSourceChain[]Chains a deposit can start from. Defaults to every source in the config: Base Sepolia, Arbitrum Sepolia, Unichain Sepolia, Ethereum Sepolia, Monad Testnet and Solana Devnet
defaultAmountstringInitial amount in USDC. Defaults to "1"
defaultDestinationIdstringWhich destination starts selected
titlestringThe widget heading
onRecord(record: IntentRecord) => voidCalled with every update of the intent record, from creation to the final state. Use it to show the deposit elsewhere in your app or to route to a status page

ExitWidget takes destinations, relayerUrl, defaultDestinationId, title and onExit, and only shows the destinations that carry an exit entry.

InletProvider

PropTypeMeaning
privyAppIdstringYour Privy app. Email and wallet login, an embedded wallet for users without one
relayerUrlstringThe relayer the widget talks to. Point it at your own or at the hosted one
rpcRecord<number, string>RPC overrides by chain id
appearance{ theme, accentColor, logo }Passed to Privy's login modal so it matches your app

Destination builders

BuilderAdapterArguments
erc4626Destinationerc4626:v1vault, optionally minShares, and exitContract to make the vault withdrawable when it implements EIP 2612
aaveV3Destinationaave-v3:v1pool, and optionally minATokens
compoundV3Destinationcompound-v3:v1comet, and optionally minSupplied
uniswapV4LpDestinationuniswap-v4-lp:v1poolKey, rangeTicks, minLiquidity, and a price hint for the live quote

Every builder also takes id, name, destinationDomain, receiver and an explorer base URL. The ready made presets for the testnet destinations are exported as testnetDestinations, with findDestination(record) to look one up from an intent record. The Aave V3, Morpho and Compound III presets carry an exit entry, so they show up on the withdraw side as well. A destination of your own is withdrawable once its exit names the exit adapter, the position token, the kind of signature it accepts and the InletExit on its chain.

What the user sees

Before login the form is visible: destination, source chain, the wallet that pays, amount, and the route. The wallet row follows the source: for an EVM chain it is the session wallet with its network, for Solana Devnet it lists the connected Solana wallets, Phantom before Privy's embedded one, with a button to connect another, and it says which EVM address will own the position. After login the widget quotes the deposit every time the amount changes: the route it picked, what the user sends, what the position receives, Circle's fee at most, the wallet balance and the Gateway balances on every source chain. When a Gateway balance on another chain can pay, the widget moves the source there and says so, without touching the wallet's network. Deposit stays disabled until the route can succeed. From Solana Devnet the widget asks for a Solana wallet through Privy, Phantom for instance, quotes the CCTP route and sends one transaction on Solana; the position still lands to the user's EVM address, and a refund would go back to the Solana USDC account that paid.

After the signature the widget shows the status timeline: intent registered, USDC on Arc, swept, attested, position delivered, each with its explorer link. A user who wants gasless deposits later can fund a Gateway balance from the same widget.

On the withdraw side the user picks one of the positions the wallet holds, shown with its live balance, enters an amount with a Max control, and lists where the USDC should land: one chain by default, up to four, the last one taking the rest. The quote shows the position redeemed, the USDC out at least, Circle's fee at most and what lands on each chain. One signature follows. An injected wallet has to be on the position's chain to sign it, which is a network switch and nothing else. The timeline then shows the redemption, Circle's attestation and one landing per chain, each with its link.

Theming

The widget is styled through CSS custom properties on .inlet, so a host app restyles it without touching its markup:

theme.css
.inlet {
  --inlet-bg: #393e53;
  --inlet-fg: #f5f5f5;
  --inlet-muted: #8e98ac;
  --inlet-line: rgba(245, 245, 245, 0.18);
  --inlet-accent: #f08a59;
  --inlet-accent-fg: #2d3142;
  --inlet-warn: #ffee8c;
  --inlet-warn-bg: rgba(255, 238, 140, 0.08);
  --inlet-done: #a6eca8;
  font-family: inherit;
}

The defaults are a light theme with a dark variant behind prefers-color-scheme.

Following a deposit or a withdrawal outside the widget

onRecord hands you the IntentRecord on every change and onExit the ExitRecord. Each hash identifies the record on the relayer, so GET /intents/:hash or GET /exits/:hash returns the same record from any client, and the live app accepts ?hash= for either.