Swagger

Swaps

Exchange one token for another as a single gasless operation, on LI.FI's routes, on one chain or ending on another.

Whose route it is

Every route here comes from LI.FI. UGTP asks it for a route across the exchanges and bridges it aggregates, then wraps that route's own calldata into a single gasless operation on whichever rail the account's network uses — so a swap is prepared, signed, submitted and polled exactly like a transfer.

Where the boundary falls decides where you handle a problem. Whether a corridor is routable at all, what a route costs, and how long a bridged leg takes are the routing provider's. The fee, the signing payload, the operation id, and every status you poll are ours. Its full profile — inputs, outputs, trust boundary and failure modes — is on the Services page.

The route is LI.FI's, the operation is UGTP's

LI.FI does the route discovery and returns the execution calldata for the route it offers. UGTP checks that calldata back against the request — a route whose chain, token, recipient or amount drifted from what was asked is rejected rather than handed to an account to sign — then prices the fee, has the account sign, relays it, and tracks it to a terminal status.

A route is chosen by price, not by speed

A swap takes the cheapest of the routes the provider offers. Slippage is yours to set per request (0.005 = 0.5%, the provider's own default when omitted); nothing else about route selection is configurable through this API.

The swap chain and token lists are the provider's catalog

GET /swap/chains and GET /swap/tokens are LI.FI's own catalog, filtered to the networks this deployment serves, and each token carries LI.FI's coinKey — which is what GET /swap/token-map pairs a cross-chain corridor on. Neither is the same list as GET /network/:networkId/tokens: that one is the far shorter set of tokens an operation can pay its FEE in.

A quote is a preview, and prepare routes again

POST /swap/quote creates no operation. POST /swap/prepare fetches its OWN fresh route, because by then the one the user reviewed is minutes old — so pass reviewedMinimumReceivedRaw, the minimum the user accepted. Given it, prepare refuses with SWAP_QUOTE_MOVED rather than signing a route nobody reviewed.

Cross-chain is the same call with a destination

Give toNetworkId a different chain and the operation still runs on the source chain; the route bridges to the destination from there. So the source operation reaching a terminal status is not the same event as funds arriving on the destination chain, and how long that second leg takes belongs to the bridge on the route.

A route can fail before anything is signed

The provider having no route for a corridor and a route that exists but reverts when its calldata is simulated are different answers. The second comes back coded: SWAP_AMOUNT_TOO_SMALL when a bridge or DEX on the route rejects the amount as below its minimum, or EXECUTION_SIMULATION_REVERTED with the raw reason. Neither is fixed by retrying the identical request.

Read the LI.FI service profile →

Each of these ran. The request, the response, and the object that went on chain are the operation's own, read back out of the platform's records — and the transaction link opens the result in a block explorer, so every number here can be checked against something outside this page.

A same-chain swap on Avalanche

Executor rail (EIP-712 execution request) · Avalanche C-Chain

0.3 EURC became 0.344742682283956914 DAI.e in one gasless operation, on a route LI.FI took through the OKX DEX aggregator. The account signed typed data rather than a UserOperation hash, because this network has no EIP-7702.

Operation id
ugtp_operation_F593RTPHmsEw
Charged
0.043757 EURC: 0.000267 gas + 0.04319 static + 0.0003 transfer percentage — the static part is smaller in token units here because EURC was priced at $1.15771144. The route also carried a 20 bps LI.FI integrator fee of 0.0006 EURC.

The ExecutionRequest the executor received

One action of kind 1 — a swap — whose 2944 ABI-encoded bytes hold the input token, the output token, the amount, the minimum out, and the router calldata LI.FI returned. This one ran through the executor that preceded the published one, which is why the verifyingContract here is not the address on the contracts page.

{
  "account": "0x696edf2490b92a5f5714ffbf69c1f6967cb2cd6d",
  "nonce": "0x0",
  "actions": [
    {
      "kind": 1,
      "params": "0x0000000000000000000000000000000000000000…2944 bytes"
    }
  ],
  "terms": {
    "feeToken": "0xC891EB4cbdEFf6e073e859e987815Ed1505c2ACD",
    "transferredValueInFeeToken": "300000",
    "platformTransferFeeBps": 10,
    "projectTransferFeeBps": 0,
    "platformStaticFeeUsd8dp": "5000000",
    "projectStaticFeeUsd8dp": "0",
    "projectPayoutAddress": "0x0000000000000000000000000000000000000000",
    "calldataGas": "27304",
    "maxFeeTokenAmount": "44568",
    "validUntil": 1788166341
  },
  "backendSignature": "0x33203ef5633e3a6691ea3dff38a13424c6073ed7…65 bytes",
  "executor": "0x…the executor deployed before 0xA7C8776C…E767",
  "requestHash": "0x169f5c887bfba4c7cd75d1c58ffe82dd1ee29f9f9bf675bdbcee4e4016cc6da2",
  "transaction": "0xa630f06fe8ebe731a4dbfb723f69c97e2eec29bff4d6d0157fe3f0a47db35ced"
}

POST /swap/prepare (executor rail)
{
  "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": "0xC891EB4cbdEFf6e073e859e987815Ed1505c2ACD"
  },
  "destinationAssetSymbol": "DAI.e",
  "destinationAssetDecimals": 18,
  "destinationAmountRaw": "344742682283956914"
}

