0xtrails
v0.18.6
Published
SDK for Trails
Readme
0xtrails
Trails SDK for embedding the widget and calling Trails hooks in React apps.
Install
npm install 0xtrails @tanstack/react-query viemIf you want Trails to use your existing wagmi setup, also install @0xtrails/adapter-wagmi and wagmi.
Standalone Widget
For widget-only usage, TrailsWidget does not require QueryClientProvider or TrailsProvider.
import { TrailsWidget } from '0xtrails'
const apiKey = 'YOUR_TRAILS_API_KEY'
export function App() {
return <TrailsWidget apiKey={apiKey} renderInline />
}When there is no parent wallet context and no adapters, the widget uses its built-in EVM runtime.
Hooks With TrailsProvider
Use TrailsProvider when you want to call Trails hooks directly.
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { TrailsProvider, useAccountTotalBalanceUsd } from '0xtrails'
const apiKey = 'YOUR_TRAILS_API_KEY'
const queryClient = new QueryClient()
function BalancePreview() {
const address = '0x0000000000000000000000000000000000000000'
const totalBalance = useAccountTotalBalanceUsd(address)
return <pre>{JSON.stringify(totalBalance.data, null, 2)}</pre>
}
export function App() {
return (
<QueryClientProvider client={queryClient}>
<TrailsProvider config={{ trailsApiKey: apiKey }}>
<BalancePreview />
</TrailsProvider>
</QueryClientProvider>
)
}Hooks
Hooks are for custom UI. Wrap your app with your existing wagmi and React Query providers, then add TrailsProvider.
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
import { WagmiProvider } from "wagmi"
import { TrailsProvider } from "0xtrails"
import { wagmiConfig } from "./wagmi"
const queryClient = new QueryClient()
export function App() {
return (
<WagmiProvider config={wagmiConfig}>
<QueryClientProvider client={queryClient}>
<TrailsProvider config={{ trailsApiKey: "<YOUR_TRAILS_API_KEY>" }}>
<YourRoutes />
</TrailsProvider>
</QueryClientProvider>
</WagmiProvider>
)
}Widget Modes
Import mode-specific wrappers from 0xtrails/widget.
Pay
Use Pay when your app knows the exact recipient, destination token, chain, and amount.
import { Pay } from "0xtrails/widget"
export function PayInvoice() {
return (
<Pay
apiKey="<YOUR_TRAILS_API_KEY>"
to={{
recipient: "0xMerchantAddress",
chain: "base",
token: "USDC",
amount: "49.99",
}}
from={{ chain: "arbitrum", token: "ETH" }}
paymentMethod="CONNECTED_WALLET"
payMessage="Invoice #INV-1001"
buttonText="Pay now"
/>
)
}Fund
Use Fund to let a user add funds to a wallet or destination account. You can allow different payment methods, use a connected wallet, qr code transfers, exchanges, or fiat on ramp.
The user selects payment method in the widget UI
import { Fund } from "0xtrails/widget"
export function FundWallet() {
return (
<Fund
apiKey="<YOUR_TRAILS_API_KEY>"
to={{
recipient: "0xUserWallet",
chain: "base",
token: "USDC",
}}
onFundingSuccess={(data) => console.log("Funded", data.sessionId)}
/>
)
}The host app pre-selects paymentMethod to credit card for funding crypto with 100 USD.
import { Fund } from "0xtrails/widget"
export function FundWallet() {
return (
<Fund
apiKey="<YOUR_TRAILS_API_KEY>"
paymentMethod="CREDIT_DEBIT_CARD"
from={{
currency="USD",
amount="100"
}}
to={{
recipient: "0xUserWallet",
chain: "base",
token: "USDC",
}}
onFundingSuccess={(data) => console.log("Funded", data.sessionId)}
/>
)
}Swap
Use Swap for same-chain and cross-chain token swaps.
import { Swap } from "0xtrails/widget"
export function SwapToBaseUsdc() {
return (
<Swap
apiKey="<YOUR_TRAILS_API_KEY>"
onSwapSuccess={(data) => console.log("Swap complete", data.sessionId)}
/>
)
}Earn
Use Earn for a built-in UI cross-chain experience for earning yield.
import { Earn } from "0xtrails/widget"
export function EarnUsdc() {
return (
<Earn
apiKey="<YOUR_TRAILS_API_KEY>"
buttonText="Deposit into a protocol"
onEarnSuccess={(data) => console.log("Earn deposit", data.sessionId)}
/>
)
}Withdraw
Use Withdraw for crypto withdrawal flows. Let users withdraw and swap their assets to any token on any chain.
import { Withdraw } from "0xtrails/widget"
export function WithdrawToBase() {
return (
<Withdraw
apiKey="<YOUR_TRAILS_API_KEY>"
onWithdrawSuccess={(data) => console.log("Withdrawn", data.sessionId)}
/>
)
}Custom Labels
Every mode accepts a labels object that renames the widget's own copy. Each
field is optional and falls back to the built-in string, so you can override
one word or all four. A blank string counts as no override too — binding a
field straight to a text input does the right thing when the box is cleared. A donation flow, for instance, is fund mode with the
funding vocabulary swapped out:
import { Fund } from "0xtrails/widget"
export function Donate() {
return (
<Fund
apiKey="<YOUR_TRAILS_API_KEY>"
to={{ recipient: "0xReliefFund", chain: "polygon", token: "USDC" }}
labels={{
confirmButton: "Donate",
formTitle: "Donate",
methodsTitle: "Choose a donation method",
tokensTitle: "Donate with any token",
}}
/>
)
}| Field | Renames | Default |
| --------------- | --------------------------------------------- | ---------------------------------------------------------------- |
| confirmButton | The primary form button | "Fund", "Pay", "Swap", "Deposit", "Withdraw", by mode |
| formTitle | The amount screen's title, and the crypto-transfer screens that continue it | "Fund", "Pay", "Swap", "Withdraw", "Earn" by mode; "Deposit" on the transfer screens |
| methodsTitle | The payment method picker's title | "Choose a funding method", "Choose a payment method" in pay and earn, "Payment methods" on the pay flow's own picker |
| tokensTitle | The title of every "pay with this" token list | Varies by mode and screen — "Fund with any token", "Select From Token", "Select Token to Withdraw", … |
formTitle deliberately follows the user into the crypto-transfer screens (the
QR code and deposit-address steps), so a donation flow keeps saying "Donate"
instead of switching to "Deposit" halfway through. The card on-ramp screens are
left alone — they hand off to a third party and read that way by design.
tokensTitle covers the origin pickers only. Destination pickers ("Receive",
"Select destination token") keep their own titles, since the token the user
sends and the token the recipient gets are different questions.
Two things the overrides deliberately leave alone:
- Transient button states. "Enter an amount", "Getting quote...", "Connect your wallet" and "Insufficient Balance" report what the widget is doing, not what the button does, so they keep their own copy.
- Screens borrowed from another mode. Overrides apply only to the configured mode. Open the swap tab from a fund widget's header and the button still says "Swap" — a "Donate" label there would be wrong.
For the modal trigger button specifically, buttonText still wins over
labels.confirmButton.
Consent Checkbox
consentCheckbox puts a consent line directly above the primary button, with a
link the user can open before they commit. Setting the prop is what makes the
tick mandatory — there is no separate flag, because an optional consent
checkbox is not a thing anyone wants.
import { Fund } from "0xtrails/widget"
export function DonateWithTerms() {
return (
<Fund
apiKey="<YOUR_TRAILS_API_KEY>"
to={{ recipient: "0xReliefFund", chain: "polygon", token: "USDC" }}
labels={{ confirmButton: "Donate" }}
consentCheckbox={{
label:
"I've read and agree to the " +
'<a href="https://example.org/terms">Terms & Conditions</a>',
requiredMessage: "Please accept the Terms & Conditions to donate.",
}}
/>
)
}| Field | Purpose | Default |
| ----------------- | ---------------------------------------------------- | -------------------- |
| label | The consent line, as inline HTML. Required. | — |
| requiredMessage | Standing notice raised by a press on an unticked box | none — see below |
A blank
labelremoves the gate. Setting the prop is what makes the tick mandatory, so a whitespace-only label means no checkbox renders and the confirm button is not gated — the flow keeps working with the consent step silently gone. That is deliberate (there is nothing to render), but it is the failure mode to watch if you bindlabelto a CMS field or an i18n key that might not resolve. The SDK logs a warning when it happens.
The button stays live. While the box is unticked the confirm button keeps its normal enabled styling, so a ready quote still reads as ready. Pressing it does not submit: the row shakes, tints, and focus moves to the checkbox.
requiredMessage decides what a failed press leaves behind. Set it and the
message stands under the checkbox until the box is ticked, so a user who missed
the shake still has the reason on screen. Omit it and the shake and tint are the
whole response — lighter, and it never changes the resting layout, but a second
press teaches nothing the first did not. Either way the failed press moves
focus to the checkbox, so assistive technology announces the unticked control.
label is sanitised. It is the one prop rendered as HTML, against a fixed
allowlist:
- Kept:
a,b,strong,i,em,span,br. Every other tag is dropped rather than escaped, so an unsupported tag renders as clean text. - Every attribute is stripped. An anchor keeps its
hrefif that href resolves tohttp(s)ormailto— which includes relative (/terms) and protocol-relative (//host/path) forms, emitted exactly as written. Anything else (javascript:,data:) is unwrapped to plain text so nothing inert looks clickable. - Surviving anchors are rewritten to
target="_blank" rel="noopener noreferrer", so reading the terms never costs the user a filled-in quote.
This allowlist keeps a label from breaking the widget or injecting an element. It is not a validator for a URL you interpolate from somewhere else — if the href comes from a CMS, a query string or user input, check it yourself before it gets here.
Opening the link never ticks the box — the two are deliberately separate acts.
If you would rather the link opened your own in-page modal, add a delegated
click listener and call preventDefault(); the widget renders inside a shadow
root, but its events are composed, so composedPath() sees the anchor.
Consent is per session and never persisted. A reload asks again, which is what makes the tick evidence of assent rather than a remembered preference.
Like labels, the checkbox belongs to the configured mode's screens. Open the
swap tab from a fund widget's header and it does not follow.
useQuote
useQuote gives you a quote and an executable send function for a fully custom UI. Exactly one amount should be provided: from.amount or from.amountRaw for exact-input, or to.amount or to.amountRaw for exact-output. You can use "feeOptions" to allow paying gas in stablecoins.
import { useQuote, QuoteError } from "0xtrails"
import { useAccount, useWalletClient } from "wagmi"
export function CustomSwap() {
const { address } = useAccount()
const { data: walletClient } = useWalletClient()
const {
quote,
send,
isLoadingQuote,
quoteError,
quoteErrorPrettified,
feeOptions,
refetchQuote,
abort,
} = useQuote({
from: {
chain: "base",
token: "USDC",
amount: "10",
},
to: {
chain: "arbitrum",
token: "ETH",
recipient: address,
},
onStatusUpdate: (states) => console.log("status", states),
})
if (quoteError instanceof QuoteError) {
console.log("Quote failed with code", quoteError.code)
}
return (
<section>
{isLoadingQuote && <p>Loading quote...</p>}
{quoteErrorPrettified && <p>{quoteErrorPrettified}</p>}
{quote && (
<>
<p>
Spend {quote.originAmountDisplay} for{" "}
{quote.destinationAmountDisplay}
</p>
<p>Estimated time: {quote.completionEstimateDisplay}</p>
<p>Fee options: {feeOptions.length}</p>
</>
)}
<button disabled={!send} onClick={() => send?.()}>
Send
</button>
<button onClick={() => refetchQuote()}>Refresh</button>
<button onClick={() => abort()}>Abort</button>
</section>
)
}For previewing a quote, without needing a connected wallet, pass walletAddress.
useTrailsSendTransaction
useTrailsSendTransaction is a destination-first transaction hook with Trails routing. It mirrors useQuote's from/to shape and can open the Trails widget modal when the user needs to choose the origin chain, token, or amount so you don't have to build this step.
Render TrailsHookModal once inside TrailsProvider so modal-driven sends have somewhere to mount.
Describe what the recipient should receive via to and let the user pick how to pay. Set to.amount for an exact-output send (the modal only needs origin token selection).
import { useTrailsSendTransaction } from "0xtrails"
export function RequestUsdcPayment() {
const { sendTransaction, isPending } = useTrailsSendTransaction({
receiptActionButtonText: "Back to app",
})
return (
<>
<button
disabled={isPending}
onClick={() =>
sendTransaction({
to: {
chain: "polygon",
token: "USDC", // or a contract address
recipient: "0xMerchantAddress",
amount: "25",
},
})
}
>
Pay 25 USDC
</button>
</>
)
}Provide from ({ chain, token, amount }) when your app already knows the source asset to skip the modal. Use from.amountRaw/to.amountRaw when you already have smallest-unit amounts.
Composable Actions
Composable actions describe what should happen after funds arrive on the destination chain. They are resolved into destination calls by the SDK and can be used from both useQuote and useTrailsSendTransaction.
Available action builders include:
swapfor destination-chain swaps.lendanddepositfor supported earn markets.customfor bulding your own action.assertConditionfor runtime checks.dynamic()to spend the intent wallet's runtime balance when value amounts are unknown.
Quote And Execute Actions
import {
assertCondition,
dynamic,
lend,
swap,
useQuote,
} from "0xtrails"
import { useAccount, useWalletClient } from "wagmi"
export function QuoteActions() {
const { address } = useAccount()
const { data: walletClient } = useWalletClient()
const actions = [
// Check if we received enough USDC on the destination chain
assertCondition({
erc20Balance: { token: "USDC", gte: "100" },
}),
// Swap 100 USDC in WETH (defaults to Uniswap V3)
swap({
tokenIn: "USDC",
tokenOut: "WETH",
amountIn: "100",
fee: "0.05"
}),
// Lend all WETH received from the swap
lend({
marketId: "base-weth-aave-v3-lending",
amount: dynamic(),
receiverAddress: address,
}),
]
const { quote, send, isLoadingQuote } = useQuote({
from: { chain: "base", token: "USDC" },
to: { chain: "base", token: "USDC", amount: "100" },
actions,
})
return (
<button disabled={!send || isLoadingQuote} onClick={() => send?.()}>
{quote ? "Execute recipe" : "Get quote"}
</button>
)
}actions is mutually exclusive with to.calls and deprecated to.calldata.
Execute Actions With The Widget Modal
import { dynamic, erc20Utils, lend, swap, useTrailsSendTransaction } from "0xtrails"
export function WidgetActionRecipe({ userAddress }: { userAddress: `0x${string}` }) {
const actions = [
swap({
tokenIn: "USDC",
tokenOut: "USDT",
amountIn: dynamic(),
fee: "0.05",
}),
lend({
marketId: "polygon-usdt-fluid-lending",
amount: dynamic(),
receiverAddress: userAddress,
}),
]
const { sendTransaction, isPending } = useTrailsSendTransaction({
actions,
receiptActionButtonText: "Done",
})
return (
<>
<button
disabled={isPending}
onClick={() =>
sendTransaction({
to: {
chain: "polygon",
token: erc20Utils.USDC.addressOn("polygon"),
recipient: userAddress,
amount: "50"
},
})
}
>
Choose payment token
</button>
</>
)
}Raw Destination Calls
Use to.calls when you need low-level ABI-encoded calls instead of action builders.
import { buildApproveAndCall, buildCall, erc20Utils, useQuote } from "0xtrails"
import { useAccount } from "wagmi"
import { stakingAbi } from "./stakingAbi"
export function RawCallsQuote() {
const { address } = useAccount()
const stakingCall = buildCall({
to: "0xStakingContract",
data: {
abi: stakingAbi,
functionName: "stake",
args: [1_000_000n],
},
})
const calls = buildApproveAndCall({
token: erc20Utils.USDC.addressOn("base"),
call: stakingCall,
})
const { quote, send } = useQuote({
from: { chain: "base", token: "USDC" },
to: {
chain: "base",
token: "USDC",
amount: "1",
calls,
},
})
return (
<button disabled={!send} onClick={() => send?.()}>
{quote ? "Stake USDC" : "Loading quote"}
</button>
)
}Utilities
Common exports from 0xtrails:
getSupportedChains,useSupportedChains, andresolveChainId.getChainInfo,getAllChains, andattemptSwitchChain.useTokenList,useSupportedTokens,useTokenBalances,useTokenBalancesForMultipleAccounts,useAccountTotalBalanceUsd,useHasSufficientBalanceToken, anduseHasSufficientBalanceUsd.getTokenPrice,getTokenPrices,useTokenPrice,useTokenPriceserc20Utils,buildCall,buildApproveAndCall,buildErc20Approve, andgetAmountWithSlippagefor token metadata, ERC-20 calls, approvals, and custom calldata.uniswapV3andsushiswapV3for custom Uniswap V3 / SushiSwap V3 pool lookup, quote, and swap call construction.useEarnProviders,useEarnMarkets,useEarnBalances,getEarnProviders,getEarnMarkets, andgetEarnBalances.useResolveActions,encodeDestinationForIntentProtocol,createHydratePayload, andencodeMulticallHydrateExecutefor advanced destination-call and hydrate flows.prepareSend,commitIntent,quoteIntent,sendOriginTransaction,getIntent,useGetIntent,pollIntentReceipt, anduseIntentReceiptMonitor.getAccountTransactionHistory,useAccountTransactionHistory,useIntentTransactionHistory, andgetTxTimeDiff.useFiatCurrencyList,useExchangeRate,getCurrencyInfogetTrailsClient,useTrailsClient,TrailsClientuseIntentRecover,useIntentRecoverWithAddress,buildRefundTransactionWithSignature, andsignPayload.
TrailsProvider mounts the built-in wallet runtime by default. Add config.adapters when Trails should use explicit wallet runtime adapters instead.
Use Trails with wagmi
If your app already uses wagmi, add @0xtrails/adapter-wagmi so Trails can share your wallet setup. Wagmi 3 is the primary integration target; wagmi 2 remains supported through the same adapter peer range.
For more options and wagmi 2/3 examples, see packages/wallet/adapters/wagmi/README.md, apps/examples/wagmi3, and apps/examples/wagmi2.
import { TrailsProvider, TrailsWidget } from '0xtrails'
import { wagmiAdapter } from '@0xtrails/adapter-wagmi'
import { WagmiProvider } from 'wagmi'
const apiKey = 'YOUR_TRAILS_API_KEY'
// Create wagmiConfig with your chains, transports, and connectors.
const adapters = [wagmiAdapter({ wagmiConfig })]
export function App() {
return (
<WagmiProvider config={wagmiConfig}>
<TrailsProvider config={{ trailsApiKey: apiKey, adapters }}>
<TrailsWidget renderInline />
</TrailsProvider>
</WagmiProvider>
)
}Use the same wagmiConfig instance for both WagmiProvider and wagmiAdapter(...) when Trails should bridge into the host-owned wagmi runtime. If there is no matching surrounding WagmiProvider, the adapter mounts its own runtime with the provided config.
Runtime ownership
Adapter runtime selection and lifecycle ownership are separate:
- wagmi runtime selection is based on
wagmiConfigidentity- same config instance as the surrounding
WagmiProvider→ bridge to the host runtime - no matching surrounding provider → mount an adapter-owned runtime
- same config instance as the surrounding
managedByis for composite adapter lists and controls lifecycle ownership in TrailsmanagedBy: 'host'means Trails can use the wallet but should not own host lifecycle actions like disconnectmanagedBy: 'trails'means Trails owns that adapter's wallet lifecycle
const adapters = [
{ adapter: wagmiAdapter({ wagmiConfig: hostWagmiConfig }), managedBy: 'host' },
{ adapter: wagmiAdapter({ wagmiConfig: trailsWagmiConfig }), managedBy: 'trails' },
]Development
Regenerate widget SVG components from a folder of raw SVGs with pnpm --dir packages/0xtrails icons:generate <source-dir>.
Rules
wagmiis optional and only needed for adapter-based integrations; wagmi 3 is the primary target and wagmi 2 remains supported for compatibility- hooks require both
QueryClientProviderandTrailsProvider adapterscan live onTrailsProvider config.adaptersorTrailsWidget adapters- do not provide adapters in both places at the same time
TrailsWidget adapterscannot be used inside an existing parent wallet context, including the default runtime mounted byTrailsProvider
Use Trails with Solana
Install @0xtrails/svm to enable Solana edge payments — users fund from a Solana wallet and the payment lands on any EVM chain.
npm install @0xtrails/svmPass svmAdapter alongside your EVM adapter. With no wallet option, it auto-detects Wallet Standard Solana wallets such as Phantom and Solflare:
import { TrailsWidget, evmAdapter } from '0xtrails'
import { svmAdapter } from '@0xtrails/svm'
const adapters = [
evmAdapter({ wallets: [{ id: 'injected', name: 'MetaMask' }] }),
svmAdapter({ rpcUrls: ['https://api.mainnet-beta.solana.com'] }),
]
export function App() {
return (
<TrailsWidget
apiKey="YOUR_TRAILS_API_KEY"
adapters={adapters}
fundOptions={{ fundMethodsList: ['connected-wallet'] }}
renderInline
/>
)
}Solana funding is available through the connected-wallet funding path when a Solana adapter is configured. Supported origin tokens are SOL, USDC, USDT, and PYUSD.
With wagmi
Combine wagmiAdapter and svmAdapter when your app uses both EVM and Solana wallets:
import { wagmiAdapter } from '@0xtrails/adapter-wagmi'
import { svmAdapter } from '@0xtrails/svm'
const adapters = [
wagmiAdapter({ wagmiConfig }),
svmAdapter({ rpcUrls: ['https://api.mainnet-beta.solana.com'] }),
]For more options and examples, see packages/0xtrails-svm/README.md.
License
Apache-2.0
