React component
<RecoilIntent /> is the RecoilPay intent experience as a React component. It uses the wallet connection your app already has, so there's no second connect button and no second wallet library.
npm install @recoilpay/intent-react viem
It needs React 18+ and viem 2. wagmi is optional.
With wagmi and RainbowKit
import { RecoilIntent } from '@recoilpay/intent-react';
import { useWagmiIntentWallet } from '@recoilpay/intent-react/wagmi';
import '@recoilpay/intent-react/styles.css';
import { useConnectModal } from '@rainbow-me/rainbowkit';
export function Swap() {
const wallet = useWagmiIntentWallet(); // null until connected
const { openConnectModal } = useConnectModal();
return (
<RecoilIntent
hfAccessToken={import.meta.env.VITE_HF_ACCESS_TOKEN}
wallet={wallet}
onConnectWallet={openConnectModal}
/>
);
}
That's it. Put <Swap /> anywhere inside your existing WagmiProvider.
onConnectWalletruns when the component needs a wallet. Pass whatever opens your connect flow: RainbowKit'sopenConnectModal, ConnectKit'ssetOpen(true), or your own.wallettells the component which account is connected. If a user types an intent before connecting, the flow waits and continues on its own once they connect.hfAccessTokenis your Hugging Face token for plain-English parsing, which runs in the browser. It ends up in your bundle, so use a fine-grained, inference-only token. See Bring your own Hugging Face token.@recoilpay/intent-react/wagmiis a separate entry point, so apps without wagmi never load it.
Import the stylesheet once, anywhere in your app. It's scoped (see Styling).
Without wagmi
Pass any IntentWallet. From a viem WalletClient:
import { RecoilIntent, viemWallet } from '@recoilpay/intent-react';
import '@recoilpay/intent-react/styles.css';
<RecoilIntent
wallet={walletClient ? viemWallet(walletClient) : null}
onConnectWallet={connect}
/>
Using another wallet SDK, such as an embedded or smart wallet? Implement IntentWallet directly. It's six members; see Headless SDK → Wallets.
Props
| Prop | Type | |
|---|---|---|
hfAccessToken | string | null | Your Hugging Face token for plain-English parsing. Without it, parsing is off. |
wallet | IntentWallet | null | The connected wallet. |
onConnectWallet | () => void | Opens your connect flow. |
theme | { mode, accent, accentText, radius, fontFamily } | See Styling. |
examples | { label, sentence }[] | false | Clickable suggestions under the field. false hides them. |
placeholder | string | Text in the empty field. |
defaultValue | string | Pre-fills the field. It isn't run until the user submits. |
onOrderSubmitted | (orderId: string) => void | RecoilPay accepted the signed order. |
onComplete | (state: IntentState) => void | Every intent in the sentence reached a final status. |
onError | (message: string) => void | Any failure. message is written for the end user. |
apiUrl | string | Defaults to https://api.recoilpay.com. |
session | IntentSession | Share one flow between components. See below. |
className | string | Added to the root element. |
onCashOutStarted, onPurchaseStarted | (tradeId: string) => void | A cash-out or purchase opened. Store the id so the user can come back to it. |
onCashOutEnded, onPurchaseEnded | (trade: FiatTrade) => void | It settled, was refunded or failed. |
resumeCashOut, resumePurchase | string | null | Show this trade on mount, e.g. an id saved from the callbacks above. |
offrampSession, onrampSession | OfframpSession, OnrampSession | Share or inject the fiat flows. |
launchKyc | (launch) => void | Opens the identity check before a first purchase. Defaults to openDojah, Dojah's web widget. |
<RecoilIntent
wallet={wallet}
onConnectWallet={openConnectModal}
theme={{ mode: 'light', accent: '#0a7cff', radius: 8 }}
examples={[{ label: 'Move 5 USDC to Base', sentence: 'swap 5 USDC on op sepolia for USDC on base sepolia' }]}
onComplete={(s) => analytics.track('recoilpay_settled', { orderId: s.orderId })}
/>
Cash out and buy
The component handles fiat with no extra setup. cash out 50 USDC on Base Sepolia to NGN shows the best offer and a form for bank details, which are encrypted on the device for the solver who pays. buy ₦20,000 of USDC on Base Sepolia runs a one-time identity check, locks the solver's USDC, then shows where to pay. While a cash-out or purchase needs the user, the text field is disabled so a new sentence can't hide it.
Both can take a while, and both need the user to come back, so persist the trade id:
const [cashOut, setCashOut] = useState(() => localStorage.getItem('cash-out'));
<RecoilIntent
wallet={wallet}
resumeCashOut={cashOut}
onCashOutStarted={(id) => { localStorage.setItem('cash-out', id); setCashOut(id); }}
onCashOutEnded={() => { localStorage.removeItem('cash-out'); setCashOut(null); }}
/>
Purchases work the same way with resumePurchase, onPurchaseStarted and onPurchaseEnded. The wallet must support signMessage (viemWallet and useWagmiIntentWallet do). useOfframp() and useOnramp() give you either flow as state, for your own UI. More in Fiat ramps.
Styling
Every rule in styles.css is scoped under .rp-root, the component's own root element, so it can't restyle anything else on your page. By default the component uses your page's font.
For simple changes, use the theme prop. For anything more, override the CSS variables from your own stylesheet:
.checkout .rp-root {
--rp-surface: #0f1115;
--rp-line: #222631;
--rp-accent: #22c55e;
--rp-accent-text: #04120a;
--rp-radius: 6px;
}
| Variable | Used for |
|---|---|
--rp-surface, --rp-raised, --rp-sunken | Card backgrounds, from front to back |
--rp-ink, --rp-ink-2, --rp-muted | Text, from strongest to quietest |
--rp-line | Borders and dividers |
--rp-accent, --rp-accent-text | Primary buttons, highlights, focus ring |
--rp-success, --rp-danger | Settled / failed states and errors |
--rp-radius | Corner radius |
--rp-font, --rp-mono | Text and monospace fonts |
Your own UI, same flow
If you want different markup, useRecoilIntent() gives you the same flow as React state and renders nothing:
import { useRecoilIntent } from '@recoilpay/intent-react';
function MyIntentBar({ wallet }) {
const intent = useRecoilIntent({ wallet });
if (intent.phase === 'quoted' && intent.preview) {
const p = intent.preview;
return (
<MyQuoteCard
pay={`${p.payAmount} ${p.paySymbol} on ${p.srcChainName}`}
receive={`${p.receiveAmount} ${p.receiveSymbol} on ${p.dstChainName}`}
onConfirm={intent.confirm}
onCancel={intent.reset}
/>
);
}
return <MyInput disabled={!intent.ready} onSubmit={(text) => intent.run(text)} />;
}
The fields (phase, preview, issues, status, explorerUrl and so on) are documented in Headless SDK → The state.
Sharing a flow
Each <RecoilIntent /> creates its own session. To drive one flow from two places, for example an input in your header and a confirm card in a drawer, create the session yourself and pass it to both:
import { createIntentSession, RecoilIntent, useRecoilIntent } from '@recoilpay/intent-react';
const session = createIntentSession(); // module scope, or in a context
<RecoilIntent session={session} wallet={wallet} onConnectWallet={connect} />
// elsewhere:
const intent = useRecoilIntent({ session });
Next.js
The component talks to the user's wallet, and it starts loading solver coverage as soon as it's created, so render it on the client only.
App Router: use it from a client component.
'use client';
Pages Router: load it without server rendering.
import dynamic from 'next/dynamic';
const Swap = dynamic(() => import('../components/Swap'), { ssr: false });