LayerHopdocs

Get started

Quickstart

Quote, build, sign and track your first LayerHop swap with the TypeScript SDK, viem and a Solana wallet — in under 10 minutes.

Open in ClaudeOpen in ChatGPT

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.

Terminal
git clone <layerhop-repo> 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:

Terminal
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:

TypeScript
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".

TypeScript
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#

TypeScript
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.

TypeScript
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.

TypeScript
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 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.

TypeScript
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.

Next steps#