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.
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.
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, lifiCheck it:
curl http://localhost:8787/v1/health
# {"ok":true,"providers":["jupiter","relay","debridge","zerox","lifi"]}Tip
Start the API with
LAYERHOP_ENABLE_MOCK=1to add a deterministicmockprovider that always quotes and always reportsfilled. It is handy for UI work and tests. Never broadcast a mock transaction — it is a dummy transaction to0x…dEaD.
Note
0x quotes need
ZEROX_API_KEYin.env; without it the 0x provider reportsUNAUTHORIZEDinerrorsand 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:
{
"dependencies": { "@layerhop/sdk": "workspace:*" }
}Coming soon
npm install @layerhop/sdkfrom the public npm registry.
Create a client:
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".
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#
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.
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.
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.
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#
- EVM → Solana guide — the full flow with error handling.
- Error handling & retries — what to do with
QUOTE_EXPIRED,NO_ROUTEand friends. - SDK reference and API reference.