# LayerHop Docs > Developer docs for LayerHop: one API and SDK for best-price cross-chain and same-chain swaps across Jupiter, Relay, deBridge, 0x and LI.FI. Non-custodial. This file concatenates every page of https://docs.layerhop.com as Markdown. --- # Introduction > LayerHop is one API and SDK for best-price cross-chain and same-chain swaps. It compares Jupiter, Relay, deBridge, 0x and LI.FI on every trade and never holds your funds. Source: https://docs.layerhop.com/ ## What LayerHop does LayerHop is a **meta-aggregator** for token swaps and bridging. You ask for a quote once. LayerHop asks every liquidity provider that can serve the route, in parallel, and ranks the answers by what the user actually receives. You then get a ready-to-sign transaction for the winning route. The user's own wallet signs it and sends it straight to the provider's audited contracts. - **One integration, five providers.** [Jupiter](/providers#jupiter), [Relay](/providers#relay), [deBridge](/providers#debridge), [0x](/providers#0x) and [LI.FI](/providers#lifi) sit behind a single API. - **EVM and Solana.** Ethereum, Arbitrum, Base, Optimism, BNB Chain, Polygon and Solana, in any direction a provider supports. See [Supported chains & tokens](/chains). - **Best for the user, not for us.** Quotes are ranked by [time-adjusted output](/concepts/providers-and-ranking). What a route pays LayerHop never affects the ranking. - **Non-custodial.** LayerHop never holds funds or keys. See [Non-custodial model](/concepts/non-custodial). - **Transparent fees.** LayerHop's fee is reported separately on every quote as `fees.ourFeeBps`. See [Fees & channels](/concepts/fees-and-channels). ## How a swap works 1. **Quote** — `GET /v1/quote` with the source and destination chain, token, amount and addresses. You get `best`, all other `quotes`, and any per-provider `errors`. 2. **Build** — `POST /v1/orders` with the `quoteId`. You get a transaction: an `EvmTx` (maybe with an [approval](/concepts/approvals)) or a base64 Solana `SvmTx`. 3. **Sign** — the user's wallet signs. EVM transactions and most Solana transactions are broadcast by the wallet. Solana transactions with `submitViaApi: true` go back through [`POST /v1/orders/submit`](/concepts/submit-via-api). 4. **Track** — poll `GET /v1/orders/{provider}/{id}` until the order is `filled`, `refunded` or `failed`. See [Order tracking](/concepts/order-tracking). ```ts import { LayerHop } from "@layerhop/sdk"; const lh = new LayerHop({ baseUrl: "http://localhost:8787" }); const { best } = await lh.quote({ srcChain: "base", srcToken: "USDC", amount: "100000000", // 100 USDC (6 decimals) dstChain: "solana", dstToken: "SOL", sender: "0xYourEvmAddress", recipient: "YourSolanaAddress", }); const tx = await lh.buildTx(best.quoteId); // sign `tx` with the user's wallet, then: for await (const s of lh.watch(best.provider, srcTxHash)) console.log(s.state); ``` ## Where to start - New here? Follow the [Quickstart](/quickstart). - Building a backend or a non-TypeScript client? Go to the [API reference](/api-reference). - Want a swap UI without writing one? See [Embed the widget](/guides/embed-widget) and [Deep links](/guides/deep-links). - Using an AI assistant? Point it at [`/llms.txt`](/llms.txt) or [`/llms-full.txt`](/llms-full.txt), or use **Copy page as Markdown** on any page. ## Status LayerHop is in its first phase: a zero-capital meta-aggregator. What exists today is the REST API (`services/api`), the TypeScript SDK (`@layerhop/sdk`), the swap web app and these docs. > **Coming soon:** a hosted API at `api.layerhop.com` and the SDK on npm, API keys and rate limits, webhooks, `POST /v1/simulate`, order history, React hooks (`@layerhop/react`), Python/Go/Rust SDKs, a CLI with a local devnet, an MCP server, testnet support, and LayerHop's own intent protocol and solver network. Until the hosted API is live, run it locally — the [Quickstart](/quickstart) shows how. --- # Quickstart > Quote, build, sign and track your first LayerHop swap with the TypeScript SDK, viem and a Solana wallet — in under 10 minutes. Source: https://docs.layerhop.com/quickstart You will run the API, get a quote, build the transaction, sign it with the user's wallet and watch the order until it lands. ## 1. Run the API The hosted API (`https://api.layerhop.com`) is not live yet, so run it from the monorepo. You need Node 20+ and pnpm. ```bash git clone layerhop && cd layerhop pnpm install cp .env.example .env pnpm --filter @layerhop/api dev # LayerHop API on http://localhost:8787/v1 — providers: jupiter, relay, debridge, zerox, lifi ``` Check it: ```bash curl http://localhost:8787/v1/health # {"ok":true,"providers":["jupiter","relay","debridge","zerox","lifi"]} ``` > [!TIP] > Start the API with `LAYERHOP_ENABLE_MOCK=1` to add a deterministic `mock` provider that always quotes and always reports `filled`. It is handy for UI work and tests. **Never broadcast a mock transaction** — it is a dummy transaction to `0x…dEaD`. > [!NOTE] > 0x quotes need `ZEROX_API_KEY` in `.env`; without it the 0x provider reports `UNAUTHORIZED` in `errors` and the others still answer. The other providers work without keys. ## 2. Install the SDK `@layerhop/sdk` has no dependencies besides `@layerhop/shared` and works in Node, browsers and edge runtimes. Inside the monorepo, add it as a workspace dependency: ```json { "dependencies": { "@layerhop/sdk": "workspace:*" } } ``` > **Coming soon:** `npm install @layerhop/sdk` from the public npm registry. Create a client: ```ts import { LayerHop } from "@layerhop/sdk"; export const lh = new LayerHop({ baseUrl: "http://localhost:8787", // default: https://api.layerhop.com channel: "api", // fee channel, see Concepts → Fees & channels }); ``` ## 3. Get a quote Amounts are **integer strings in base units**. 100 USDC (6 decimals) is `"100000000"`. ```ts const { best, quotes, errors } = await lh.quote({ srcChain: "base", srcToken: "USDC", amount: "100000000", dstChain: "solana", dstToken: "SOL", sender: "0x1111111111111111111111111111111111111111", // pays on Base recipient: "9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM", // receives on Solana slippageBps: 50, }); console.log(best.provider, best.dstAmount, best.dstAmountMin, best.estimatedSeconds); console.log(`LayerHop fee: ${best.fees.ourFeeBps} bps`); ``` `best` is `quotes[0]`. `errors` lists providers that could not quote; they do not fail the request. Each quote expires at `expiresAt` (unix seconds), usually 30–60 s after it was made. ## 4. Build the transaction ```ts const tx = await lh.buildTx(best.quoteId); ``` `tx` is a `BuiltTx`: either `{ vm: "evm", chainId, to, data, value, approval? }` or `{ vm: "svm", serializedTx, submitViaApi? }`. Build it right after the user confirms — some providers expect the transaction on chain within about 30 seconds. ## 5a. Sign on EVM with viem If `tx.approval` is present, the user must first approve the spender for the input token. Then send the main transaction. ```ts import { createWalletClient, createPublicClient, custom, http, erc20Abi } from "viem"; import { base } from "viem/chains"; const wallet = createWalletClient({ chain: base, transport: custom(window.ethereum!) }); const pub = createPublicClient({ chain: base, transport: http() }); const [account] = await wallet.requestAddresses(); if (tx.vm === "evm") { if (tx.approval) { const { token, spender, amount } = tx.approval; const allowance = await pub.readContract({ address: token as `0x${string}`, abi: erc20Abi, functionName: "allowance", args: [account!, spender as `0x${string}`], }); if (allowance < BigInt(amount)) { const approveHash = await wallet.writeContract({ account: account!, address: token as `0x${string}`, abi: erc20Abi, functionName: "approve", args: [spender as `0x${string}`, BigInt(amount)], }); await pub.waitForTransactionReceipt({ hash: approveHash }); } } const srcTxHash = await wallet.sendTransaction({ account: account!, to: tx.to as `0x${string}`, data: tx.data as `0x${string}`, value: BigInt(tx.value), }); } ``` The wallet must be on `tx.chainId`; switch chains first if needed (`wallet.switchChain({ id: tx.chainId })`). ## 5b. Sign on Solana `serializedTx` is a base64 `VersionedTransaction`. Sign it with any wallet adapter. If `submitViaApi` is true, send the signed bytes back through LayerHop; otherwise broadcast them yourself. ```ts import { Connection, VersionedTransaction } from "@solana/web3.js"; const connection = new Connection("https://api.mainnet-beta.solana.com"); if (tx.vm === "svm") { const unsigned = VersionedTransaction.deserialize(Buffer.from(tx.serializedTx, "base64")); const signed = await wallet.signTransaction(unsigned); // @solana/wallet-adapter-react `useWallet()` const bytes = signed.serialize(); const signature = tx.submitViaApi ? await lh.submit(best.quoteId, Buffer.from(bytes).toString("base64")) : await connection.sendRawTransaction(bytes, { skipPreflight: false, maxRetries: 3 }); } ``` In the browser, use a base64 helper instead of `Buffer` if your bundler does not polyfill it. See [The submitViaApi flow](/concepts/submit-via-api) for why some transactions must go through the API. ## 6. Watch the order `watch()` polls `GET /v1/orders/{provider}/{id}` every 3 s and yields each state change until the order is final. ```ts for await (const status of lh.watch(best.provider, srcTxHash)) { console.log(status.state, status.dstTxHash ?? ""); // pending → filled (or refunded / failed) } ``` For cross-chain providers (`relay`, `debridge`, `lifi`) pass the **source transaction hash**. Same-chain providers (`jupiter`, `zerox`) always report `unknown`; confirm the transaction on chain instead. See [Order tracking](/concepts/order-tracking). ## Next steps - [EVM → Solana guide](/guides/evm-to-solana) — the full flow with error handling. - [Error handling & retries](/guides/error-handling) — what to do with `QUOTE_EXPIRED`, `NO_ROUTE` and friends. - [SDK reference](/sdk) and [API reference](/api-reference). --- # Meta-aggregation > How one LayerHop quote fans out to every eligible provider in parallel, and what you get back. Source: https://docs.layerhop.com/concepts/meta-aggregation ## One request, many providers LayerHop does not own liquidity. Each provider — Jupiter, Relay, deBridge, 0x, LI.FI — is a full routing engine with its own liquidity, bridges or solver network. LayerHop sits one level above them: it is an aggregator of aggregators. When you call `GET /v1/quote`, the API: 1. **Resolves** the chains (id or slug) and tokens (symbol from the [token list](/chains), or a raw address / mint). 2. **Filters** providers with `supports(request)`. For example, Jupiter only handles Solana → Solana; 0x only handles same-chain EVM swaps. See [Providers](/providers) for the full matrix. 3. **Asks every eligible provider in parallel**, each with its own timeout (8 s by default). 4. **Keeps the successes** and **ranks** them best-for-the-user first (see [Providers & ranking](/concepts/providers-and-ranking)). 5. **Stores** each quote under its own `quoteId` and returns them. ```json { "best": { "quoteId": "q_…", "provider": "relay", "dstAmount": "612345678", "...": "..." }, "quotes": [ { "provider": "relay", "...": "..." }, { "provider": "debridge", "...": "..." } ], "errors": [ { "provider": "lifi", "code": "PROVIDER_TIMEOUT", "message": "Timed out: li.quest" } ] } ``` ## Partial failure is normal A provider that errors or times out does not fail your request; it shows up in `errors` with a stable [error code](/errors). Only when **no** provider returns a quote does the API answer `404 NO_ROUTE`. This means you can show the user a price even while a provider is degraded, and you can show *why* a provider is missing (for example `UNSUPPORTED_TOKEN` from one provider while another still routes the token). ## Every quote is executable `quotes` is not just a price comparison. Every entry has its own `quoteId`, so you can let the user pick a slower-but-cheaper or faster route and build that one with `POST /v1/orders`. ## The RouteProvider interface Internally, every provider implements the same interface (`packages/shared/src/types.ts`): | Method | Purpose | |---|---| | `supports(req)` | Can this provider serve the chain/token pair at all? | | `quote(req, ctx)` | Price the trade with LayerHop's fee for the channel. | | `buildTx(quote, ctx)` | Return the transaction to sign (or the one already returned with the quote). | | `status(id, ctx)` | Report `pending` / `filled` / `refunded` / `failed` / `unknown`. | | `submit?(quote, signedTx, ctx)` | Only for [submitViaApi](/concepts/submit-via-api) transactions. | New liquidity sources — including LayerHop's own solver later — plug in as just another provider. Nothing changes for API clients. ## Quote storage Quotes are held in memory by the API instance that produced them, and dropped five minutes after they expire. A `quoteId` is therefore only valid on the same API instance and only for a short time: build the order promptly and treat [`QUOTE_NOT_FOUND`](/errors/QUOTE_NOT_FOUND) and [`QUOTE_EXPIRED`](/errors/QUOTE_EXPIRED) as "get a fresh quote". > **Coming soon:** a shared quote store (Redis) so a hosted, multi-instance API can build any quote it issued. --- # Providers & ranking > How LayerHop orders quotes — by time-adjusted output for the user, never by what a route pays LayerHop. Source: https://docs.layerhop.com/concepts/providers-and-ranking ## The rule Quotes are ranked **best for the user first**. LayerHop compares what the user receives — `dstAmount`, which is already net of the provider's fees and LayerHop's own fee — and **never** looks at how much a route pays LayerHop. The ranking code lives in `packages/shared/src/rank.ts`. ## ETA-adjusted output Raw output alone would let a route that is 0.08% better but takes 18 minutes beat a route that lands in 2 seconds. So LayerHop compares *effective output*: - Every quote has `estimatedSeconds`. - The first minute is free. Each started minute beyond it costs **1 basis point** of output. - The penalty is capped at **50 bps**. ```text slowMinutes = max(0, ceil((estimatedSeconds - 60) / 60)) penaltyBps = min(50, slowMinutes × 1) effectiveOut = dstAmount × (10000 − penaltyBps) / 10000 ``` Quotes are sorted by `effectiveOut` (highest first); ties go to the faster route. | Route | dstAmount | ETA | Penalty | Effective output | |---|---|---|---|---| | A | 1 000 000 | 5 s | 0 bps | 1 000 000 | | B | 1 000 800 | 18 min | 17 bps | 999 098 | Route A wins: B's extra 0.08% does not pay for 18 minutes of waiting. > [!NOTE] > The penalty only affects **ordering**. The `dstAmount` and `dstAmountMin` you get back are the provider's real numbers. ## What the ranking does not do - It does not hide quotes. Everything that succeeded is in `quotes`, so your UI can offer the alternatives. - It does not prefer providers that share fees with LayerHop. If a provider cannot carry LayerHop's fee (for example Jupiter below 50 bps), that quote simply reports `fees.ourFeeBps: 0` and is ranked on output like any other. - It does not take gas paid separately in the native coin into account beyond what providers fold into `dstAmount`. Where providers report it, `fees.gasUsd` and `fees.providerFeeUsd` let you show it. ## Which providers are asked Only providers whose `supports()` accepts the route are queried. The current matrix: | Route | Providers asked | |---|---| | Solana → Solana | Jupiter, LI.FI | | EVM → same EVM chain | 0x, Relay, LI.FI | | EVM → other EVM chain | Relay, deBridge, LI.FI | | EVM → Solana | Relay, deBridge, LI.FI | | Solana → EVM | deBridge, LI.FI | Details, limits and fee mechanics per provider: [Providers](/providers). --- # Fees & channels > LayerHop's fee per channel (web, widget, api, bot), how it is applied through each provider, and how it is reported. Source: https://docs.layerhop.com/concepts/fees-and-channels ## The fee is on top, and shown separately LayerHop earns a fee on each trade, charged through the provider's own integrator-fee mechanism. It is always reported on the quote as `fees.ourFeeBps` — basis points of the **input** amount — separately from provider fees (`fees.providerFeeUsd`) and gas (`fees.gasUsd`). `dstAmount` already accounts for all of them. ## Channels The fee depends on where the trade comes from. Pass `channel` to `GET /v1/quote` (the SDK sends its `channel` option, default `api`). | Channel | Default fee | Used by | |---|---|---| | `api` | 20 bps (0.20%) | Direct API and SDK integrations | | `web` | 30 bps (0.30%) | The LayerHop web app | | `widget` | 30 bps (0.30%) | The embeddable widget (`/embed`) | | `bot` | 90 bps (0.90%) | The LayerHop Telegram bot | The defaults come from `DEFAULT_FEE_BPS` in `packages/shared/src/fees.ts`. Operators can override each with `FEE_BPS_API`, `FEE_BPS_WEB`, `FEE_BPS_WIDGET` and `FEE_BPS_BOT`. ## When `ourFeeBps` is lower than the channel fee The fee actually applied can be lower — often 0 — because each provider has its own rules: | Provider | Mechanism | Limits | |---|---|---| | Jupiter | Referral fee via a Jupiter referral account | 50–255 bps. Below 50 bps no fee is charged (so `api` at 20 bps earns 0 on Solana same-chain). Jupiter keeps 20%. | | Relay | `appFees` to an EVM address | Accrues as USDC, claimable on Base. | | deBridge | Affiliate fee on the source chain | Up to 1000 bps. Paid automatically on EVM; withdrawn manually on Solana. | | 0x | `swapFeeBps` taken in the sell token | Up to 1000 bps. | | LI.FI | Integrator fee | Only once the integrator is registered with LI.FI; otherwise 0. LI.FI also charges its own fee. | A fee also needs a configured recipient (`FEE_RECIPIENT_EVM`, `FEE_RECIPIENT_SVM`, `JUPITER_REFERRAL_ACCOUNT`). If the operator has not set one, quotes report `ourFeeBps: 0`. Trust the number on the quote; it is what the user pays. ## Integrator fees > **Coming soon:** your own integrator fee on top of LayerHop's (affiliate address + bps) and an earnings dashboard. Today the `integrator` parameter on the embed is attribution only. ## Fee math Amounts are integers in base units, so fees are computed with integer math and rounded down: ```ts import { applyBps, minAfterSlippage } from "@layerhop/shared"; applyBps("100000000", 20); // "200000" → 0.2 USDC of a 100 USDC input minAfterSlippage("612345678", 50); // "609283949" ``` --- # Approvals > When an EVM transaction comes with an ERC-20 approval, what it means and how to send it safely. Source: https://docs.layerhop.com/concepts/approvals ## When you get one Selling an ERC-20 token (USDC, USDT…) on an EVM chain requires the provider's contract to pull the tokens from the user's wallet. That needs an ERC-20 allowance. When it does, `POST /v1/orders` returns the transaction with an `approval`: ```json { "vm": "evm", "chainId": 8453, "to": "0xa5F5…9934", "data": "0x58109c…", "value": "0", "approval": { "token": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", "spender": "0xa5F5…9934", "amount": "100000000" } } ``` Selling the native coin (ETH, BNB, POL) never needs an approval; the amount travels in `value` instead. Solana transactions never carry an approval. ## How to handle it 1. Read the current allowance: `allowance(owner = user, spender)` on `approval.token`. 2. If it is **less than** `approval.amount`, send `approve(spender, amount)` from the user's wallet and **wait for it to be mined**. 3. Then send the main transaction (`to`, `data`, `value`). ```ts if (tx.vm === "evm" && tx.approval) { const { token, spender, amount } = tx.approval; const current = await pub.readContract({ address: token, abi: erc20Abi, functionName: "allowance", args: [user, spender] }); if (current < BigInt(amount)) { const hash = await wallet.writeContract({ address: token, abi: erc20Abi, functionName: "approve", args: [spender, BigInt(amount)] }); await pub.waitForTransactionReceipt({ hash }); } } ``` ## Good practice - **Approve the exact amount.** LayerHop returns the exact input amount; avoid unlimited approvals unless the user opts in. - **Spender is not always `to`.** For 0x the spender is the AllowanceHolder contract, while `to` is the settlement contract. Always use `approval.spender`. - **USDT on Ethereum** rejects changing a non-zero allowance to another non-zero value. If the current allowance is non-zero but too small, approve `0` first, then the new amount. - **The quote clock keeps running** while the approval confirms. If the approval took long, the main transaction may fail to build or execute; re-quote if `expiresAt` has passed. > **Coming soon:** Permit2 / EIP-2612 signatures to skip the separate approval transaction where providers support it. --- # The submitViaApi flow > Why some Solana transactions must be signed by the wallet but submitted through POST /v1/orders/submit, and how to do it. Source: https://docs.layerhop.com/concepts/submit-via-api ## What the flag means A Solana transaction (`vm: "svm"`) can come with `submitViaApi: true`. Then the wallet must **only sign** it. Do not broadcast it to an RPC node; send the signed bytes to `POST /v1/orders/submit` and LayerHop forwards them to the provider. ```json { "vm": "svm", "serializedTx": "AQAAAA…", "submitViaApi": true } ``` ## Why Today this applies to **Jupiter**. Jupiter's best routes are often RFQ quotes (a market maker co-signs the transaction) or gasless swaps (Jupiter pays the fee and co-signs). Those transactions only become valid when the provider adds its signature, so a plain RPC broadcast will never land them. Jupiter executes every order it builds through its own endpoint, so LayerHop marks all Jupiter transactions `submitViaApi: true`. ## Flow 1. `POST /v1/orders` → `{ vm: "svm", serializedTx, submitViaApi: true }`. 2. Deserialize, sign with the user's wallet, serialize, base64-encode. 3. `POST /v1/orders/submit` with `{ quoteId, signedTransaction }`. 4. The response is `{ provider, txHash }` — the Solana transaction signature. ```ts import { VersionedTransaction } from "@solana/web3.js"; const tx = await lh.buildTx(quote.quoteId); if (tx.vm === "svm") { const signed = await wallet.signTransaction(VersionedTransaction.deserialize(Buffer.from(tx.serializedTx, "base64"))); const b64 = Buffer.from(signed.serialize()).toString("base64"); const signature = tx.submitViaApi ? await lh.submit(quote.quoteId, b64) // through LayerHop : await connection.sendRawTransaction(signed.serialize()); // straight to the network } ``` ## Rules - Calling submit for a quote whose provider broadcasts from the wallet returns [`INVALID_PARAMS`](/errors/INVALID_PARAMS). - Submit within seconds. If the provider rejects the transaction (price moved, quote stale) you get [`PROVIDER_ERROR`](/errors/PROVIDER_ERROR); get a new quote and sign again. - LayerHop only relays the bytes your user signed. It cannot change them, and it never sees a private key. --- # Order tracking > How to follow an order to completion with GET /v1/orders/{provider}/{id} or sdk.watch(), and what id to use per provider. Source: https://docs.layerhop.com/concepts/order-tracking ## The endpoint ```text GET /v1/orders/{provider}/{id} → { provider, id, state, srcTxHash?, dstTxHash? } ``` `provider` is the `provider` field of the quote you executed. `state` is one of: | State | Meaning | Final? | |---|---|---| | `pending` | In flight — source confirmed, destination not yet delivered. | no | | `filled` | Delivered to the recipient. | yes | | `refunded` | Not filled; funds returned to the sender. | yes | | `failed` | Failed. Check the source transaction. | yes | | `unknown` | The provider cannot tell (yet), or does not track this kind of order. | no | ## Which id to pass | Provider | `id` | |---|---| | `relay` | Source (deposit) transaction hash, or Relay's request id | | `debridge` | Source transaction hash (EVM hash or Solana signature), or a DLN order id | | `lifi` | Source transaction hash | | `jupiter`, `zerox` | Anything — always `unknown`; see below | Same-chain swaps (Jupiter, 0x) settle in the transaction itself, so there is nothing to track off-chain. Confirm the transaction on chain (`waitForTransactionReceipt` on EVM, `confirmTransaction` / `getSignatureStatuses` on Solana). If it succeeded, the swap is done. ## Polling with the SDK ```ts for await (const s of lh.watch("relay", srcTxHash, { intervalMs: 3000, timeoutMs: 30 * 60_000 })) { render(s.state, s.dstTxHash); } ``` `watch()` yields only when the state changes and stops at `filled`, `refunded` or `failed`, or after `timeoutMs` (30 minutes by default). ## Tips - Right after broadcasting, providers may answer `unknown` (or `pending`) until they index the source transaction. Keep polling; do not treat `unknown` as failure. - Link the user to the explorer: every chain in [`GET /v1/chains`](/api-reference#list-chains) has an `explorerTx` prefix. - Status calls go to the provider, so they can fail with [`PROVIDER_ERROR`](/errors/PROVIDER_ERROR), [`PROVIDER_TIMEOUT`](/errors/PROVIDER_TIMEOUT) or [`RATE_LIMITED`](/errors/RATE_LIMITED). Retry with backoff. > **Coming soon:** webhooks (`order.created`, `order.fulfilled`, `order.refunded`…) signed with HMAC so you do not have to poll, and order history by address. --- # Non-custodial model > LayerHop never holds user funds or keys — transactions go from the user's wallet straight to audited provider contracts. Source: https://docs.layerhop.com/concepts/non-custodial ## What LayerHop touches, and what it does not | | LayerHop | |---|---| | User funds | **Never.** Funds move from the user's wallet directly into the provider's contracts. | | Private keys | **Never.** The user's wallet signs. The API, SDK and widget never ask for a key or seed phrase. | | Smart contracts | **None of its own (yet).** Transactions target the providers' audited contracts. | | Transaction data | Built by the provider. LayerHop returns it as-is so your user's wallet can show and sign it. | | Signed bytes | Only for [`submitViaApi`](/concepts/submit-via-api) transactions, which LayerHop forwards unchanged. | ## Why it matters - **No honeypot.** There is no LayerHop pool or vault to drain. - **No custody risk.** If LayerHop goes offline, nothing is stuck with LayerHop. In-flight cross-chain orders are settled or refunded by the provider's protocol. - **The wallet is the last line of defence.** Users see the real destination contract and calldata in their wallet before signing. ## Your responsibilities as an integrator - Show the user the route (`provider`), the expected and minimum output, and the fees before they sign. - Check that `recipient` is what the user intended — especially when it is not the connected wallet, and when it is on another chain family (an EVM address cannot receive on Solana and vice versa). - Never collect private keys to "sign on the user's behalf". That would make *you* custodial. > **Coming soon:** LayerHop's own intent protocol (escrow per order, 0-TVL), audited before it handles funds. Partner routes stay available as fallbacks. --- # EVM → Solana swap > Bridge USDC on Base into SOL on Solana end to end — quote, approve, send with viem, and track the order. Source: https://docs.layerhop.com/guides/evm-to-solana This guide moves 100 USDC on Base into SOL on Solana. Relay, deBridge and LI.FI all serve EVM → Solana; LayerHop picks the best one. ## 1. Quote ```ts import { LayerHop, type Quote } from "@layerhop/sdk"; const lh = new LayerHop({ baseUrl: process.env.LAYERHOP_API_URL }); const res = await lh.quote({ srcChain: "base", srcToken: "USDC", amount: "100000000", // 100 USDC, 6 decimals dstChain: "solana", dstToken: "SOL", sender: evmAddress, // the connected EVM wallet recipient: solanaAddress, // a Solana address — not an EVM one slippageBps: 50, }); const quote: Quote = res.best; ``` Show the user `quote.dstAmount` and `quote.dstAmountMin` in SOL (9 decimals), `quote.estimatedSeconds`, `quote.provider` and `quote.fees.ourFeeBps`. Other routes are in `res.quotes`. > [!WARNING] > `recipient` must be a valid address on the **destination** chain. Validate it before quoting; a wrong-family address will be rejected by providers or, worse, deliver to an address the user does not control. ## 2. Build ```ts const tx = await lh.buildTx(quote.quoteId); if (tx.vm !== "evm") throw new Error("expected an EVM transaction for an EVM source"); ``` The source is EVM, so the transaction is an `EvmTx` on `tx.chainId` (8453). Selling USDC means it usually carries an `approval`. ## 3. Approve and send with viem ```ts import { createPublicClient, createWalletClient, custom, erc20Abi, http, type Address, type Hex } from "viem"; import { base } from "viem/chains"; const wallet = createWalletClient({ chain: base, transport: custom(window.ethereum!) }); const pub = createPublicClient({ chain: base, transport: http() }); const [account] = await wallet.requestAddresses(); if ((await wallet.getChainId()) !== tx.chainId) await wallet.switchChain({ id: tx.chainId }); if (tx.approval) { const spender = tx.approval.spender as Address; const token = tx.approval.token as Address; const need = BigInt(tx.approval.amount); const have = await pub.readContract({ address: token, abi: erc20Abi, functionName: "allowance", args: [account!, spender] }); if (have < need) { const h = await wallet.writeContract({ account: account!, address: token, abi: erc20Abi, functionName: "approve", args: [spender, need] }); await pub.waitForTransactionReceipt({ hash: h }); } } const srcTxHash = await wallet.sendTransaction({ account: account!, to: tx.to as Address, data: tx.data as Hex, value: BigInt(tx.value), // non-zero for some providers (e.g. deBridge's fixed fee) or native-coin input }); await pub.waitForTransactionReceipt({ hash: srcTxHash }); ``` ## 4. Track ```ts for await (const s of lh.watch(quote.provider, srcTxHash)) { if (s.state === "filled") console.log("Delivered:", `https://solscan.io/tx/${s.dstTxHash}`); if (s.state === "refunded") console.log("Refunded to the sender on Base"); } ``` Cross-chain providers are tracked by the source transaction hash. Expect `unknown` or `pending` first; Relay and deBridge typically fill in seconds. ## Handling failures | Step | Error | Do this | |---|---|---| | Quote | `NO_ROUTE` | Retry shortly, or change amount/token. | | Build | `QUOTE_EXPIRED`, `QUOTE_NOT_FOUND` | Re-quote, show the new price, ask the user to confirm again. | | Approve/send | Wallet rejection | Nothing was spent; let the user retry. | | Track | `PROVIDER_*`, `RATE_LIMITED` | Keep polling with backoff; the order is on chain regardless. | More in [Error handling & retries](/guides/error-handling). --- # Solana → EVM swap > Move SOL or USDC from Solana to an EVM chain — sign the base64 VersionedTransaction with a Solana wallet and track the order. Source: https://docs.layerhop.com/guides/solana-to-evm Solana → EVM routes are served by **deBridge** and **LI.FI** today. (Relay quotes EVM-source routes only.) ## 1. Quote ```ts const { best: quote } = await lh.quote({ srcChain: "solana", srcToken: "SOL", // native SOL — the wrapped-SOL mint So111…112 amount: "2000000000", // 2 SOL, 9 decimals dstChain: "arbitrum", dstToken: "USDC", sender: solanaAddress, recipient: evmAddress, }); ``` ## 2. Build and sign The source is Solana, so you get an `SvmTx` with a base64 serialized `VersionedTransaction`. ```tsx import { useConnection, useWallet } from "@solana/wallet-adapter-react"; import { VersionedTransaction } from "@solana/web3.js"; const { connection } = useConnection(); const { signTransaction } = useWallet(); const tx = await lh.buildTx(quote.quoteId); if (tx.vm !== "svm") throw new Error("expected a Solana transaction"); const unsigned = VersionedTransaction.deserialize(base64ToBytes(tx.serializedTx)); const signed = await signTransaction!(unsigned); let signature: string; if (tx.submitViaApi) { signature = await lh.submit(quote.quoteId, bytesToBase64(signed.serialize())); } else { signature = await connection.sendRawTransaction(signed.serialize(), { maxRetries: 3 }); } await connection.confirmTransaction(signature, "confirmed"); ``` Browser-safe base64 helpers (no `Buffer` needed): ```ts const base64ToBytes = (b64: string) => Uint8Array.from(atob(b64), (c) => c.charCodeAt(0)); const bytesToBase64 = (bytes: Uint8Array) => btoa(String.fromCharCode(...bytes)); ``` > [!NOTE] > The transaction already contains a recent blockhash chosen by the provider. Sign and send it promptly — Solana blockhashes expire after roughly a minute, and deBridge asks for orders to be submitted within about 30 seconds of building. ## 3. Track ```ts for await (const s of lh.watch(quote.provider, signature)) console.log(s.state); ``` For deBridge and LI.FI the id is the Solana transaction signature. ## Common issues - **Insufficient SOL for fees and rent.** Solana-source swaps need some SOL on top of the amount for the network fee and any token accounts the transaction creates. Keep a small buffer when selling SOL. - **`Blockhash not found` / expired.** Too much time passed between build and send. Build again (re-quote if the quote expired). - **Simulation failure in the wallet.** Usually a price move beyond `slippageBps`. Re-quote. --- # Same-chain swaps > Swap tokens on one chain — Jupiter on Solana, 0x/Relay/LI.FI on EVM — and confirm them on chain. Source: https://docs.layerhop.com/guides/same-chain Same-chain swaps use the same three calls as bridging: quote, build, sign. Only tracking differs. ## Solana (Jupiter, LI.FI) ```ts const { best } = await lh.quote({ srcChain: "solana", srcToken: "USDC", amount: "25000000", // 25 USDC dstChain: "solana", dstToken: "SOL", sender: solanaAddress, recipient: solanaAddress, }); const tx = await lh.buildTx(best.quoteId); // Jupiter → { vm: "svm", serializedTx, submitViaApi: true } ``` Jupiter transactions are always `submitViaApi: true`: sign, then `lh.submit(quoteId, signedBase64)`. See [the submitViaApi flow](/concepts/submit-via-api). > [!NOTE] > Jupiter's referral fee has a 50 bps minimum. With the default `api` channel fee (20 bps) LayerHop takes **no fee** on Jupiter routes, and the quote shows `ourFeeBps: 0`. ## EVM (0x, Relay, LI.FI) ```ts const { best } = await lh.quote({ srcChain: "arbitrum", srcToken: "ETH", amount: "500000000000000000", // 0.5 ETH dstChain: "arbitrum", dstToken: "USDC", sender: evmAddress, recipient: evmAddress, }); const tx = await lh.buildTx(best.quoteId); // { vm: "evm", ..., value: "500000000000000000" } ``` Selling ETH puts the amount in `value` and needs no approval. Selling an ERC-20 returns an [`approval`](/concepts/approvals). 0x quotes need the operator's `ZEROX_API_KEY`. ## Confirming Same-chain providers settle inside the transaction, so `GET /v1/orders/jupiter/{id}` and `GET /v1/orders/zerox/{id}` always return `state: "unknown"`. Confirm on chain instead: ```ts // EVM const receipt = await pub.waitForTransactionReceipt({ hash }); const ok = receipt.status === "success"; // Solana const { value } = await connection.confirmTransaction(signature, "confirmed"); const ok = value.err === null; ``` Relay and LI.FI same-chain swaps can be tracked through the orders endpoint like cross-chain ones. ## Same token, same chain Quoting the same token in and out on one chain fails with [`NO_ROUTE`](/errors/NO_ROUTE): there is nothing to swap. --- # Embed the widget > Put a LayerHop swap card in any site with an iframe, configure it with URL parameters, and listen to its postMessage events. Source: https://docs.layerhop.com/guides/embed-widget The embed is a chromeless swap card — no header, no footer — served by the LayerHop web app at `/embed`. It uses the `widget` [fee channel](/concepts/fees-and-channels). Its URL parameters and events are a public contract (`docs/URL_CONTRACT.md`): parameters are only ever added, never changed. > [!NOTE] > The embed lives at `https://layerhop.com/embed` once the web app is deployed. Until then, run `pnpm --filter @layerhop/web dev` and use `http://localhost:3000/embed`. ## Minimal embed ```html ``` ## Parameters All [deep-link swap parameters](/guides/deep-links) (`srcChain`, `srcToken`, `dstChain`, `dstToken`, `amount`, `recipient`, `slippageBps`), plus: | Param | Example | Meaning | |---|---|---| | `theme` | `dark` \| `light` | Default `dark`. | | `accent` | `22d3ee` | Accent colour, hex without `#`. | | `radius` | `16` | Card corner radius in px. | | `lockSrc` / `lockDst` | `1` | Lock that side's chain and token (e.g. "deposit into my app on Base"). | | `hideHistory` | `1` | Hide the history panel. | | `integrator` | `myapp` | Attribution label, `[a-z0-9-]{1,32}`. | Unknown or invalid parameters are ignored and fall back to defaults; they never break the embed. ## Events The embed posts messages to the parent window: ```ts window.parent.postMessage({ source: "layerhop", type, payload }, "*"); ``` | `type` | `payload` | |---|---| | `ready` | `{ version }` | | `quote` | `{ provider, srcChain, dstChain, amount, dstAmount }` | | `submitted` | `{ provider, srcChainId, srcTxHash }` | | `status` | `{ provider, srcTxHash, state }` — `pending` / `filled` / `refunded` / `failed` | | `error` | `{ code, message }` | | `resize` | `{ height }` — px; resize the iframe to fit | Payloads never contain secrets. ## Listening safely ```ts const LAYERHOP_ORIGIN = "https://layerhop.com"; const frame = document.getElementById("layerhop") as HTMLIFrameElement; window.addEventListener("message", (event) => { if (event.origin !== LAYERHOP_ORIGIN) return; // production: always check origin const msg = event.data; if (!msg || msg.source !== "layerhop") return; switch (msg.type) { case "resize": frame.style.height = `${msg.payload.height}px`; break; case "submitted": analytics.track("swap_submitted", msg.payload); break; case "status": if (msg.payload.state === "filled") showToast("Swap complete"); break; case "error": console.warn("LayerHop", msg.payload.code, msg.payload.message); break; } }); ``` `error.code` uses the same [error codes](/errors) as the API. ## Example: deposit into your app on Base ```html ``` The user can pay from any supported chain and token; the output always lands as USDC on Base at your address. A `recipient` that differs from the connected wallet is shown with a warning, and remains editable. ## `@layerhop/widget` package The package wraps the iframe, validates options, filters messages by origin and auto-resizes. It is not yet published to npm; use it from the monorepo (`packages/widget`) until then. **React** ```tsx import { LayerHopWidget } from "@layerhop/widget/react"; console.log("submitted", e.srcTxHash)} onStatus={(e) => console.log(e.state)} />; ``` **Vanilla JS** ```ts import { mount } from "@layerhop/widget"; const widget = mount("#swap", { dstChain: "base", dstToken: "USDC", lockDst: true, onQuote: console.log }); // widget.update({ theme: "light" }); widget.destroy(); ``` **Script tag** — `dist/embed.js` exposes `window.LayerHop.mount` and auto-mounts elements: ```html
``` Each auto-mounted element also re-dispatches events as bubbling `layerhop:` DOM events (payload in `event.detail`). > **Coming soon:** npm release of `@layerhop/widget`, and integrator fees on top of LayerHop's fee. --- # Deep links > Link straight into a prefilled LayerHop swap with /swap URL parameters, and to the order explorer. Source: https://docs.layerhop.com/guides/deep-links Deep links open the LayerHop app with a swap prefilled. They are a public contract: parameters are only ever added, never changed. ## Swap — `https://layerhop.com/swap?…` | Param | Example | Meaning | |---|---|---| | `srcChain` | `base` | Chain slug or id. | | `srcToken` | `USDC` | Symbol from the [token list](/chains), or address / mint. | | `dstChain` | `solana` | Chain slug or id. | | `dstToken` | `SOL` | Symbol or address / mint. | | `amount` | `100` | **Human units** — a decimal string with `.` as the separator. | | `recipient` | `9WzD…` | Optional. Prefilled but editable; the app warns when it is not the connected wallet. | | `slippageBps` | `50` | Optional. | | `ref` | `x` | Attribution. `ref=bot` uses the `bot` fee channel; any other value uses `web`. | > [!IMPORTANT] > Unlike the API, deep links take `amount` in **human units** (`100` = 100 USDC), because people type and read them. Unknown or invalid parameters are ignored and the app falls back to its defaults; a bad link never shows an error page. ## Examples ```text https://layerhop.com/swap?srcChain=base&srcToken=USDC&dstChain=solana&dstToken=SOL&amount=100 https://layerhop.com/swap?srcChain=solana&srcToken=SOL&dstChain=arbitrum&dstToken=USDC&amount=2.5&slippageBps=100 https://layerhop.com/swap?dstChain=base&dstToken=USDC&recipient=0xYourAddress&ref=newsletter ``` Building one in code: ```ts const url = new URL("https://layerhop.com/swap"); url.search = new URLSearchParams({ srcChain: "base", srcToken: "USDC", dstChain: "solana", dstToken: "SOL", amount: "100", ref: "docs", }).toString(); ``` ## Explorer — `https://layerhop.com/explorer?provider=relay&tx=0x…` Both parameters are optional. Without them the page shows a lookup form; with both, it shows the order's status (same data as [`GET /v1/orders/{provider}/{id}`](/api-reference#get-order-status)). > [!NOTE] > `layerhop.com` is not deployed yet. Locally, the same paths work on `http://localhost:3000` with `pnpm --filter @layerhop/web dev`. --- # Error handling & retries > Branch on stable error codes, know which errors to retry, and recover from expired quotes and provider outages. Source: https://docs.layerhop.com/guides/error-handling ## The error shape Every non-2xx response has the same body, and every response has an `x-request-id` header: ```json { "error": { "code": "QUOTE_EXPIRED", "message": "Quote expired, request a new one", "requestId": "0d5e1f6a-3b2c-4d1e-8f9a-7b6c5d4e3f21", "docsUrl": "https://docs.layerhop.com/errors/QUOTE_EXPIRED" } } ``` - Branch on `code` — it is stable. `message` is for humans and may change. - Log `requestId`; include it when you report a problem. - `docsUrl` points at the [error catalog](/errors). The SDK throws `LayerHopApiError` with `code`, `status`, `message` and `requestId`: ```ts import { LayerHopApiError } from "@layerhop/sdk"; try { await lh.buildTx(quoteId); } catch (e) { if (e instanceof LayerHopApiError && (e.code === "QUOTE_EXPIRED" || e.code === "QUOTE_NOT_FOUND")) { // re-quote } else throw e; } ``` ## What to retry | Code | Retry? | How | |---|---|---| | `INVALID_PARAMS`, `UNSUPPORTED_CHAIN`, `UNSUPPORTED_TOKEN`, `AMOUNT_TOO_SMALL`, `AMOUNT_TOO_LARGE` | No | Fix the request. | | `QUOTE_EXPIRED`, `QUOTE_NOT_FOUND` | After a new quote | Re-quote and ask the user to confirm the new price. | | `NO_ROUTE` | With backoff | Providers may be briefly down; also try other amounts/tokens. | | `PROVIDER_ERROR`, `PROVIDER_TIMEOUT`, `RATE_LIMITED`, `INTERNAL` | With backoff | Exponential backoff with jitter, a few attempts. | | `ORDER_NOT_FOUND` | With backoff | Right after broadcast the provider may not have indexed the order yet. | | `UNAUTHORIZED` | No | A provider key problem on the API side; other quotes still work. | ## A retry helper ```ts import { LayerHopApiError } from "@layerhop/sdk"; const RETRYABLE = new Set(["NO_ROUTE", "PROVIDER_ERROR", "PROVIDER_TIMEOUT", "RATE_LIMITED", "INTERNAL", "ORDER_NOT_FOUND"]); export async function withRetry(fn: () => Promise, attempts = 4, baseMs = 500): Promise { for (let i = 0; ; i++) { try { return await fn(); } catch (e) { const retryable = e instanceof LayerHopApiError ? RETRYABLE.has(e.code) : e instanceof TypeError; // TypeError: network failure if (!retryable || i >= attempts - 1) throw e; await new Promise((r) => setTimeout(r, baseMs * 2 ** i + Math.random() * baseMs)); } } } const quotes = await withRetry(() => lh.quote(params)); ``` ## Expired quotes Quotes live 30–60 seconds. A robust UI: 1. Re-quotes on a timer (for example every 15–20 s) while the user is looking at a price. 2. Builds the transaction only after the user confirms. 3. On `QUOTE_EXPIRED` / `QUOTE_NOT_FOUND`, fetches a fresh quote, shows the new output, and asks again — never silently signs a worse price. ```ts async function buildFresh(params: QuoteParams) { let { best } = await lh.quote(params); if (best.expiresAt - Date.now() / 1000 < 5) ({ best } = await lh.quote(params)); return { quote: best, tx: await lh.buildTx(best.quoteId) }; } ``` ## Per-provider errors in a successful quote A `200` from `GET /v1/quote` can still contain `errors`. Each entry has `provider`, `code` and `message`. Use them for diagnostics ("deBridge: amount too small") — not to block the user, since the quotes you got are valid. ## After the user signed Once a transaction is on chain, the order belongs to the provider's protocol: errors from the status endpoint mean "could not check right now", not "the swap failed". Keep polling with backoff, and show the source transaction in the explorer. > **Coming soon:** an `Idempotency-Key` header on POST requests and LayerHop-side rate limits with `Retry-After`. --- # API reference > Every LayerHop REST endpoint, parameter and schema — generated from openapi.yaml (API 0.1.0). Source: https://docs.layerhop.com/api-reference > [!NOTE] > This page is generated at build time from `services/api/openapi.yaml` (OpenAPI 3.1.0, API version 0.1.0). LayerHop is a meta-aggregator. `GET /v1/quote` asks every liquidity provider that supports the route (Jupiter, Relay, deBridge, 0x, LI.FI) in parallel and ranks the answers by time-adjusted output for the user. `POST /v1/orders` turns a quote into a transaction the user's own wallet signs. LayerHop never holds funds. Conventions: - Amounts are integer strings in base units (never floats). - Chains are given as a numeric id or a slug (`base`, `solana`). Solana uses LayerHop's internal id `7565164`. - Tokens are given as a symbol from the built-in token list (`USDC`) or as an address / mint. - Every response carries an `x-request-id` header; error bodies repeat it as `requestId`. - Errors use one shape, `{ "error": { code, message, requestId, docsUrl } }`, with a stable `code` (see the `ErrorCode` schema). ## Base URLs | URL | Description | |---|---| | `http://localhost:8787` | Local API (`pnpm --filter @layerhop/api dev`) | | `https://api.layerhop.com` | Hosted API (planned — not live yet) | ## Authentication - `x-api-key` (header) — Optional. The SDK sends it when configured, but the API does not enforce keys or rate limits yet (coming soon). ## Conventions - All paths are under `/v1`. Request and response bodies are JSON. - Every response has an `x-request-id` header. Error bodies repeat it as `requestId`. - Non-2xx responses use the [Error](#error) schema; its `docsUrl` points at the [error catalog](/errors). ## Meta Liveness and static registries. ### Health check `GET /v1/health` Liveness probe. Lists the providers loaded in this API instance. **Responses** | Status | Description | Body | |---|---|---| | `200` | The API is up. | [HealthResponse](#healthresponse) | **Example request** ```bash curl "http://localhost:8787/v1/health" ``` **Example response** (`200`) ```json { "ok": true, "providers": [ "jupiter", "relay", "debridge", "zerox", "lifi" ] } ``` ### List chains `GET /v1/chains` Every chain LayerHop can route from or to. **Responses** | Status | Description | Body | |---|---|---| | `200` | The chain registry. | [ChainsResponse](#chainsresponse) | **Example request** ```bash curl "http://localhost:8787/v1/chains" ``` **Example response** (`200`) ```json { "chains": [ { "id": 8453, "slug": "base", "name": "Base", "vm": "evm", "nativeSymbol": "ETH", "nativeDecimals": 18, "explorerTx": "https://basescan.org/tx/" }, { "id": 7565164, "slug": "solana", "name": "Solana", "vm": "svm", "nativeSymbol": "SOL", "nativeDecimals": 9, "explorerTx": "https://solscan.io/tx/" } ] } ``` ### List tokens `GET /v1/tokens` The built-in token list for one chain. Symbols on this list can be used in place of addresses in `GET /v1/quote`. Any other token can still be quoted by address / mint. **Query parameters** | Name | Type | Required | Description | |---|---|---|---| | `chain` | string | yes | Chain id or slug. | **Responses** | Status | Description | Body | |---|---|---| | `200` | Tokens on the chain. | [TokensResponse](#tokensresponse) | | `400` | The chain is unknown (`UNSUPPORTED_CHAIN`). | [Error](#error) | | `500` | Unexpected server error (`INTERNAL`). | [Error](#error) | **Example request** ```bash curl "http://localhost:8787/v1/tokens?chain=base" ``` **Example response** (`200`) ```json { "tokens": [ { "chainId": 8453, "address": "0x0000000000000000000000000000000000000000", "symbol": "ETH", "decimals": 18 }, { "chainId": 8453, "address": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", "symbol": "USDC", "decimals": 6 } ] } ``` ## Quotes Ask every provider for a price. ### Get quotes `GET /v1/quote` Fans the request out to every provider that supports the route, in parallel, with a per-provider timeout (8 s by default). Successful quotes are ranked best-for-the-user first by time-adjusted output (1 bp penalty per minute of ETA beyond the first minute, capped at 50 bps), then by speed. Providers that failed are listed in `errors` and do not fail the request, unless every provider failed (`NO_ROUTE`). Each quote has its own `quoteId`; pass it to `POST /v1/orders` before `expiresAt`. **Query parameters** | Name | Type | Required | Description | |---|---|---|---| | `srcChain` | string | yes | Source chain id or slug. | | `srcToken` | string | yes | Source token symbol from the token list, or address / mint. | | `amount` | string | yes | Input amount, integer string in base units of `srcToken`. Must be greater than 0. (pattern `^\d+$`) | | `dstChain` | string | yes | Destination chain id or slug. | | `dstToken` | string | yes | Destination token symbol from the token list, or address / mint. | | `sender` | string | yes | Address that signs and pays on the source chain. (min length 1) | | `recipient` | string | yes | Address that receives the output on the destination chain. (min length 1) | | `slippageBps` | integer | no | Maximum slippage in basis points. (default `50`) (1–5000) | | `channel` | [FeeChannel](#feechannel) | no | Where the trade comes from. Decides the LayerHop fee (see `FeeChannel`). | **Responses** | Status | Description | Body | |---|---|---| | `200` | Ranked quotes. | [QuoteResponse](#quoteresponse) | | `400` | Invalid request (`INVALID_PARAMS`, `UNSUPPORTED_CHAIN`, `UNSUPPORTED_TOKEN`, `AMOUNT_TOO_SMALL`, `AMOUNT_TOO_LARGE`). | [Error](#error) | | `404` | No provider supports the route, or every provider failed (`NO_ROUTE`). | [Error](#error) | | `500` | Unexpected server error (`INTERNAL`). | [Error](#error) | **Example request** ```bash curl "http://localhost:8787/v1/quote?srcChain=base&srcToken=USDC&amount=100000000&dstChain=solana&dstToken=SOL&sender=0x1111111111111111111111111111111111111111&recipient=9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM" ``` **Example response** (`200`) ```json { "best": { "quoteId": "q_6f1c2d0e-8a51-4c1e-9d0b-2f7a9e3c4b10", "provider": "relay", "dstAmount": "612345678", "dstAmountMin": "609283949", "estimatedSeconds": 8, "fees": { "ourFeeBps": 20, "providerFeeUsd": 0.12, "gasUsd": 0.03 }, "expiresAt": 1760000030 }, "quotes": [ { "quoteId": "q_6f1c2d0e-8a51-4c1e-9d0b-2f7a9e3c4b10", "provider": "relay", "dstAmount": "612345678", "dstAmountMin": "609283949", "estimatedSeconds": 8, "fees": { "ourFeeBps": 20, "providerFeeUsd": 0.12, "gasUsd": 0.03 }, "expiresAt": 1760000030 }, { "quoteId": "q_0b9a4f7e-1d2c-4e3b-8a6f-5c4d3e2b1a09", "provider": "debridge", "dstAmount": "611900000", "dstAmountMin": "611900000", "estimatedSeconds": 6, "fees": { "ourFeeBps": 20 }, "expiresAt": 1760000030 } ], "errors": [ { "provider": "lifi", "code": "PROVIDER_TIMEOUT", "message": "Timed out: li.quest" } ] } ``` ## Orders Build, submit and track the transaction for a quote. ### Build the transaction `POST /v1/orders` Returns the transaction for a quote, for the user's wallet to sign. EVM transactions may carry an `approval` (an ERC-20 `approve` to send first). Solana transactions are a base64 serialized `VersionedTransaction`; when `submitViaApi` is true, sign it and send it to `POST /v1/orders/submit` instead of an RPC node. The quote must not be expired. Some providers ask you to submit within ~30 s of building. **Request body** — [BuildOrderRequest](#buildorderrequest) (required) **Responses** | Status | Description | Body | |---|---|---| | `200` | The transaction to sign. | [BuildOrderResponse](#buildorderresponse) | | `400` | Invalid request (`INVALID_PARAMS`, `UNSUPPORTED_CHAIN`, `UNSUPPORTED_TOKEN`, `AMOUNT_TOO_SMALL`, `AMOUNT_TOO_LARGE`). | [Error](#error) | | `404` | Unknown `quoteId`, or the provider can no longer build it (`QUOTE_NOT_FOUND`). | [Error](#error) | | `410` | The quote is past `expiresAt` (`QUOTE_EXPIRED`). | [Error](#error) | | `500` | Unexpected server error (`INTERNAL`). | [Error](#error) | | `502` | The upstream provider failed (`PROVIDER_ERROR`). | [Error](#error) | | `504` | The upstream provider did not answer in time (`PROVIDER_TIMEOUT`). | [Error](#error) | **Example request** ```bash curl -X POST "http://localhost:8787/v1/orders" \ -H "content-type: application/json" \ -d '{"quoteId":"q_6f1c2d0e-8a51-4c1e-9d0b-2f7a9e3c4b10"}' ``` **Example response** (`200`) ```json { "quoteId": "q_6f1c2d0e-8a51-4c1e-9d0b-2f7a9e3c4b10", "provider": "relay", "tx": { "vm": "evm", "chainId": 8453, "to": "0xa5F565650890fBA1824Ee0F21EbBbF660a179934", "data": "0x58109c...", "value": "0", "approval": { "token": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", "spender": "0xa5F565650890fBA1824Ee0F21EbBbF660a179934", "amount": "100000000" } } } ``` ### Submit a signed transaction `POST /v1/orders/submit` Only for Solana transactions built with `submitViaApi: true` (today: Jupiter, whose RFQ and gasless transactions are co-signed and never land through a plain RPC node). Send the wallet-signed transaction as base64; LayerHop forwards it to the provider and returns the transaction signature. For every other provider this returns `INVALID_PARAMS` — broadcast from the wallet instead. **Request body** — [SubmitOrderRequest](#submitorderrequest) (required) **Responses** | Status | Description | Body | |---|---|---| | `200` | The provider accepted and executed the transaction. | [SubmitOrderResponse](#submitorderresponse) | | `400` | Invalid request (`INVALID_PARAMS`, `UNSUPPORTED_CHAIN`, `UNSUPPORTED_TOKEN`, `AMOUNT_TOO_SMALL`, `AMOUNT_TOO_LARGE`). | [Error](#error) | | `404` | Unknown `quoteId`, or the provider can no longer build it (`QUOTE_NOT_FOUND`). | [Error](#error) | | `500` | Unexpected server error (`INTERNAL`). | [Error](#error) | | `502` | The upstream provider failed (`PROVIDER_ERROR`). | [Error](#error) | | `504` | The upstream provider did not answer in time (`PROVIDER_TIMEOUT`). | [Error](#error) | **Example request** ```bash curl -X POST "http://localhost:8787/v1/orders/submit" \ -H "content-type: application/json" \ -d '{"quoteId":"q_6f1c2d0e-8a51-4c1e-9d0b-2f7a9e3c4b10","signedTransaction":"AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA..."}' ``` **Example response** (`200`) ```json { "provider": "jupiter", "txHash": "5VERv8NMvzbJMEkV8xnrLkEaWRtSz9CosKDYjCJjBRnbJLgp8uirBgmQpjKhoR4tjF3ZpRzrFmBV6UjKdiSZkQUW" } ``` ### Get order status `GET /v1/orders/{provider}/{id}` Asks the provider for the current state of an order. What `id` means depends on the provider: - `relay`: Relay request id or the source deposit tx hash. - `debridge`: source tx hash (EVM hash or Solana signature) or a DLN order id. - `lifi`: source tx hash. - `jupiter`, `zerox`: same-chain swaps have nothing to track off-chain; the state is always `unknown`. Confirm the transaction on chain instead. Poll until `state` is `filled`, `refunded` or `failed`. **Path parameters** | Name | Type | Required | Description | |---|---|---|---| | `provider` | [ProviderId](#providerid) | yes | The `provider` from the quote. Must be a provider loaded in this API instance. | | `id` | string | yes | Provider order id or source tx hash (see above). URL-encode it. | **Responses** | Status | Description | Body | |---|---|---| | `200` | Current order state. | [OrderStatus](#orderstatus) | | `400` | Invalid request (`INVALID_PARAMS`, `UNSUPPORTED_CHAIN`, `UNSUPPORTED_TOKEN`, `AMOUNT_TOO_SMALL`, `AMOUNT_TOO_LARGE`). | [Error](#error) | | `404` | The provider does not know this order (`ORDER_NOT_FOUND`). | [Error](#error) | | `429` | The upstream provider rate-limited LayerHop (`RATE_LIMITED`). | [Error](#error) | | `500` | Unexpected server error (`INTERNAL`). | [Error](#error) | | `502` | The upstream provider failed (`PROVIDER_ERROR`). | [Error](#error) | | `504` | The upstream provider did not answer in time (`PROVIDER_TIMEOUT`). | [Error](#error) | **Example request** ```bash curl "http://localhost:8787/v1/orders/relay/0x3f1e6a2b9c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f" ``` **Example response** (`200`) ```json { "provider": "relay", "id": "0x3f1e6a2b9c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f", "state": "filled", "srcTxHash": "0x3f1e6a2b9c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f", "dstTxHash": "3nKqYdW1hS8vZ4cT2pXeR7mB9uJfL6aG5oN1wQ8kE3sD7yH2xV4tC6bM9rP1zA5uF8jL2gK7eW3qS6dN4hT9vB1" } ``` ## Schemas ### ErrorCode Stable error code. Each has a page at `https://docs.layerhop.com/errors/{code}`. `string`, one of: - [`INVALID_PARAMS`](/errors/INVALID_PARAMS) - [`UNSUPPORTED_CHAIN`](/errors/UNSUPPORTED_CHAIN) - [`UNSUPPORTED_TOKEN`](/errors/UNSUPPORTED_TOKEN) - [`AMOUNT_TOO_SMALL`](/errors/AMOUNT_TOO_SMALL) - [`AMOUNT_TOO_LARGE`](/errors/AMOUNT_TOO_LARGE) - [`NO_ROUTE`](/errors/NO_ROUTE) - [`QUOTE_NOT_FOUND`](/errors/QUOTE_NOT_FOUND) - [`QUOTE_EXPIRED`](/errors/QUOTE_EXPIRED) - [`ORDER_NOT_FOUND`](/errors/ORDER_NOT_FOUND) - [`PROVIDER_ERROR`](/errors/PROVIDER_ERROR) - [`PROVIDER_TIMEOUT`](/errors/PROVIDER_TIMEOUT) - [`RATE_LIMITED`](/errors/RATE_LIMITED) - [`UNAUTHORIZED`](/errors/UNAUTHORIZED) - [`INTERNAL`](/errors/INTERNAL) ### Error Every non-2xx response has this body. | Field | Type | Required | Description | |---|---|---|---| | `error` | object | yes | — | | `error.code` | [ErrorCode](#errorcode) | yes | — | | `error.message` | string | yes | Human-readable detail. Do not parse it. | | `error.requestId` | string (uuid) | yes | Same value as the `x-request-id` header. | | `error.docsUrl` | string (uri) | yes | Error catalog page, `https://docs.layerhop.com/errors/{code}`. | ### ProviderId Liquidity provider. Loaded by default: `jupiter`, `relay`, `debridge`, `zerox`, `lifi`. `mayan` is quote-only and disabled; `odos` is shut down; `mock` exists only in local development (`LAYERHOP_ENABLE_MOCK=1`). `string`, one of: - `jupiter` - `relay` - `debridge` - `mayan` - `zerox` - `odos` - `lifi` - `mock` ### FeeChannel Where the trade comes from; decides LayerHop's fee. Defaults: `web` 30 bps, `widget` 30 bps, `api` 20 bps, `bot` 90 bps (operators can override them). `string`, one of: - `web` - `widget` - `bot` - `api` ### Vm Virtual machine family. `svm` is Solana. `string`, one of: - `evm` - `svm` ### Chain | Field | Type | Required | Description | |---|---|---|---| | `id` | integer | yes | EVM chain id, or `7565164` for Solana (LayerHop internal id). | | `slug` | string | yes | Short name usable anywhere a chain is expected, e.g. `base`. | | `name` | string | yes | — | | `vm` | [Vm](#vm) | yes | — | | `nativeSymbol` | string | yes | — | | `nativeDecimals` | integer | yes | — | | `explorerTx` | string (uri) | yes | Block explorer prefix; append a tx hash. | ### Token | Field | Type | Required | Description | |---|---|---|---| | `chainId` | integer | yes | — | | `address` | string | yes | Token address or mint. The native coin is `0x0000000000000000000000000000000000000000` on EVM chains and the wrapped-SOL mint `So11111111111111111111111111111111111111112` on Solana. | | `symbol` | string | yes | — | | `decimals` | integer | yes | — | ### HealthResponse | Field | Type | Required | Description | |---|---|---|---| | `ok` | `true` | yes | — | | `providers` | [ProviderId](#providerid)[] | yes | — | ### ChainsResponse | Field | Type | Required | Description | |---|---|---|---| | `chains` | [Chain](#chain)[] | yes | — | ### TokensResponse | Field | Type | Required | Description | |---|---|---|---| | `tokens` | [Token](#token)[] | yes | — | ### FeeBreakdown | Field | Type | Required | Description | |---|---|---|---| | `ourFeeBps` | integer | yes | LayerHop's fee in bps of the input amount actually applied to this quote. Can be lower than the channel fee (or 0) when a provider cannot carry it. | | `providerFeeUsd` | number | no | Provider / protocol fees in USD, when the provider reports them. | | `gasUsd` | number | no | Estimated network gas in USD, when known. | ### Quote | Field | Type | Required | Description | |---|---|---|---| | `quoteId` | string | yes | Pass to `POST /v1/orders`. (pattern `^q_`) | | `provider` | [ProviderId](#providerid) | yes | — | | `dstAmount` | string | yes | Expected output in base units of the destination token, after all fees. (pattern `^\d+$`) | | `dstAmountMin` | string | yes | Minimum output after slippage. (pattern `^\d+$`) | | `estimatedSeconds` | integer | yes | Estimated time until funds arrive. (≥ 0) | | `fees` | [FeeBreakdown](#feebreakdown) | yes | — | | `expiresAt` | integer | yes | Unix seconds. Build the order before this time. | ### ProviderError A provider that failed while the request as a whole succeeded. | Field | Type | Required | Description | |---|---|---|---| | `provider` | [ProviderId](#providerid) | yes | — | | `code` | [ErrorCode](#errorcode) | yes | — | | `message` | string | yes | — | ### QuoteResponse | Field | Type | Required | Description | |---|---|---|---| | `best` | [Quote](#quote) | yes | Same object as `quotes[0]`. | | `quotes` | [Quote](#quote)[] | yes | Every successful quote, best for the user first. (min items 1) | | `errors` | [ProviderError](#providererror)[] | yes | — | ### Approval An ERC-20 `approve(spender, amount)` on `token` that must be mined before the main transaction. | Field | Type | Required | Description | |---|---|---|---| | `token` | string | yes | ERC-20 contract to call `approve` on. | | `spender` | string | yes | — | | `amount` | string | yes | Base units. (pattern `^\d+$`) | ### EvmTx | Field | Type | Required | Description | |---|---|---|---| | `vm` | `"evm"` | yes | — | | `chainId` | integer | yes | — | | `to` | string | yes | — | | `data` | string | yes | Hex calldata. | | `value` | string | yes | Native value in wei. (pattern `^\d+$`) | | `approval` | [Approval](#approval) | no | — | ### SvmTx | Field | Type | Required | Description | |---|---|---|---| | `vm` | `"svm"` | yes | — | | `serializedTx` | string | yes | Base64 serialized Solana `VersionedTransaction`, for the user's wallet to sign. (base64) | | `submitViaApi` | boolean | no | When true the wallet must only sign; send the signed transaction to `POST /v1/orders/submit`. Plain RPC will not land it. | ### BuiltTx The transaction for the user's wallet, discriminated by `vm`. One of: [EvmTx](#evmtx) · [SvmTx](#svmtx), discriminated by `vm`. ### BuildOrderRequest | Field | Type | Required | Description | |---|---|---|---| | `quoteId` | string | yes | — | ### BuildOrderResponse | Field | Type | Required | Description | |---|---|---|---| | `quoteId` | string | yes | — | | `provider` | [ProviderId](#providerid) | yes | — | | `tx` | [BuiltTx](#builttx) | yes | — | ### SubmitOrderRequest | Field | Type | Required | Description | |---|---|---|---| | `quoteId` | string | yes | — | | `signedTransaction` | string | yes | The wallet-signed transaction, base64. (min length 1) (base64) | ### SubmitOrderResponse | Field | Type | Required | Description | |---|---|---|---| | `provider` | [ProviderId](#providerid) | yes | — | | `txHash` | string | yes | Solana transaction signature. | ### OrderState `pending` in flight · `filled` delivered · `refunded` returned to the sender · `failed` · `unknown` the provider cannot tell (yet), or does not track this kind of order. `string`, one of: - `pending` - `filled` - `refunded` - `failed` - `unknown` ### OrderStatus | Field | Type | Required | Description | |---|---|---|---| | `provider` | [ProviderId](#providerid) | yes | — | | `id` | string | yes | The id you asked about. | | `state` | [OrderState](#orderstate) | yes | — | | `srcTxHash` | string | no | — | | `dstTxHash` | string | no | — | --- # SDK reference > The @layerhop/sdk TypeScript client — LayerHop class options, quote, buildTx, submit, status, watch, types and errors. Source: https://docs.layerhop.com/sdk `@layerhop/sdk` is a thin, typed client for the [REST API](/api-reference). It has no runtime dependencies besides `@layerhop/shared`, uses the global `fetch`, and works in Node 20+, browsers and edge runtimes. > **Coming soon:** publication on npm. Inside the monorepo use `"@layerhop/sdk": "workspace:*"`. ## new LayerHop(options?) ```ts import { LayerHop } from "@layerhop/sdk"; const lh = new LayerHop({ baseUrl: "http://localhost:8787", apiKey: process.env.LAYERHOP_API_KEY, channel: "api", }); ``` | Option | Type | Default | Description | |---|---|---|---| | `baseUrl` | `string` | `https://api.layerhop.com` | API origin, without `/v1`. A trailing slash is removed. | | `apiKey` | `string` | — | Sent as the `x-api-key` header. Not enforced by the API yet. | | `channel` | `"api" \| "web" \| "widget" \| "bot"` | `"api"` | [Fee channel](/concepts/fees-and-channels) sent with every quote. | | `fetch` | `typeof fetch` | `globalThis.fetch` | Custom fetch (tests, proxies, retries). | ## quote(params) Calls [`GET /v1/quote`](/api-reference#get-quotes). Returns every successful quote, best for the user first. ```ts quote(p: QuoteParams): Promise ``` | Param | Type | Description | |---|---|---| | `srcChain` | `string \| number` | Chain id or slug, e.g. `"base"`, `8453`. | | `srcToken` | `string` | Symbol from the [token list](/chains) or address / mint. | | `amount` | `string` | Integer string in base units of `srcToken`. | | `dstChain` | `string \| number` | Chain id or slug. | | `dstToken` | `string` | Symbol or address / mint. | | `sender` | `string` | Address that signs on the source chain. | | `recipient` | `string` | Address that receives on the destination chain. | | `slippageBps` | `number?` | Default `50`. Range 1–5000. | ```ts interface QuoteResponse { best: Quote; // same as quotes[0] quotes: Quote[]; errors: { provider: string; code: string; message: string }[]; } interface Quote { quoteId: string; provider: ProviderId; // "jupiter" | "relay" | "debridge" | "zerox" | "lifi" | … dstAmount: string; // expected output, base units dstAmountMin: string; // after slippage estimatedSeconds: number; fees: { ourFeeBps: number; providerFeeUsd?: number; gasUsd?: number }; expiresAt: number; // unix seconds } ``` ## buildTx(quoteId) Calls [`POST /v1/orders`](/api-reference#build-the-transaction) and returns only the transaction. ```ts buildTx(quoteId: string): Promise type BuiltTx = EvmTx | SvmTx; type EvmTx = { vm: "evm"; chainId: number; to: string; data: string; value: string; // wei approval?: { token: string; spender: string; amount: string }; }; type SvmTx = { vm: "svm"; serializedTx: string; // base64 VersionedTransaction submitViaApi?: boolean; // sign only, then call submit() }; ``` Narrow on `tx.vm`. See [Approvals](/concepts/approvals) and [the submitViaApi flow](/concepts/submit-via-api). ## submit(quoteId, signedTransaction) Calls [`POST /v1/orders/submit`](/api-reference#submit-a-signed-transaction). Only for `SvmTx` with `submitViaApi: true`. Returns the Solana transaction signature. ```ts submit(quoteId: string, signedTransaction: string /* base64 */): Promise ``` ## status(provider, id) Calls [`GET /v1/orders/{provider}/{id}`](/api-reference#get-order-status). The id is URL-encoded for you. ```ts status(provider: ProviderId, id: string): Promise interface OrderStatus { provider: ProviderId; id: string; state: "pending" | "filled" | "refunded" | "failed" | "unknown"; srcTxHash?: string; dstTxHash?: string; } ``` Which `id` to pass per provider: [Order tracking](/concepts/order-tracking). ## watch(provider, id, options?) Polls `status()` and yields each **change** of state until it is `filled`, `refunded` or `failed`, or until the timeout. ```ts watch(provider: ProviderId, id: string, opts?: { intervalMs?: number; timeoutMs?: number }): AsyncGenerator ``` | Option | Default | |---|---| | `intervalMs` | `3000` | | `timeoutMs` | `1800000` (30 minutes) | ```ts for await (const s of lh.watch("debridge", srcTxHash)) console.log(s.state); ``` When the timeout is reached the generator simply ends; check the last state you saw. Errors from `status()` are thrown out of the loop — wrap it if you want to keep polling through transient provider errors. ## LayerHopApiError Every non-2xx response is thrown as `LayerHopApiError`: | Property | Type | Description | |---|---|---| | `code` | `string` | Stable [error code](/errors) (`"INTERNAL"` if the body had none). | | `message` | `string` | Human-readable message. | | `status` | `number` | HTTP status. | | `requestId` | `string?` | Request id for support. | ```ts import { LayerHopApiError } from "@layerhop/sdk"; try { await lh.buildTx(id); } catch (e) { if (e instanceof LayerHopApiError && e.code === "QUOTE_EXPIRED") { /* re-quote */ } } ``` ## Re-exports For convenience the SDK re-exports from `@layerhop/shared`: | Export | What | |---|---| | `CHAINS`, `getChain(idOrSlug)` | The [chain registry](/chains). | | `TOKENS`, `findToken(chainId, symbolOrAddress)` | The built-in token list. | | `SOLANA_CHAIN_ID` | `7565164`, LayerHop's id for Solana. | | `EVM_NATIVE`, `SOL_NATIVE` | Native coin addresses (`0x000…000`, wrapped-SOL mint). | | Types | `BuiltTx`, `EvmTx`, `SvmTx`, `OrderStatus`, `ProviderId`, `FeeChannel`. | ```ts import { findToken, getChain } from "@layerhop/sdk"; const base = getChain("base")!; // { id: 8453, … } const usdc = findToken(base.id, "USDC")!; // { decimals: 6, address: "0x8335…" } const amount = (100n * 10n ** BigInt(usdc.decimals)).toString(); // "100000000" ``` > **Coming soon:** `@layerhop/react` hooks (`useQuote`, `useSwap`, `useOrderStatus`, `useTokens`) and Python, Go and Rust SDKs generated from the OpenAPI spec. --- # Error codes > Every stable LayerHop error code with its HTTP status, cause and fix. Source: https://docs.layerhop.com/errors Every error response has the same shape. `code` is stable and safe to branch on; `message` is for humans and may change. ```json { "error": { "code": "QUOTE_EXPIRED", "message": "Quote expired, request a new one", "requestId": "0d5e1f6a-…", "docsUrl": "https://docs.layerhop.com/errors/QUOTE_EXPIRED" } } ``` The same codes appear per provider in the `errors` array of a successful [quote response](/api-reference#quoteresponse). The SDK throws them as `LayerHopApiError` with `code`, `status` and `requestId`. ## All codes | Code | HTTP | Meaning | |---|---|---| | [`INVALID_PARAMS`](/errors/INVALID_PARAMS) | `400` | A required parameter is missing or malformed, or the request is not allowed for this provider. | | [`UNSUPPORTED_CHAIN`](/errors/UNSUPPORTED_CHAIN) | `400` | The chain id or slug is not in LayerHop's chain registry, or a provider does not serve it. | | [`UNSUPPORTED_TOKEN`](/errors/UNSUPPORTED_TOKEN) | `400` | A provider does not recognise or will not route the source or destination token. | | [`AMOUNT_TOO_SMALL`](/errors/AMOUNT_TOO_SMALL) | `400` | The input amount is zero or below the provider's minimum. | | [`AMOUNT_TOO_LARGE`](/errors/AMOUNT_TOO_LARGE) | `400` | The input amount exceeds what a provider can fill. | | [`NO_ROUTE`](/errors/NO_ROUTE) | `404` | No provider supports this route, or every provider that does failed to quote. | | [`QUOTE_NOT_FOUND`](/errors/QUOTE_NOT_FOUND) | `404` | The `quoteId` is unknown to this API instance, or the provider can no longer build it. | | [`QUOTE_EXPIRED`](/errors/QUOTE_EXPIRED) | `410` | The quote is past its `expiresAt` and can no longer be built. | | [`ORDER_NOT_FOUND`](/errors/ORDER_NOT_FOUND) | `404` | The provider does not know the order id or transaction hash. | | [`PROVIDER_ERROR`](/errors/PROVIDER_ERROR) | `502` | An upstream liquidity provider returned an error or an unusable response. | | [`PROVIDER_TIMEOUT`](/errors/PROVIDER_TIMEOUT) | `504` | An upstream provider did not answer in time. | | [`RATE_LIMITED`](/errors/RATE_LIMITED) | `429` | An upstream provider rate-limited LayerHop. | | [`UNAUTHORIZED`](/errors/UNAUTHORIZED) | `401` | A provider rejected LayerHop's credentials. | | [`INTERNAL`](/errors/INTERNAL) | `500` | An unexpected error inside LayerHop. | Codes come from `packages/shared/src/errors.ts`; this page is generated from it. --- # Supported chains & tokens > The 7 chains and 18 built-in tokens LayerHop routes today, generated from the shared registry. Source: https://docs.layerhop.com/chains This page is generated from `CHAINS` and `TOKENS` in `packages/shared`. The same data is served by [GET /v1/chains](/api-reference#list-chains) and [GET /v1/tokens](/api-reference#list-tokens). ## Chains | Chain | Slug | Id | VM | Native coin (decimals) | |---|---|---|---|---| | Ethereum | `ethereum` | `1` | EVM | ETH (18) | | Arbitrum | `arbitrum` | `42161` | EVM | ETH (18) | | Base | `base` | `8453` | EVM | ETH (18) | | Optimism | `optimism` | `10` | EVM | ETH (18) | | BNB Chain | `bsc` | `56` | EVM | BNB (18) | | Polygon | `polygon` | `137` | EVM | POL (18) | | Solana | `solana` | `7565164` | SVM | SOL (9) | > [!NOTE] > Solana has no numeric chain id, so LayerHop uses the internal id `7565164` everywhere a chain id is expected. Providers that use other ids for Solana are translated for you. Use either the slug or the id wherever the API or SDK asks for a chain. ## Native coins - EVM native coin (ETH, BNB, POL): `0x0000000000000000000000000000000000000000` - Solana native SOL: the wrapped-SOL mint `So11111111111111111111111111111111111111112` (providers treat it as SOL) ## Built-in tokens Symbols on this list can be used instead of addresses (`srcToken=USDC`). Any other token can still be quoted by its address or mint; whether it routes depends on the [providers](/providers). ### Ethereum | Symbol | Address / mint | Decimals | |---|---|---| | ETH | `0x0000000000000000000000000000000000000000` | 18 | | USDC | `0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48` | 6 | | USDT | `0xdAC17F958D2ee523a2206206994597C13D831ec7` | 6 | ### Arbitrum | Symbol | Address / mint | Decimals | |---|---|---| | ETH | `0x0000000000000000000000000000000000000000` | 18 | | USDC | `0xaf88d065e77c8cC2239327C5EDb3A432268e5831` | 6 | | USDT | `0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9` | 6 | ### Base | Symbol | Address / mint | Decimals | |---|---|---| | ETH | `0x0000000000000000000000000000000000000000` | 18 | | USDC | `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913` | 6 | ### Optimism | Symbol | Address / mint | Decimals | |---|---|---| | ETH | `0x0000000000000000000000000000000000000000` | 18 | | USDC | `0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85` | 6 | ### BNB Chain | Symbol | Address / mint | Decimals | |---|---|---| | BNB | `0x0000000000000000000000000000000000000000` | 18 | | USDC | `0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d` | 18 | | USDT | `0x55d398326f99059fF775485246999027B3197955` | 18 | ### Polygon | Symbol | Address / mint | Decimals | |---|---|---| | POL | `0x0000000000000000000000000000000000000000` | 18 | | USDC | `0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359` | 6 | ### Solana | Symbol | Address / mint | Decimals | |---|---|---| | SOL | `So11111111111111111111111111111111111111112` | 9 | | USDC | `EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v` | 6 | | USDT | `Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB` | 6 | ## Testnets > **Coming soon:** testnet chains (Sepolia, Base Sepolia, Arbitrum Sepolia, Solana Devnet). The providers LayerHop aggregates today are mainnet-only; for development, run the API with the mock provider (see the [Quickstart](/quickstart)). --- # Providers > The liquidity providers behind LayerHop — Jupiter, Relay, deBridge, 0x, LI.FI — what each covers, how fees and tracking work, and what is coming. Source: https://docs.layerhop.com/providers LayerHop routes through these providers. Each one is queried only for routes it supports; see [Providers & ranking](/concepts/providers-and-ranking) for how the winner is chosen. ## Coverage at a glance | Provider | `provider` id | Routes | Tx type | Tracking id | |---|---|---|---|---| | Jupiter | `jupiter` | Solana → Solana | `svm`, `submitViaApi` | — (confirm on chain) | | Relay | `relay` | EVM → EVM (same or cross-chain), EVM → Solana | `evm` | source tx hash or Relay request id | | deBridge | `debridge` | Cross-chain between all supported chains, incl. Solana → EVM | `evm` or `svm` | source tx hash or DLN order id | | 0x | `zerox` | Same-chain on EVM | `evm` | — (confirm on chain) | | LI.FI | `lifi` | All supported chain pairs | `evm` or `svm` | source tx hash | ## Jupiter Solana's swap aggregator. LayerHop uses Jupiter's order-and-execute flow, which returns a quote together with a ready-to-sign transaction. - **Routes:** Solana same-chain swaps between different tokens. - **Transactions:** always `submitViaApi: true`. RFQ and gasless routes are co-signed by Jupiter and only land through its execute endpoint. See [the submitViaApi flow](/concepts/submit-via-api). - **Quote lifetime:** at most 30 s (shorter if the RFQ quote expires sooner). - **LayerHop fee:** Jupiter's referral program, 50–255 bps; Jupiter keeps 20%. Channel fees below 50 bps are not charged on Jupiter routes (`ourFeeBps: 0`). - **Tracking:** nothing to track off-chain; status is always `unknown`. ## Relay A cross-chain and same-chain execution network covering EVM chains and Solana. - **Routes:** any EVM source to any supported destination (EVM or Solana). - **Not supported:** Solana as the **source** — Relay returns raw Solana instructions rather than a signable transaction for those routes, so LayerHop does not quote them yet. - **Transactions:** `evm`, often with an ERC-20 `approval`. - **Quote lifetime:** 30 s. - **LayerHop fee:** Relay app fees (bps of input), accrued in USDC. - **Tracking:** by source transaction hash or Relay request id. ## deBridge deBridge's DLN is an intent-based cross-chain network: makers fill orders on the destination chain from their own liquidity. - **Routes:** cross-chain only, between Ethereum, Arbitrum, Base, Optimism, BNB Chain, Polygon and Solana, in both directions. - **Transactions:** `evm` (with `approval` for ERC-20 input; `value` includes deBridge's fixed fee in the native coin) or `svm` for Solana sources. - **Quote lifetime:** 30 s. deBridge asks for orders to be submitted within about 30 s of building. - **LayerHop fee:** affiliate fee, up to 1000 bps, paid on the source chain. - **Tracking:** by source transaction hash (EVM hash or Solana signature) or DLN order id. Fills commonly land in seconds. ## 0x EVM DEX aggregator (Swap API v2, AllowanceHolder flow). - **Routes:** same-chain swaps on the supported EVM chains. - **Requires:** the API operator's `ZEROX_API_KEY`. Without it, 0x reports `UNAUTHORIZED` in `errors`. - **Transactions:** `evm`; ERC-20 sells carry an `approval` whose spender is 0x's AllowanceHolder contract (not `to`). - **Quote lifetime:** 30 s. - **LayerHop fee:** 0x swap fee in the sell token, up to 1000 bps. 0x may also charge its own fee on some pairs, already reflected in the output. - **Tracking:** nothing to track off-chain; status is always `unknown`. ## LI.FI A bridge and DEX aggregator. LayerHop uses it as a **fallback** because it adds its own fee on top, so it tends to win only when nothing else routes the pair well. - **Routes:** every pair of supported chains, including same-chain and Solana source. - **Transactions:** `evm` (with `approval` when needed) or `svm`. - **Quote lifetime:** 60 s. - **LayerHop fee:** LI.FI integrator fee, only once the integrator is registered with LI.FI; until then `ourFeeBps: 0`. - **Tracking:** by source transaction hash. ## Mayan > **Coming soon:** Mayan (Solana ↔ EVM via Swift / MCTP). Quoting is implemented but transaction building is not, so Mayan is disabled and never appears in quotes. ## Not available - **Odos** shut down its service on 2026-07-30 and is not loaded. A second EVM same-chain source (KyberSwap or 1inch) is planned. - **`mock`** is a deterministic test provider, only present when an API runs with `LAYERHOP_ENABLE_MOCK=1`. Its transactions are dummies; never broadcast them. > [!NOTE] > Provider limits and fee terms change. LayerHop reports what each quote actually charges in `fees.ourFeeBps`; treat the numbers above as a guide, not a guarantee. --- # Changelog > What changed in the LayerHop API, SDK and docs, newest first. Source: https://docs.layerhop.com/changelog The API is versioned under `/v1`. Additive changes (new fields, endpoints, error codes, providers) can ship at any time; breaking changes will get a new version and advance notice. ## 0.1.0 — 2026-10-17 First public developer preview of the meta-aggregator. **API (`services/api`)** - `GET /v1/health`, `GET /v1/chains`, `GET /v1/tokens?chain=`. - `GET /v1/quote` fans out to every eligible provider in parallel and ranks by ETA-adjusted output (1 bp per minute beyond the first, max 50 bps). - `POST /v1/orders` returns an `EvmTx` (with optional ERC-20 `approval`) or a base64 Solana `SvmTx`. - `POST /v1/orders/submit` for `submitViaApi` Solana transactions (Jupiter). - `GET /v1/orders/{provider}/{id}` for order status. - Stable error codes with `requestId` and `docsUrl`; `x-request-id` on every response. - Fee channels `api` (20 bps), `web` (30), `widget` (30), `bot` (90). - OpenAPI 3.1 spec at `services/api/openapi.yaml`. **Providers** - Live: Jupiter, Relay, deBridge, 0x, LI.FI. - Mayan: quote-only, disabled. Odos: removed (service shut down). **SDK (`@layerhop/sdk`)** - `LayerHop` client with `quote`, `buildTx`, `submit`, `status`, `watch`; typed `LayerHopApiError`. **Docs** - This site: guides, concepts, API reference generated from OpenAPI, error catalog, chain and token tables generated from `@layerhop/shared`, `llms.txt` and `llms-full.txt`. ## Upcoming > **Coming soon:** hosted API at `api.layerhop.com` and the SDK on npm; API keys, rate limits and `Idempotency-Key`; webhooks; `POST /v1/simulate`; order history; `@layerhop/react`; npm release of `@layerhop/widget` (the package exists in the monorepo); Python/Go/Rust SDKs; CLI and local devnet; MCP server; testnets; Mayan; LayerHop's own intent protocol. --- # FAQ > Answers to common questions about LayerHop fees, custody, amounts, chain ids, testnets, quote expiry and order status. Source: https://docs.layerhop.com/faq ## Does LayerHop hold my users' funds? No. The user's wallet signs and sends transactions directly to the provider's contracts. LayerHop never receives funds or keys. See [Non-custodial model](/concepts/non-custodial). ## How much does LayerHop charge? 20 bps for API/SDK integrations by default (30 bps on the web app and widget, 90 bps in the Telegram bot), shown on every quote as `fees.ourFeeBps`. Some routes carry less or none — for example Jupiter routes below its 50 bps minimum. See [Fees & channels](/concepts/fees-and-channels). ## Why are amounts strings? Token amounts exceed the safe range of JavaScript numbers (1 ETH is `1000000000000000000` wei). Every amount in the API is an **integer string in base units**. Convert with the token's `decimals`: `100 USDC` → `"100000000"`. Use `BigInt` for math. ## What is chain id 7565164? Solana has no EVM-style chain id, so LayerHop uses `7565164` as Solana's internal id. You can always pass the slug `solana` instead. See [Supported chains](/chains). ## Do I need an API key? Not today. The SDK can send one (`x-api-key`) but the API does not enforce keys or rate limits yet. > **Coming soon:** API keys from a developer dashboard, with per-key usage and limits. ## Is there a testnet? > **Coming soon:** testnet support (Sepolia, Base Sepolia, Arbitrum Sepolia, Solana Devnet). The providers LayerHop aggregates quote mainnet only. For development, run the API locally with `LAYERHOP_ENABLE_MOCK=1` to get a deterministic `mock` provider — and never broadcast its dummy transactions. ## How do I run the API myself? ```bash pnpm install cp .env.example .env pnpm --filter @layerhop/api dev # http://localhost:8787/v1 ``` Useful variables: `PORT`, `CORS_ORIGINS`, `PROVIDERS` (e.g. `relay,debridge`), `PROVIDER_TIMEOUT_MS`, `LAYERHOP_ENABLE_MOCK`, the `FEE_BPS_*` channel fees, fee recipients (`FEE_RECIPIENT_EVM`, `FEE_RECIPIENT_SVM`, `JUPITER_REFERRAL_ACCOUNT`) and provider keys (`ZEROX_API_KEY` is required for 0x; `JUPITER_API_KEY`, `RELAY_API_KEY`, `LIFI_API_KEY`, `DEBRIDGE_API_KEY` are optional). ## Why did I get QUOTE_EXPIRED? Quotes live 30–60 seconds, set by the provider. Build the transaction right after the user confirms, and re-quote on a timer while they decide. See [Error handling](/guides/error-handling). ## Why is the status always "unknown" for my swap? Same-chain swaps through Jupiter and 0x settle inside one transaction, so there is nothing for the provider to track. Confirm the transaction on chain instead. For cross-chain providers, `unknown` right after sending usually means "not indexed yet" — keep polling. See [Order tracking](/concepts/order-tracking). ## Why does a cheaper route not win? Ranking uses ETA-adjusted output: each minute beyond the first costs 1 bp (max 50 bps). A marginally better but much slower route can lose to a near-instant one. All quotes are returned, so you can still offer the alternative. See [Providers & ranking](/concepts/providers-and-ranking). ## Can I swap from Solana with Relay? Not yet. Relay is used for EVM-source routes only. Solana → EVM is served by deBridge and LI.FI. ## Can I use these docs with an AI assistant? Yes. [`/llms.txt`](/llms.txt) lists every page with a one-line description, and [`/llms-full.txt`](/llms-full.txt) contains the whole site as Markdown. Every page also has **Copy page as Markdown**. ## Where do I report a problem? Include the `requestId` from the error body (or the `x-request-id` header), the endpoint, and the `quoteId` or transaction hash. --- # INVALID_PARAMS > Invalid parameters: A required parameter is missing or malformed, or the request is not allowed for this provider. Source: https://docs.layerhop.com/errors/INVALID_PARAMS | HTTP status | Retry | |---|---| | `400` | no | **Where it comes from:** `GET /v1/quote`, `POST /v1/orders`, `POST /v1/orders/submit`, `GET /v1/orders/{provider}/{id}`; also mapped from provider validation errors. ## Causes - A required query parameter of `GET /v1/quote` is missing (`srcChain`, `srcToken`, `amount`, `dstChain`, `dstToken`, `sender`, `recipient`). - `amount` is not an integer string in base units (for example `"1.5"` or `"1e6"`). - `slippageBps` is outside 1–5000, or `channel` is not one of `web`, `widget`, `bot`, `api`. - `quoteId` (or `signedTransaction`) is missing from a POST body, or the body is not JSON. - `POST /v1/orders/submit` was called for a provider whose transactions are broadcast by the wallet (only `submitViaApi` transactions can be submitted). - `GET /v1/orders/{provider}/{id}` names a provider that is not loaded in this API instance. - A provider rejected an address or slippage value (for example Relay `INVALID_RECIPIENT`, deBridge affiliate fee over its cap). ## How to fix - Read `error.message`: it lists each failing field as `field: reason`. - Convert human amounts to base units with the token's `decimals` (see [GET /v1/tokens](/api-reference#list-tokens)). - Only call submit when `tx.vm === "svm" && tx.submitViaApi`; otherwise send the transaction from the wallet. - Use a `provider` value returned by the quote you are tracking. ## Example response ```json { "error": { "code": "INVALID_PARAMS", "message": "amount: amount must be an integer string in base units", "requestId": "0d5e1f6a-3b2c-4d1e-8f9a-7b6c5d4e3f21", "docsUrl": "https://docs.layerhop.com/errors/INVALID_PARAMS" } } ``` See [Error handling & retries](/guides/error-handling) for a retry strategy, and the [error catalog](/errors) for every code. --- # UNSUPPORTED_CHAIN > Unsupported chain: The chain id or slug is not in LayerHop's chain registry, or a provider does not serve it. Source: https://docs.layerhop.com/errors/UNSUPPORTED_CHAIN | HTTP status | Retry | |---|---| | `400` | no | **Where it comes from:** `GET /v1/tokens`, `GET /v1/quote`; also mapped from provider errors (for example Relay `CHAIN_DISABLED`). ## Causes - `srcChain`, `dstChain` or `chain` is not one of the [supported chains](/chains) — for example a testnet id, or a typo in the slug. - `GET /v1/tokens` was called without `chain`. ## How to fix - Use a slug or id from [GET /v1/chains](/api-reference#list-chains). Solana is `solana` or `7565164`. - Testnets are not supported yet — see the [FAQ](/faq). ## Example response ```json { "error": { "code": "UNSUPPORTED_CHAIN", "message": "Unsupported chain: avalanche", "requestId": "0d5e1f6a-3b2c-4d1e-8f9a-7b6c5d4e3f21", "docsUrl": "https://docs.layerhop.com/errors/UNSUPPORTED_CHAIN" } } ``` See [Error handling & retries](/guides/error-handling) for a retry strategy, and the [error catalog](/errors) for every code. --- # UNSUPPORTED_TOKEN > Unsupported token: A provider does not recognise or will not route the source or destination token. Source: https://docs.layerhop.com/errors/UNSUPPORTED_TOKEN | HTTP status | Retry | |---|---| | `400` | no | **Where it comes from:** Provider errors from Jupiter, Relay, deBridge and 0x. Usually appears per provider in `QuoteResponse.errors`. ## Causes - The token address / mint does not exist on that chain, or is not tradable on the provider. - A symbol that is not on LayerHop's built-in token list was passed; unknown symbols are forwarded as if they were addresses. - The token is restricted by the provider (for example a sanctioned asset). ## How to fix - Pass the token's address / mint instead of a symbol when it is not on the [token list](/chains). - Check that the address belongs to the chain you named (USDC on Base is not USDC on Arbitrum). ## Example response ```json { "error": { "code": "UNSUPPORTED_TOKEN", "message": "jupiter: Token not tradable", "requestId": "0d5e1f6a-3b2c-4d1e-8f9a-7b6c5d4e3f21", "docsUrl": "https://docs.layerhop.com/errors/UNSUPPORTED_TOKEN" } } ``` See [Error handling & retries](/guides/error-handling) for a retry strategy, and the [error catalog](/errors) for every code. --- # AMOUNT_TOO_SMALL > Amount too small: The input amount is zero or below the provider's minimum. Source: https://docs.layerhop.com/errors/AMOUNT_TOO_SMALL | HTTP status | Retry | |---|---| | `400` | no | **Where it comes from:** `GET /v1/quote` (amount `0`); also mapped from provider errors (for example Relay `AMOUNT_TOO_LOW`). ## Causes - `amount` is `"0"`. - The amount does not cover the route's fixed costs (bridge fees, destination gas). - The amount was sent in human units instead of base units (`"1"` USDC means 0.000001 USDC). ## How to fix - Send base units: 1 USDC is `"1000000"` (6 decimals), 1 SOL is `"1000000000"` (9 decimals). - Increase the amount; cross-chain routes need more than same-chain swaps. ## Example response ```json { "error": { "code": "AMOUNT_TOO_SMALL", "message": "amount must be > 0", "requestId": "0d5e1f6a-3b2c-4d1e-8f9a-7b6c5d4e3f21", "docsUrl": "https://docs.layerhop.com/errors/AMOUNT_TOO_SMALL" } } ``` See [Error handling & retries](/guides/error-handling) for a retry strategy, and the [error catalog](/errors) for every code. --- # AMOUNT_TOO_LARGE > Amount too large: The input amount exceeds what a provider can fill. Source: https://docs.layerhop.com/errors/AMOUNT_TOO_LARGE | HTTP status | Retry | |---|---| | `400` | no | **Where it comes from:** Provider errors from Relay, Mayan and LI.FI. Usually appears per provider in `QuoteResponse.errors`. ## Causes - The provider's liquidity or per-order cap for this route is lower than the amount. ## How to fix - Use another quote from `quotes[]` if one exists. - Split the trade into smaller orders. ## Example response ```json { "error": { "code": "AMOUNT_TOO_LARGE", "message": "Relay: Amount too high", "requestId": "0d5e1f6a-3b2c-4d1e-8f9a-7b6c5d4e3f21", "docsUrl": "https://docs.layerhop.com/errors/AMOUNT_TOO_LARGE" } } ``` See [Error handling & retries](/guides/error-handling) for a retry strategy, and the [error catalog](/errors) for every code. --- # NO_ROUTE > No route: No provider supports this route, or every provider that does failed to quote. Source: https://docs.layerhop.com/errors/NO_ROUTE | HTTP status | Retry | |---|---| | `404` | with backoff | **Where it comes from:** `GET /v1/quote`. ## Causes - No loaded provider handles the pair — for example a Solana → EVM route when deBridge and LI.FI are both unavailable, or a same-token swap on one chain. - Every eligible provider failed or timed out (each provider has 8 s by default). - Liquidity is insufficient or price impact is too high (mapped from provider errors). ## How to fix - Check [Providers](/providers) for which provider covers which route. - Retry after a short delay; transient provider outages are common. - Try a different amount or a more liquid token (USDC, native coin). ## Example response ```json { "error": { "code": "NO_ROUTE", "message": "No provider returned a quote", "requestId": "0d5e1f6a-3b2c-4d1e-8f9a-7b6c5d4e3f21", "docsUrl": "https://docs.layerhop.com/errors/NO_ROUTE" } } ``` See [Error handling & retries](/guides/error-handling) for a retry strategy, and the [error catalog](/errors) for every code. --- # QUOTE_NOT_FOUND > Quote not found: The `quoteId` is unknown to this API instance, or the provider can no longer build it. Source: https://docs.layerhop.com/errors/QUOTE_NOT_FOUND | HTTP status | Retry | |---|---| | `404` | after a new quote | **Where it comes from:** `POST /v1/orders`, `POST /v1/orders/submit`. ## Causes - The id was mistyped, or came from a different API instance or environment. - The API restarted. Quotes are held in memory and dropped 5 minutes after they expire. - The provider returned no transaction data with the quote (deBridge, LI.FI, Relay ask you to re-quote). ## How to fix - Request a new quote and build it straight away. ## Example response ```json { "error": { "code": "QUOTE_NOT_FOUND", "message": "Unknown quoteId", "requestId": "0d5e1f6a-3b2c-4d1e-8f9a-7b6c5d4e3f21", "docsUrl": "https://docs.layerhop.com/errors/QUOTE_NOT_FOUND" } } ``` See [Error handling & retries](/guides/error-handling) for a retry strategy, and the [error catalog](/errors) for every code. --- # QUOTE_EXPIRED > Quote expired: The quote is past its `expiresAt` and can no longer be built. Source: https://docs.layerhop.com/errors/QUOTE_EXPIRED | HTTP status | Retry | |---|---| | `410` | after a new quote | **Where it comes from:** `POST /v1/orders`. ## Causes - More time passed between `GET /v1/quote` and `POST /v1/orders` than the quote's lifetime (typically 30–60 s, set by the provider). ## How to fix - Request a new quote. Build the transaction as soon as the user confirms, and refresh quotes on a timer while the user decides. - Compare `expiresAt` (unix seconds) with the clock before building. ## Example response ```json { "error": { "code": "QUOTE_EXPIRED", "message": "Quote expired, request a new one", "requestId": "0d5e1f6a-3b2c-4d1e-8f9a-7b6c5d4e3f21", "docsUrl": "https://docs.layerhop.com/errors/QUOTE_EXPIRED" } } ``` See [Error handling & retries](/guides/error-handling) for a retry strategy, and the [error catalog](/errors) for every code. --- # ORDER_NOT_FOUND > Order not found: The provider does not know the order id or transaction hash. Source: https://docs.layerhop.com/errors/ORDER_NOT_FOUND | HTTP status | Retry | |---|---| | `404` | with backoff | **Where it comes from:** `GET /v1/orders/{provider}/{id}` (mapped from deBridge `UNKNOWN_ORDER`). ## Causes - The id was never an order on that provider. - The wrong `provider` was used for the id. ## How to fix - Use the `provider` of the quote you executed and the id described in [Order tracking](/concepts/order-tracking). - Right after broadcasting, most providers return `pending` or `unknown` until they index the source transaction — keep polling. ## Example response ```json { "error": { "code": "ORDER_NOT_FOUND", "message": "deBridge: order not found", "requestId": "0d5e1f6a-3b2c-4d1e-8f9a-7b6c5d4e3f21", "docsUrl": "https://docs.layerhop.com/errors/ORDER_NOT_FOUND" } } ``` See [Error handling & retries](/guides/error-handling) for a retry strategy, and the [error catalog](/errors) for every code. --- # PROVIDER_ERROR > Provider error: An upstream liquidity provider returned an error or an unusable response. Source: https://docs.layerhop.com/errors/PROVIDER_ERROR | HTTP status | Retry | |---|---| | `502` | with backoff | **Where it comes from:** Any endpoint that calls a provider. Per provider in `QuoteResponse.errors`; top-level for `POST /v1/orders`, `POST /v1/orders/submit` and status. ## Causes - The provider's API returned a 4xx/5xx that has no more specific mapping. - Network failure between LayerHop and the provider. - For `POST /v1/orders/submit`: Jupiter refused to execute the signed transaction (for example the quote's price moved). ## How to fix - Retry with exponential backoff. - For submit failures, request a new quote and sign again. - Quote `requestId` when reporting a persistent failure. ## Example response ```json { "error": { "code": "PROVIDER_ERROR", "message": "api.relay.link 500", "requestId": "0d5e1f6a-3b2c-4d1e-8f9a-7b6c5d4e3f21", "docsUrl": "https://docs.layerhop.com/errors/PROVIDER_ERROR" } } ``` See [Error handling & retries](/guides/error-handling) for a retry strategy, and the [error catalog](/errors) for every code. --- # PROVIDER_TIMEOUT > Provider timeout: An upstream provider did not answer in time. Source: https://docs.layerhop.com/errors/PROVIDER_TIMEOUT | HTTP status | Retry | |---|---| | `504` | with backoff | **Where it comes from:** Any endpoint that calls a provider (8 s default timeout, 30 s for Jupiter execute). ## Causes - The provider is slow or degraded. ## How to fix - Retry with backoff. In `GET /v1/quote` other providers usually still answer. ## Example response ```json { "error": { "code": "PROVIDER_TIMEOUT", "message": "Timed out: li.quest", "requestId": "0d5e1f6a-3b2c-4d1e-8f9a-7b6c5d4e3f21", "docsUrl": "https://docs.layerhop.com/errors/PROVIDER_TIMEOUT" } } ``` See [Error handling & retries](/guides/error-handling) for a retry strategy, and the [error catalog](/errors) for every code. --- # RATE_LIMITED > Rate limited: An upstream provider rate-limited LayerHop. Source: https://docs.layerhop.com/errors/RATE_LIMITED | HTTP status | Retry | |---|---| | `429` | with backoff | **Where it comes from:** Any endpoint that calls a provider (HTTP 429 from the provider). LayerHop itself does not rate-limit yet. ## Causes - Too many requests to a provider from this API instance — for example Jupiter's keyless limit. ## How to fix - Back off and retry. - Operators: configure the provider's API key (see the [FAQ](/faq#how-do-i-run-the-api-myself)). ## Example response ```json { "error": { "code": "RATE_LIMITED", "message": "li.quest 429", "requestId": "0d5e1f6a-3b2c-4d1e-8f9a-7b6c5d4e3f21", "docsUrl": "https://docs.layerhop.com/errors/RATE_LIMITED" } } ``` See [Error handling & retries](/guides/error-handling) for a retry strategy, and the [error catalog](/errors) for every code. --- # UNAUTHORIZED > Unauthorized: A provider rejected LayerHop's credentials. Source: https://docs.layerhop.com/errors/UNAUTHORIZED | HTTP status | Retry | |---|---| | `401` | no | **Where it comes from:** Provider errors from 0x, Relay and LI.FI. LayerHop API keys are not enforced yet. ## Causes - The operator's provider API key is missing or invalid (0x always needs `ZEROX_API_KEY`). ## How to fix - Operators: set or rotate the provider key in the API environment. - Integrators: report it with the `requestId`; other quotes in `quotes[]` remain usable. ## Example response ```json { "error": { "code": "UNAUTHORIZED", "message": "0x: Invalid API key", "requestId": "0d5e1f6a-3b2c-4d1e-8f9a-7b6c5d4e3f21", "docsUrl": "https://docs.layerhop.com/errors/UNAUTHORIZED" } } ``` See [Error handling & retries](/guides/error-handling) for a retry strategy, and the [error catalog](/errors) for every code. --- # INTERNAL > Internal error: An unexpected error inside LayerHop. Source: https://docs.layerhop.com/errors/INTERNAL | HTTP status | Retry | |---|---| | `500` | with backoff | **Where it comes from:** Any endpoint. ## Causes - A bug or an unhandled condition in the API. ## How to fix - Retry once. - If it persists, report it with the `requestId` (also in the `x-request-id` header). ## Example response ```json { "error": { "code": "INTERNAL", "message": "Internal error", "requestId": "0d5e1f6a-3b2c-4d1e-8f9a-7b6c5d4e3f21", "docsUrl": "https://docs.layerhop.com/errors/INTERNAL" } } ``` See [Error handling & retries](/guides/error-handling) for a retry strategy, and the [error catalog](/errors) for every code.