Cookbook Recipes
Short, copy-pasteable patterns for common Nexus SDK tasks.
Each recipe assumes an initialized TypeScript
List spendable, verified balances, largest first.
TypeScript
TypeScript
Provider support does not guarantee destination-call support. Each quote reports that in
Call TypeScript
A disconnected preview always rejects. After the wallet connects, start a new swap to get an executable quote.
TypeScript
The swap waits until you call
TypeScript
A bridge is a swap with the same asset's address on each chain. Omit
TypeScript
With TypeScript
Relay's router is the caller, so the contract must credit the user (here
TypeScript
Errors thrown in
TypeScript
TypeScript
Reset
TypeScript
Pages hold 20 intents for the connected wallet.
client with a connected wallet unless it says otherwise. See SDK setup. Names such as showError, openReview, params, and userAddress stand in for your app's code.
The recipes share these constants:
import { parseUnits, formatUnits } from '@avail-project/nexus-core/utils';
const ETH_USDC = '0xA0b86991c6218b36c1d19d4a2e9eb0cE3606eB48';
const ARB_USDC = '0xaf88d065e77c8cC2239327C5EDb3A432268e5831';
const BASE_USDC = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913';
const BASE_WETH = '0x4200000000000000000000000000000000000006';
const AAVE_POOL = '0xA238Dd80C259a72e81d7e4664a9801593F98d1c5'; // Aave v3 Pool on BaseShow balances in a token picker
const chainNames = new Map(client.getSupportedChains().map((c) => [c.id, c.name]));
const balances = await client.getBalances();
const rows = balances
.filter((b) => b.usable && b.verified && b.usableBalanceRaw > 0n)
.sort((a, b) => (b.valueUsd ?? 0) - (a.valueUsd ?? 0))
.map((b) => ({
key: `${b.chainId}:${b.tokenAddress}`,
label: `${b.symbol} on ${chainNames.get(b.chainId)}`,
amount: formatUnits(b.usableBalanceRaw, b.decimals),
valueUsd: b.valueUsd, // null when no price is available
}));usableBalanceRaw excludes gas reserves. Show actualBalanceRaw if you also display the full wallet balance.
Show only Relay-supported balances
const relayBalances = (await client.getBalances()).filter(
(b) => b.usable && b.providers.some((p) => p.id === 'relay'),
);quote.supportsDestinationCalls.
Preview a rate before wallet connection
swapWithExactIn after initialize() and before setEVMProvider().
import { UserActionError } from '@avail-project/nexus-core';
const from = { chainId: 1, tokenAddress: ETH_USDC } as const;
const [fromToken, toToken] = await Promise.all([
client.getToken(from),
client.getToken({ chainId: 8453, tokenAddress: BASE_WETH }),
]);
await client
.swapWithExactIn(
{
sources: [{ ...from, amountRaw: parseUnits('100', fromToken.decimals) }],
toChainId: 8453,
toTokenAddress: BASE_WETH,
},
{
hooks: {
onIntent: (hook) => {
showRate(formatUnits(hook.quote.output.amountRaw, toToken.decimals), hook.quote.fees);
hook.deny();
},
},
},
)
.catch((error) => {
if (!(error instanceof UserActionError)) showQuoteError(error);
});Swap with a review UI
const usdc = await client.getToken({ chainId: 8453, tokenAddress: BASE_USDC });
const result = await client.swapWithExactOut(
{ toChainId: 8453, toTokenAddress: BASE_USDC, toAmountRaw: parseUnits('25', usdc.decimals) },
{
hooks: {
onIntent: (hook) =>
openReview({
quote: hook.quote,
canConfirm: hook.execution.possible,
onConfirm: hook.allow,
onCancel: hook.deny,
onRefresh: () => hook.refresh(), // resolves with the replacement quote
}),
},
},
);allow() or deny(). Await refresh() before allowing, render the quote it returns, and reread hook.execution.
Bridge USDC to Base
const usdc = await client.getToken({ chainId: 8453, tokenAddress: BASE_USDC });
const result = await client.swapWithExactOut({
toChainId: 8453,
toTokenAddress: BASE_USDC,
toAmountRaw: parseUnits('100', usdc.decimals),
sources: [
{ chainId: 1, tokenAddress: ETH_USDC },
{ chainId: 42161, tokenAddress: ARB_USDC },
],
});
console.log(result.intentExplorerUrl);sources to let the quote draw from any wallet balance.
Deposit into Aave with your wallet
swapAndExecute funds the shortfall on Base, then your wallet calls the contract.
import { encodeFunctionData, parseAbi, type Hex } from 'viem';
const poolAbi = parseAbi([
'function supply(address asset, uint256 amount, address onBehalfOf, uint16 referralCode)',
]);
const supplyData = (amount: bigint, user: Hex) =>
encodeFunctionData({ abi: poolAbi, functionName: 'supply', args: [BASE_USDC, amount, user, 0] });
const usdc = await client.getToken({ chainId: 8453, tokenAddress: BASE_USDC });
const amount = parseUnits('50', usdc.decimals);
const result = await client.swapAndExecute({
toChainId: 8453,
toTokenAddress: BASE_USDC,
toAmountRaw: amount,
execute: {
to: AAVE_POOL,
data: supplyData(amount, userAddress),
gas: 300_000n,
tokenApproval: { toTokenAddress: BASE_USDC, amount, spender: AAVE_POOL },
},
});
if (result.executionMode === 'wallet') console.log(result.execute.txExplorerUrl);result.swapSkipped is true when the wallet already holds enough on Base. Narrow on executionMode before reading execute.
Deposit into Aave through Relay
useProviderDestinationExecution: true, Relay executes the call during fulfillment. This reuses supplyData from the previous recipe.
const arbUsdc = await client.getToken({ chainId: 42161, tokenAddress: ARB_USDC });
const result = await client.swapAndExecute(
{
tradeType: 'exactInput',
sources: [{ chainId: 42161, tokenAddress: ARB_USDC, amountRaw: parseUnits('50', arbUsdc.decimals) }],
toChainId: 8453,
toTokenAddress: BASE_USDC,
execute: {
to: AAVE_POOL,
gas: 300_000n,
tokenApproval: { toTokenAddress: BASE_USDC, amount: 0n, spender: AAVE_POOL },
},
},
{
useProviderDestinationExecution: true,
beforeExecute: async ({ executeAmountRaw }) => ({ data: supplyData(executeAmountRaw, userAddress) }),
},
);
console.log(result.executionMode, result.executeAmountRaw); // 'provider', fixed deposit amountonBehalfOf), not msg.sender. Unsupported routes reject before signing; see provider destination execution.
Drive a progress UI from events
import type { IntentEvent } from '@avail-project/nexus-core';
const onEvent = (event: IntentEvent) => {
switch (event.type) {
case 'quote':
setSteps(event.quote.plan.steps.map((s) => ({ id: s.id, type: s.type, state: 'pending' })));
break;
case 'step':
updateStep(event.step.id, event.state, event.errorDetails?.message);
break;
case 'status':
setStatus(event.status, event.legs.map((leg) => leg.txExplorerUrl));
break;
}
};
await client.swapWithExactOut(params, { onEvent });onEvent do not interrupt the swap. A failed step with committed: true may still settle, so check its status before retrying.
Handle quote failures
import { ERROR_CODES, NexusError, UserActionError, getIntentQuoteFailure } from '@avail-project/nexus-core';
try {
await client.swapWithExactOut(params);
} catch (error) {
if (error instanceof UserActionError) return; // user cancelled
if (!(error instanceof NexusError)) throw error;
const failure = getIntentQuoteFailure(error);
switch (error.code) {
case ERROR_CODES.BACKEND_VALUE_ABOVE_CEILING:
showError(`Maximum is $${String(failure?.details.maxValueUsd)}`);
break;
case ERROR_CODES.BACKEND_INPUT_BELOW_DEPOSIT_FEE:
showError('Increase the amount to cover the deposit fee.');
break;
case ERROR_CODES.BACKEND_NO_ROUTABLE_SOURCE:
showSourceIssues(failure?.sourceVerdicts.filter((v) => v.state === 'unroutable'));
break;
default:
showError(error.message);
}
}getIntentQuoteFailure returns null for errors that are not quote failures. These failures have retryable: false; change the request before retrying.
Paginate a token picker
import type { IntentToken } from '@avail-project/nexus-core';
let tokens: IntentToken[] = [];
let offset = 0;
let total = Infinity;
async function loadMore(chainId: number, symbol?: string) {
if (offset >= total) return;
const page = await client.getTokensByChain(chainId, { symbol, offset, limit: 50 });
tokens = [...tokens, ...page.tokens];
offset = page.offset + page.limit;
total = page.total;
}tokens, offset, and total when the chain or search changes. Pages list verified tokens only; pass includeUnverified: true to include the rest.
Show intent history
const { intents, total } = await client.listIntents({ page: 1, status: 'fulfilled' });
for (const intent of intents) {
console.log(intent.id, intent.status, intent.provider, intent.explorerUrl);
}provider is present only when the middleware reports it.How is this guide?
Show balances in a token pickerShow only Relay-supported balancesPreview a rate before wallet connectionSwap with a review UIBridge USDC to BaseDeposit into Aave with your walletDeposit into Aave through RelayDrive a progress UI from eventsHandle quote failuresPaginate a token pickerShow intent history