Swagger

Swaps end to end

Quoting a LI.FI route, preparing the one the user reviewed, and signing it on whichever rail the network uses — same chain, on Avalanche, or ending on another chain.

Quote, prepare, sign, track

A swap is one operation, with gas settled in the token being sent, that ends holding a different token. Routes come from LI.FI, across the exchanges and bridges it aggregates; UGTP prices the operation, relays it and tracks it. Give the request a different destination chain and the same call becomes a cross-chain swap.

Two things are worth internalising before the calls. The route is NOT yours to carry: prepare fetches its own, because the one your user reviewed is minutes old by the time they press the button. And the rail is the same three-way branch as an ordinary transfer — a swap on Avalanche answers typed data, a swap anywhere else answers a user operation.

Four things that decide how you build the screen

A quote is a preview; prepare re-routes

Quote and prepare each fetch their own route from LI.FI. By the time a user has read a quote and pressed a button the calldata behind it has usually expired, so prepare cannot reuse it. That is why the rate in the prepare response can differ from the one you showed, and why the next fact exists.

Send back what the user actually agreed to

ReviewedMinimumReceivedRaw is the minimum the user saw on the review screen, in the destination token's base units. Given it, prepare refuses with SWAP_QUOTE_MOVED rather than preparing a worse route than the one that was reviewed. It is optional, and leaving it out means the account can sign a route nobody looked at.

A quote can run backwards

amountMode defaults to exactInput: amount is what the account spends. Set exactOutput and amount becomes the destination target instead, with fromAmount in the response telling you the input required. Quote-only — prepare always executes exact-input, against a concrete fromAmount you already hold.

Cross-chain finality is two separate events

ToNetworkId pointing at another chain still runs the operation on the SOURCE chain; the route bridges onward from there. So the operation reaching finalized means the source leg is done — not that funds have landed on the destination. How long the second leg takes belongs to the bridge on the route, and UGTP does not track it.

The walkthrough

Part 1 · Find out what can be swapped

Three reads that populate a token picker. All cheap, all cacheable, none of them signing anything.

Step 1 · The chains routing is available on

What this is for: Populate the network selector with chains a route can actually be found for.

This is the routing catalogue, which is not the same set as the networks UGTP operates on. Use it for the swap screen specifically.

curl "https://api.ugtp.io/v1/swap/chains" \
  -H "X-API-Key: $UGTP_API_KEY"

Then: Cache it. The set moves rarely, and a swap screen that refetches it on every open pays for nothing.

Step 2 · The tokens on a chain

What this is for: Populate the token picker for the chain the user chose.

The tradable catalogue for a chain. Deliberately larger than the fee-token list from /network/:id/tokens — a token can be swapped without being something the fee can be charged in.

curl "https://api.ugtp.io/v1/swap/tokens?networkId=42161" \
  -H "X-API-Key: $UGTP_API_KEY"

Then: Do not reuse this list to choose a fee token. The two answer different questions, and a fee token that is merely tradable is refused at prepare.

Step 3 · The same asset across chains

What this is for: Offer a sensible destination token when the user changes the destination chain.

Maps an asset to its counterpart on other chains, which is what lets a cross-chain screen preselect USDC on the destination when the user is spending USDC.

curl "https://api.ugtp.io/v1/swap/token-map?networkId=42161" \
  -H "X-API-Key: $UGTP_API_KEY"

Then: Optional — a same-chain-only screen never needs it. Use the native sentinel 0xEeee…EEeE when the destination is a chain's own coin.

Part 2 · Quote the swap

What the user reads before deciding. One call, and the figures in it drive the whole review screen.

Step 1 · Ask for a route

What this is for: Show a rate, a minimum, and a total cost before the user commits to anything.

Amount is base units of fromToken. slippage is a fraction — 0.005 is 0.5% — and LI.FI's default applies when you omit it. Leave toNetworkId out for a same-chain swap.

curl -X POST "https://api.ugtp.io/v1/swap/quote" \
  -H "X-API-Key: $UGTP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "ownerAddress": "0x696edf2490b92a5f5714ffbf69c1f6967cb2cd6d",
    "networkId": "42161",
    "fromToken": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831",
    "toToken": "0x912CE59144191C1204E64559FE8253a0e49E6548",
    "amount": "1000000",
    "slippage": 0.005
  }'