Open the transaction on the block explorer ↗

A swap that ends on another chain

UserOperation rail (EIP-7702 account) · Arbitrum One

0.25 USDC on Arbitrum became 0.000068074903312756 ETH on Ethereum, routed by LI.FI over the Relay bridge. The transaction below is the source-chain operation; delivery on Ethereum is the bridge's own leg, and this one reported crossChainStatus "completed".

Operation id
ugtp_operation_wdxo2cOMXBIQ
Charged
0.072738 USDC: 0.02248 gas + 0.050008 static + 0.00025 transfer percentage. On top of it the route carried a 20 bps LI.FI integrator fee of 0.0005 USDC, recorded separately because it is not ours to keep.

The UserOperation the EntryPoint executed

One operation, two calls inside its 1924-byte callData: approve the LI.FI router for the input, then the router call that hands the amount to the bridge. Nothing about the destination chain appears in this object — it lives inside the routed calldata.

{
  "sender": "0x696edf2490b92a5f5714ffbf69c1f6967cb2cd6d",
  "nonce": "0x20",
  "initCode": "0x",
  "callData": "0xb61d27f6…1924 bytes: approve the router, then the bridge call",
  "accountGasLimits": "0x0000000000000000000000000000873e00000000000000000000000000043258",
  "preVerificationGas": "0xeeb8",
  "gasFees": "0x0000000000000000000000000000000000000000000000000000000001706880",
  "paymasterAndData": "0x49f26881b5ea4d3f3d451c7faa4bca1b4aebcd97…214 bytes",
  "signature": "0x…65 bytes",
  "userOperationHash": "0x9f165e8e0f3957722ad6520328ce00333c29f7d26ca50401bf7a5da8540c08c8",
  "entryPoint": "0x433709009B8330FDa32311DF1C2AFA402eD8D009",
  "handleOpsTransaction": "0x1125e6fa15ea46293efa3bf06e98958b16b140d4c3f2556f13e4c1b319920a4c"
}

POST /swap/prepare
{
  "ownerAddress": "0x696edf2490b92a5f5714ffbf69c1f6967cb2cd6d",
  "networkId": "42161",
  "toNetworkId": "1",
  "fromToken": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831",
  "toToken": "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
  "amount": "250000",
  "reviewedMinimumReceivedRaw": "67734528796192"
}

{
  "operationId": "ugtp_operation_wdxo2cOMXBIQ",
  "userOperation": {
    "sender": "0x696edf2490b92a5f5714ffbf69c1f6967cb2cd6d",
    "nonce": "0x20",
    "initCode": "0x",
    "callData": "0xb61d27f6…1924 bytes: approve the router, then the bridge call",
    "accountGasLimits": "0x0000000000000000000000000000873e00000000000000000000000000043258",
    "preVerificationGas": "0xeeb8",
    "gasFees": "0x0000000000000000000000000000000000000000000000000000000001706880",
    "paymasterAndData": "0x49f26881b5ea4d3f3d451c7faa4bca1b4aebcd97…214 bytes",
    "signature": "0x"
  },
  "userOperationHash": "0x9f165e8e0f3957722ad6520328ce00333c29f7d26ca50401bf7a5da8540c08c8",
  "ugtpFee": {
    "tokenAmount": "83266",
    "feeToken": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831"
  },
  "accountType": "eoa_7702",
  "authorizationRequired": false
}

GET /transaction/:id
{
  "operationId": "ugtp_operation_wdxo2cOMXBIQ",
  "status": "finalized",
  "transactionHash": "0x1125e6fa15ea46293efa3bf06e98958b16b140d4c3f2556f13e4c1b319920a4c",
  "userOperationHash": "0x9f165e8e0f3957722ad6520328ce00333c29f7d26ca50401bf7a5da8540c08c8",
  "networkId": "42161",
  "replacementCount": 0,
  "reason": null
}

Open the transaction on the block explorer ↗

Worked example

A swap, end to end

