Skip to main content

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​

Swap.tsx
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.

  • onConnectWallet runs when the component needs a wallet. Pass whatever opens your connect flow: RainbowKit's openConnectModal, ConnectKit's setOpen(true), or your own.
  • wallet tells 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.
  • hfAccessToken is 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/wagmi is 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​

PropType
hfAccessTokenstring | nullYour Hugging Face token for plain-English parsing. Without it, parsing is off.
walletIntentWallet | nullThe connected wallet.
onConnectWallet() => voidOpens your connect flow.
theme{ mode, accent, accentText, radius, fontFamily }See Styling.
examples{ label, sentence }[] | falseClickable suggestions under the field. false hides them.
placeholderstringText in the empty field.
defaultValuestringPre-fills the field. It isn't run until the user submits.
onOrderSubmitted(orderId: string) => voidRecoilPay accepted the signed order.
onComplete(state: IntentState) => voidEvery intent in the sentence reached a final status.
onError(message: string) => voidAny failure. message is written for the end user.
apiUrlstringDefaults to https://api.recoilpay.com.
sessionIntentSessionShare one flow between components. See below.
classNamestringAdded to the root element.
onCashOutStarted, onPurchaseStarted(tradeId: string) => voidA cash-out or purchase opened. Store the id so the user can come back to it.
onCashOutEnded, onPurchaseEnded(trade: FiatTrade) => voidIt settled, was refunded or failed.
resumeCashOut, resumePurchasestring | nullShow this trade on mount, e.g. an id saved from the callbacks above.
offrampSession, onrampSessionOfframpSession, OnrampSessionShare or inject the fiat flows.
launchKyc(launch) => voidOpens 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;
}
VariableUsed for
--rp-surface, --rp-raised, --rp-sunkenCard backgrounds, from front to back
--rp-ink, --rp-ink-2, --rp-mutedText, from strongest to quietest
--rp-lineBorders and dividers
--rp-accent, --rp-accent-textPrimary buttons, highlights, focus ring
--rp-success, --rp-dangerSettled / failed states and errors
--rp-radiusCorner radius
--rp-font, --rp-monoText 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 });