Skip to content

Swap from your app

Goal: a working swap button. Chord finds the route and builds the transaction; the user’s wallet signs it. Chord never holds funds or keys.

const A = "https://api.getchord.xyz/v1";
const { tokens } = await fetch(`${A}/tokens?chain=5042`).then((r) => r.json());
const USDC = tokens.find((t) => t.symbol === "USDC"); // 0x3600…0000, 6 decimals

For a token that is not on the curated list, GET /arc/tokens/search?q= returns its address and decimals from the chain. Never assume 18.

Quote when the user has typed an amount and paused, not on every keystroke. Paint a first number with fast=1 and replace it with the full answer.

const params = new URLSearchParams({ chain: "5042", inputMint: USDC.address, outputMint: token.address, amount: (5n * 10n ** 6n).toString(), slippageBps: "100" });
const [fast, full] = await Promise.all([
fetch(`${A}/quote?${params}&fast=1`).then((r) => r.json()),
fetch(`${A}/quote?${params}`).then((r) => r.json()),
]);
const quote = full; // never build from `fast` (preliminary: true)
if (quote.routePlan.length === 0) return showNoRoute(quote.noRouteReason); // INSUFFICIENT_LIQUIDITY → suggest a smaller size

Show outputAmount (scaled by the output token’s decimals), priceImpactPct, feeBps, and otherAmountThreshold as the minimum received.

For an ERC-20 input — and on Arc every input is one, since the native unit aliases to the USDC predeploy — the router must be allowed to pull inputAmount. The router address is transaction.to in the /swap answer (0x1b1fee89a381e595fe6536b99ba91f3e16e9601a on both chains). Read the allowance first and skip the approve when it is enough.

const allowance = await publicClient.readContract({ address: USDC.address, abi: erc20Abi, functionName: "allowance", args: [wallet, ROUTER] });
if (allowance < BigInt(quote.inputAmount)) {
await walletClient.writeContract({ address: USDC.address, abi: erc20Abi, functionName: "approve", args: [ROUTER, BigInt(quote.inputAmount)] });
}
const { transaction } = await fetch(`${A}/swap`, {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ quoteResponse: quote, userPublicKey: wallet, chain: 5042 }),
}).then((r) => r.json());

Send the quote back unmodified. A changed body is 400 ROUTE_MISMATCH; a different slippageBps is 400 SLIPPAGE_MISMATCH — re-quote instead. Pass feeAccount if you are a referrer.

const sim = await fetch(`${A}/swap/simulate`, {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ chain: 5042, from: wallet, to: transaction.to, data: transaction.data, value: transaction.value }),
}).then((r) => r.json());
if (sim.ok === false && !/not approved/i.test(sim.revert)) return showRevert(sim.revert);

An allowance revert right after the approve is expected (the approve may not be mined yet); proceed. Any other revert, stop.

const hash = await walletClient.sendTransaction({
to: transaction.to, data: transaction.data, value: BigInt(transaction.value), gas: BigInt(transaction.gasLimit), chain: arc,
});

On Arc value is always 0. Wait for the receipt; the output lands in the user’s wallet. Explorer: https://explorer.arc.io/tx/${hash}.

  • Quote expired. A quote is good for about 30 seconds. If the user idled, quote again before /swap.
  • Price moved past the threshold. The transaction reverts with insufficient output. Quote again; do not resubmit.
  • Gateway dropped the send. Arc’s public gateway has been seen dropping one broadcast in six. Use a dedicated RPC for sendRawTransaction and retry with the same nonce on a timeout.
  • Robinhood Chain. Same flow with chain: 4663; native ETH is the zero address and is sent as value, WETH is wrapped for you. Stocks and USDG are 0 bps.
  • Contracts calling the router. GET /swap/calldata quotes and encodes in one call with the output to msg.sender.