Quote the route, prepare the one the user reviewed, then submit and poll like any other operation.

  1. 01

    Quote the swap

    POST /swap/quote

    Exact-input by default: amount is the token being spent. Set amountMode=exactOutput to ask what input a target output needs instead. The response carries the rate, the minimum received and the fees.

    Input — the request

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

    Output — 200 response

    {
      "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"
    }

    Every status this call can answer with
    StatusWhen
    200Quote
    400Invalid request body or query parameters
    401Missing or invalid credentials
    422The recommended route cannot execute — the quote-time simulation of the real approve+swap calldata reverted deterministically (e.g.
    429Rate limit exceeded — retry after the number of seconds in the `Retry-After` response header
    500An unclassified failure, carrying code=QUOTE_FAILED.
  2. 02

    Prepare the route

    POST /swap/prepare

    Prepare fetches its OWN fresh route, because the quote the user saw is minutes old by then. Pass reviewedMinimumReceivedRaw so it refuses with SWAP_QUOTE_MOVED rather than signing a route nobody reviewed.

    Input — the request

    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"
    }'

    Output — 201 response

    {
      "operationId": "ugtp_operation_wdxo2cOMXBIQ",
      "userOperation": {
        "sender": "0x696edf2490b92a5f5714ffbf69c1f6967cb2cd6d",
        "nonce": "0x20",
        "initCode": "0x",
        "callData": "0xb61d27f6…1924 bytes: approve the router, then the bridge call",
        "accountGasLimits": "0x0000000000000000000000000000873e00000000000000000000000000043258",
        "preVerificationGas": "0xeeb8",
        "gasFees": "0x0000000000000000000000000000000000000000000000000000000001706880",
        "paymasterAndData": "0x49f26881b5ea4d3f3d451c7faa4bca1b4aebcd97…214 bytes",
        "signature": "0x"
      },
      "userOperationHash": "0x9f165e8e0f3957722ad6520328ce00333c29f7d26ca50401bf7a5da8540c08c8",
      "ugtpFee": {
        "tokenAmount": "83266",
        "feeToken": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831"
      },
      "accountType": "eoa_7702",
      "authorizationRequired": false
    }

    Every status this call can answer with
    StatusWhen
    201Prepared operation.
    400Invalid request body, the referenced account belongs to a different network than the request targets, or (7702-mode projects only) EIP-7702 is not…
    401Missing or invalid credentials
    404The referenced accountId does not exist.
    409Either a duplicate user operation (same UserOp hash already materialized), or code=SWAP_QUOTE_MOVED — `reviewedMinimumReceivedRaw` was supplied and…
    422Unprocessable — either blocked by an unsafe security verdict (returns the prepared operation; enforcement is monitor-only by default), or the…
    429Rate limit exceeded — retry after the number of seconds in the `Retry-After` response header
    500Internal server error
    503Self-funded native rail only, and always retryable — nothing is known to be wrong: code=SELF_FUNDED_ESTIMATE_UNAVAILABLE (the transfer could not be…
  3. 03

    Submit and poll

    POST /transaction/submit

    The signed payload goes to the same endpoint as every other rail, and the same operationId polls it to a terminal status.

    Input — the request

    curl -X POST "https://api.ugtp.io/v1/transaction/submit" \
      -H "X-API-Key: $UGTP_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
      "operationId": "ugtp_operation_E6SnXDoAl9AQ",
      "signature": "0xa3087507768f541a4eaa9d46b581c351d29c3137…65 bytes"
    }'

    Output — 200 response

    {
      "operationId": "ugtp_operation_E6SnXDoAl9AQ",
      "status": "queued",
      "userOperationHash": "0x1a4fece09e742c04398006ebdae6d03227aa7e822ba825e0c64b44fc78d12cb3",
      "networkId": "42161"
    }

    Every status this call can answer with
    StatusWhen
    200Submission result
    400Invalid body, or — on the self-funded native rail — bytes this rail cannot carry: code=SIGNED_TRANSACTION_UNSUPPORTED_TYPE (not EIP-1559),…
    401Missing or invalid credentials
    404Operation not found or expired — prepare it again
    409This user operation was already submitted under a different operation id; or the operation was already submitted (status mismatch); or, on the…
    429Rate limit exceeded — retry after the number of seconds in the `Retry-After` response header
    500Internal server error
    502code=SIGNED_TRANSACTION_IDENTITY_MISMATCH — the engine derived a different transaction hash from the same bytes, so nothing was recorded and the…
    503Self-funded native rail only, retryable: code=SELF_FUNDED_CHAIN_UNAVAILABLE (the chain could not be checked for whether these bytes already landed —…