Observed since 20 Oct, 17:54 UTC
No superseded metadata states collected. Unchanged observations do not create new revisions.
SDK for Trails
Trails SDK for embedding the widget and calling Trails hooks in React apps.
npm install 0xtrails @tanstack/react-query viem
If you want Trails to use your existing wagmi setup, also install @0xtrails/adapter-wagmi and wagmi.
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.
TrailsProviderUse 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 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>
)
}
Import mode-specific wrappers from 0xtrails/widget.
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"
/>
)
}
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)}
/>
)
}
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)}
/>
)
}
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)}
/>
)
}
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)}
/>
)
}
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:
For the modal trigger button specifically, buttonText still wins over
labels.confirmButton.
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:
a, b, strong, i, em, span, br. Every other tag is dropped
rather than escaped, so an unsupported tag renders as clean text.href if that href
resolves to http(s) or mailto — 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.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.
useQuoteuseQuote 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.
useTrailsSendTransactionuseTrailsSendTransaction 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 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:
swap for destination-chain swaps.lend and deposit for supported earn markets.custom for bulding your own action.assertCondition for runtime checks.dynamic() to spend the intent wallet's runtime balance when value amounts are unknown.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.
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>
</>
)
}
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>
)
}
Common exports from 0xtrails:
getSupportedChains, useSupportedChains, and resolveChainId.getChainInfo, getAllChains, and attemptSwitchChain.useTokenList, useSupportedTokens, useTokenBalances, useTokenBalancesForMultipleAccounts, useAccountTotalBalanceUsd, useHasSufficientBalanceToken, and useHasSufficientBalanceUsd.getTokenPrice, getTokenPrices, useTokenPrice, useTokenPriceserc20Utils, buildCall, buildApproveAndCall, buildErc20Approve, and getAmountWithSlippage for token metadata, ERC-20 calls, approvals, and custom calldata.uniswapV3 and sushiswapV3 for custom Uniswap V3 / SushiSwap V3 pool lookup, quote, and swap call construction.useEarnProviders, useEarnMarkets, useEarnBalances, getEarnProviders, getEarnMarkets, and getEarnBalances.useResolveActions, encodeDestinationForIntentProtocol, createHydratePayload, and encodeMulticallHydrateExecute for advanced destination-call and hydrate flows.prepareSend, commitIntent, quoteIntent, sendOriginTransaction, getIntent, useGetIntent, pollIntentReceipt, and useIntentReceiptMonitor.getAccountTransactionHistory, useAccountTransactionHistory, useIntentTransactionHistory, and getTxTimeDiff.useFiatCurrencyList, useExchangeRate, getCurrencyInfogetTrailsClient, useTrailsClient, TrailsClientuseIntentRecover, useIntentRecoverWithAddress, buildRefundTransactionWithSignature, and signPayload.TrailsProvider mounts the built-in wallet runtime by default. Add config.adapters when Trails should use explicit wallet runtime adapters instead.
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.
Adapter runtime selection and lifecycle ownership are separate:
wagmiConfig identity
WagmiProvider → bridge to the host runtimemanagedBy is for composite adapter lists and controls lifecycle ownership in Trails
managedBy: 'host' means Trails can use the wallet but should not own host lifecycle actions like disconnectmanagedBy: 'trails' means Trails owns that adapter's wallet lifecycleconst adapters = [
{ adapter: wagmiAdapter({ wagmiConfig: hostWagmiConfig }), managedBy: 'host' },
{ adapter: wagmiAdapter({ wagmiConfig: trailsWagmiConfig }), managedBy: 'trails' },
]
Regenerate widget SVG components from a folder of raw SVGs with pnpm --dir packages/0xtrails icons:generate <source-dir>.
wagmi is optional and only needed for adapter-based integrations; wagmi 3 is the primary target and wagmi 2 remains supported for compatibilityQueryClientProvider and TrailsProvideradapters can live on TrailsProvider config.adapters or TrailsWidget adaptersTrailsWidget adapters cannot be used inside an existing parent wallet context, including the default runtime mounted by TrailsProviderInstall @0xtrails/svm to enable Solana edge payments — users fund from a Solana wallet and the payment lands on any EVM chain.
npm install @0xtrails/svm
Pass 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.
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.
Apache-2.0
Download counts and popularity will appear here as the collector receives them.
| Version | Published | Direct dependencies | Open version |
|---|---|---|---|
| 0.19.1Registry latest | 7 Oct 2026 | 37 | |
| 0.19.0 | 7 Oct 2026 | 37 | |
| 0.18.6 | 16 Sept 2026 | 37 | |
| 0.18.5 | 10 Sept 2026 | 37 | |
| 0.18.4 | 1 Sept 2026 | 37 | |
| 0.18.3 | 30 Aug 2026 | 37 | |
| 0.18.2 | 19 Aug 2026 | 37 | |
| 0.18.1 | 18 Aug 2026 | 37 | |
| 0.18.0 | 18 Aug 2026 | 37 | |
| 0.17.1 | 27 Jul 2026 | 36 | |
| 0.17.0 | 21 Jul 2026 | 36 | |
| 0.16.2 | 9 Jun 2026 | 41 | |
| 0.16.1 | 4 Jun 2026 | 41 | |
| 0.16.0 | 21 May 2026 | 41 | |
| 0.0.0-canary-f34333b4e54352fe3e761ba2f3ffd4117d041fe6 | 20 May 2026 | 41 | |
| 0.0.0-canary-3a59770274bcb6f3bebd5d1b93a2c92d1fc4edbd | 8 May 2026 | 42 | |
| 0.15.1 | 7 May 2026 | 43 | |
| 0.15.0 | 4 May 2026 | 43 | |
| 0.0.0-canary-d5b980adf75ce5a288cc968588580fcad1540b7c | 29 Apr 2026 | 43 | |
| 0.0.0-canary-5e6e9be4ef5eafc6cbf711872e5a63f41346be25 | 29 Apr 2026 | 43 |
This describes our collected data; it is not a security assessment.
Observed since 20 Oct, 17:54 UTC
No superseded metadata states collected. Unchanged observations do not create new revisions.
package:b2736a63eaf2223c4c9759233b8731760261e76791a5024c6194b94581f16412