{
  "fromAmount": "1000000",        "toAmount": "990000",
  "fromAmountUsd": "1.00",        "toAmountUsd": "0.99",
  "rate": "0.99",
  "minimumReceived": "0.985050",  "minimumReceivedUsd": "0.985",
  "networkFeeUsd": "0.01",        "lifiFeesUsd": "0.002",
  "ugtpFeeUsd": "0.051",          "ugtpFeeStaticUsd": 0.05,   "ugtpFeePercent": 0.1,
  "feeTokenAmount": "0.061",      "feeTokenSymbol": "USDC",
  "feeReserveTokenAmount": "0.061",
  "slippage": 0.005,
  "estimatedTimeSeconds": 30,
  "bridge": "uniswap",            "provider": "uniswap"
}

Then: Show minimumReceived as the guarantee and toAmount as the expectation — they are different promises and users read the wrong one if you show only the second. Keep minimumReceived: converted to the destination token's base units it becomes reviewedMinimumReceivedRaw in Part 3. Reserve against feeReserveTokenAmount, not feeTokenAmount.

Part 3 · Prepare, sign and submit

The same three moves as an ordinary transfer, with one extra field that protects the user from a route they never saw.

Step 1 · Prepare the swap

What this is for: Get something signable at a rate the user already agreed to.

Pass reviewedMinimumReceivedRaw — the minimum from the quote the user read, in the destination token's base units. feeToken defaults to fromToken when omitted.

curl -X POST "https://api.ugtp.io/v1/swap/prepare" \
  -H "X-API-Key: $UGTP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "ownerAddress": "0x696edf2490b92a5f5714ffbf69c1f6967cb2cd6d",
    "networkId": "42161",
    "fromToken": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831",
    "toToken": "0x912CE59144191C1204E64559FE8253a0e49E6548",
    "amount": "1000000",
    "reviewedMinimumReceivedRaw": "985050"
  }'

{
  "operationId": "ugtp_operation_wdxo2cOMXBIQ",
  "userOperation": { "sender": "0x696edf24…", "callData": "0x…", "…": "the rest of the struct" },
  "userOperationHash": "0x…",
  "ugtpFee": { "tokenAmount": "83266", "feeToken": "0xaf88d065…" },
  "accountType": "eoa_7702",
  "authorizationRequired": false
}

Then: On 409 SWAP_QUOTE_MOVED, quote again and show the user the new figures — do not retry the same prepare, and do not drop reviewedMinimumReceivedRaw to make the refusal go away. Keep the reviewed minimum in the next request.

Step 2 · Sign and submit, by rail

What this is for: Authorize the operation in the shape this network uses.

Use the signing method for the returned rail. A UserOperation requires the raw-hash signer described in the transactions guide; forward_request uses typed data. For self_funded, sign the returned transaction and include the prepared swap operationId. Reject unknown rails before signing.

let submission;
if (prepared.rail === undefined) {
  // ownerSigner must support raw-hash signing; see Transactions end to end.
  if (!ownerSigner.sign) throw new Error("A raw-hash signer is required");
  const signature = await ownerSigner.sign({ hash: prepared.userOperationHash });
  const authorization = prepared.authorizationRequired
    ? await walletClient.signAuthorization({
        account: ownerAddress,
        contractAddress: DELEGATE_ADDRESS,
        nonce: prepared.authorizationNonce,
        chainId: Number(networkId),
      })
    : undefined;
  submission = { operationId: prepared.operationId, signature,
    ...(authorization ? { authorization } : {}) };
} else if (prepared.rail === "forward_request") {
  const signature = await walletClient.signTypedData({
    account: ownerAddress,
    ...prepared.signingPayload,
  });
  submission = { operationId: prepared.operationId, signature };
} else if (prepared.rail === "self_funded") {
  const signedTransaction = await walletClient.signTransaction({
    account: ownerAddress,
    chain: chains[Number(networkId)],
    to: prepared.transaction.to,
    data: prepared.transaction.data,
    value: BigInt(prepared.transaction.value),
    nonce: prepared.nonce,
    gas: BigInt(prepared.gasSuggestion.gasLimit),
    maxFeePerGas: BigInt(prepared.gasSuggestion.maxFeePerGas),
    maxPriorityFeePerGas: BigInt(prepared.gasSuggestion.maxPriorityFeePerGas),
  });
  submission = { operationId: prepared.operationId, signedTransaction };
} else {
  throw new Error("Unsupported rail: " + prepared.rail);
}
await post("/transaction/submit", submission);

Then: Then poll by operationId exactly as in the transactions guide — the loop is identical and the statuses are the same.

Part 4 · A swap on Avalanche

Same endpoint, executor rail, and two differences worth knowing before you build the confirmation screen. This is a real operation: 0.3 EURC into DAI.e on Avalanche C-Chain.

Step 1 · Prepare, and read the destination fields

What this is for: Get typed data to sign, and the figures the confirmation screen needs.

The fee token must be named on this rail. The response carries the destination asset and amount directly, which the UserOperation rail does not — so a confirmation screen here can state what arrives without asking anything further.

curl -X POST "https://api.ugtp.io/v1/swap/prepare" \
  -H "X-API-Key: $UGTP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "ownerAddress": "0x696edf2490b92a5f5714ffbf69c1f6967cb2cd6d",
    "networkId": "43114",
    "fromToken": "0xC891EB4cbdEFf6e073e859e987815Ed1505c2ACD",
    "toToken": "0xd586E7F844cEa2F87f50152665BCbc2C279D8d70",
    "amount": "300000",
    "feeToken": "0xC891EB4cbdEFf6e073e859e987815Ed1505c2ACD"
  }'

{
  "operationId": "ugtp_operation_F593RTPHmsEw",
  "rail": "forward_request",
  "requestHash": "0x169f5c887bfba4c7cd75d1c58ffe82dd1ee29f9f9bf675bdbcee4e4016cc6da2",
  "ugtpFee": { "tokenAmount": "44568", "expectedTokenAmount": "44087",
               "feeToken": "0xC891EB4c…" },
  "destinationAssetSymbol": "DAI.e",
  "destinationAssetDecimals": 18,
  "destinationAmountRaw": "344742682283956914"
}

Then: The input token needs sufficient executor allowance — the same setup as Part 4 of the transactions guide, refused the same way with EXECUTOR_ALLOWANCE_MISSING. There is no authorization on this network, ever.

Part 5 · A swap that ends on another chain

One extra field turns the same call into a cross-chain swap. The real operation below spends 0.25 USDC on Arbitrum One and delivers native ETH on Ethereum.

Step 1 · Prepare with a destination chain

What this is for: Spend on one chain and deliver on another, in one signed operation.

Add toNetworkId. Everything else is a same-chain swap: the operation runs on networkId, and the route bridges onward. The native sentinel as toToken means the destination chain's own coin.

curl -X POST "https://api.ugtp.io/v1/swap/prepare" \
  -H "X-API-Key: $UGTP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "ownerAddress": "0x696edf2490b92a5f5714ffbf69c1f6967cb2cd6d",
    "networkId": "42161",
    "toNetworkId": "1",
    "fromToken": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831",
    "toToken": "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
    "amount": "250000",
    "reviewedMinimumReceivedRaw": "67734528796192"
  }'

{
  "operationId": "ugtp_operation_wdxo2cOMXBIQ",
  "userOperation": { "sender": "0x696edf24…", "…": "the rest of the struct" },
  "userOperationHash": "0x…",
  "ugtpFee": { "tokenAmount": "83266", "feeToken": "0xaf88d065…" },
  "accountType": "eoa_7702",
  "authorizationRequired": false
}

Then: Sign, submit and poll exactly as for a same-chain swap. Then read the next step before you write the success screen.

Step 2 · Do not call it delivered when the operation finalizes

What this is for: Avoid telling a user their funds arrived on a chain they have not arrived on.

Finalized on this operation means the SOURCE leg is done — the funds left, and the bridge has them. Arrival on the destination is a second event, on a different chain, on the bridge's own schedule, and UGTP does not track it.

const settled = await track(prepared.operationId);

if (settled.status === "finalized") {
  // TRUE: the source operation is done and the bridge has the funds.
  // NOT TRUE: the recipient holds anything on the destination chain yet.
  show("Sent — arriving on Ethereum shortly");
  // The quote's estimatedTimeSeconds is the route's own estimate for the
  // second leg. Treat it as an estimate: it is the bridge's promise, not ours.
}

Then: If you need destination confirmation, watch the recipient's balance on the destination chain yourself, or show the transaction on a bridge explorer. There is no UGTP endpoint that answers it, and a success screen that claims otherwise is the most common cross-chain support ticket.

Before you go live

  • The review screen shows minimumReceived as the guarantee, not only toAmount.
  • reviewedMinimumReceivedRaw is sent on every prepare, and a 409 SWAP_QUOTE_MOVED re-quotes instead of retrying.
  • The fee token comes from /network/:networkId/tokens, never from the swap catalogue.
  • Your client branches on the rail in the prepare response, not on a network table of its own.
  • On Avalanche the input token's executor allowance is handled, and no authorization is ever attached.
  • A cross-chain success screen says the source leg settled — it does not claim delivery on the destination chain.
  • Operations are polled by operationId to a terminal status, with the same loop every other flow uses.
Transactions, end to end →The swap endpoint reference →