# Bridging from code

On the four EVM chains you call the token itself: it is a LayerZero OFT. Out of Solana the send is an instruction to LayerZero’s OFT program, and the simplest way to build it is to ask Pangea’s API for the unsigned transaction. The signatures and events are in [For integrators](https://www.pangea.trade/app/docs#integrators); the routes they travel are in [LayerZero routes](https://www.pangea.trade/app/docs#routes).

- `amountLD` is in base units, nine decimals. The token keeps 0.2% of it, rounded down, and what crosses is rounded down to six decimals. `quoteOFT` returns both figures, exactly.
- `minAmountLD` is the least that may arrive, or the send reverts with `SlippageExceeded`. The fee is fixed, so the received amount from `quoteOFT` can be used as it is.
- `extraOptions` stays empty. The token adds its own gas floor to every send; anything you add is added on top and paid for by you.
- `to` is 32 bytes: an EVM address left-padded with zeros, or a Solana wallet’s public key. Never a Solana token account.
- `quoteSend` gives LayerZero’s fee for that exact `SendParam`. Send it as the transaction’s value, with `payInLzToken` false. Any excess goes back to the refund address.
- No approval is needed for a standard send: the token burns from your own balance.

## EVM to EVM, with viem

Quote a transfer of 1,000 PTEST4 from Base to BSC:

```ts
import { createPublicClient, http, parseAbi, pad } from "viem";
import { base } from "viem/chains";

const OFT = parseAbi([
  "struct SendParam { uint32 dstEid; bytes32 to; uint256 amountLD; uint256 minAmountLD; bytes extraOptions; bytes composeMsg; bytes oftCmd; }",
  "struct MessagingFee { uint256 nativeFee; uint256 lzTokenFee; }",
  "struct OFTLimit { uint256 minAmountLD; uint256 maxAmountLD; }",
  "struct OFTFeeDetail { int256 feeAmountLD; string description; }",
  "struct OFTReceipt { uint256 amountSentLD; uint256 amountReceivedLD; }",
  "struct MessagingReceipt { bytes32 guid; uint64 nonce; MessagingFee fee; }",
  "function quoteOFT(SendParam _sendParam) view returns (OFTLimit, OFTFeeDetail[], OFTReceipt)",
  "function quoteSend(SendParam _sendParam, bool _payInLzToken) view returns (MessagingFee)",
  "function send(SendParam _sendParam, MessagingFee _fee, address _refundAddress) payable returns (MessagingReceipt, OFTReceipt)",
]);

const TOKEN = "0x2dEC1F0FBe66ef78a7cec00cD1FD9c58345ED6Ea"; // PTEST4 on Base
const BSC = 30102;
const recipient = "0x000000000000000000000000000000000000bEEF"; // who receives on BSC
const amount = 1_000n * 10n ** 9n; // 1,000 tokens: nine decimals

const client = createPublicClient({ chain: base, transport: http(process.env.BASE_RPC) });

// What the transfer debits and what arrives: 0.2% stays with the token, the rest crosses.
const sendParam = {
  dstEid: BSC,
  to: pad(recipient), // an EVM address, left-padded to 32 bytes
  amountLD: amount,
  minAmountLD: 0n,
  extraOptions: "0x", // empty: the token already enforces the gas the destination needs
  composeMsg: "0x",
  oftCmd: "0x",
};
const [, , receipt] = await client.readContract({ address: TOKEN, abi: OFT, functionName: "quoteOFT", args: [sendParam] });
sendParam.minAmountLD = receipt.amountReceivedLD; // the least that may arrive

// LayerZero's fee for this exact transfer, in ETH.
const fee = await client.readContract({ address: TOKEN, abi: OFT, functionName: "quoteSend", args: [sendParam, false] });
console.log({ sent: receipt.amountSentLD, arrives: receipt.amountReceivedLD, nativeFee: fee.nativeFee });

// The send, from the holder's wallet, paying the fee as the value:
// await wallet.writeContract({ address: TOKEN, abi: OFT, functionName: "send",
//   args: [sendParam, fee, account.address], value: fee.nativeFee });
```

Run against Base on 19 October 2026, it printed `sent: 1000000000000n, arrives: 998000000000n, nativeFee: 109546395808232n`: 1,000 tokens sent, 998 arrive, and about 0.00011 ETH for LayerZero.

## The same, with cast

quoteOFT and quoteSend from the command line:

```bash
TOKEN=0x2dEC1F0FBe66ef78a7cec00cD1FD9c58345ED6Ea
RPC=your-base-rpc-endpoint
TO=$(cast pad --len 32 0x000000000000000000000000000000000000bEEF)

# what is sent and what arrives: (limit), (fee details), (sent, received)
cast call $TOKEN "quoteOFT((uint32,bytes32,uint256,uint256,bytes,bytes,bytes))((uint256,uint256),(int256,string)[],(uint256,uint256))" \
  "(30102,$TO,1000000000000,0,0x,0x,0x)" --rpc-url $RPC

# LayerZero's fee in wei: (nativeFee, lzTokenFee)
cast call $TOKEN "quoteSend((uint32,bytes32,uint256,uint256,bytes,bytes,bytes),bool)((uint256,uint256))" \
  "(30102,$TO,1000000000000,998000000000,0x,0x,0x)" false --rpc-url $RPC
```

> **Note:** Pad an address with `cast pad`, which pads on the left. `cast to-bytes32` pads on the right, and a send to that would go to a different address.

## EVM to Solana

The same send, with `dstEid 30168` and the recipient’s Solana wallet as the 32 bytes. The tokens arrive in that wallet’s token account, created on delivery; its rent, 2,074,080 lamports, is in every transfer’s gas floor and so in `quoteSend`’s fee.

Quote a transfer from Base to Solana, and check the route is open:

```ts
import { createPublicClient, http, parseAbi, toHex } from "viem";
import { base } from "viem/chains";
import { Connection, PublicKey } from "@solana/web3.js";

const OFT = parseAbi([
  "struct SendParam { uint32 dstEid; bytes32 to; uint256 amountLD; uint256 minAmountLD; bytes extraOptions; bytes composeMsg; bytes oftCmd; }",
  "struct MessagingFee { uint256 nativeFee; uint256 lzTokenFee; }",
  "struct OFTLimit { uint256 minAmountLD; uint256 maxAmountLD; }",
  "struct OFTFeeDetail { int256 feeAmountLD; string description; }",
  "struct OFTReceipt { uint256 amountSentLD; uint256 amountReceivedLD; }",
  "function quoteOFT(SendParam _sendParam) view returns (OFTLimit, OFTFeeDetail[], OFTReceipt)",
  "function quoteSend(SendParam _sendParam, bool _payInLzToken) view returns (MessagingFee)",
  "function peers(uint32 eid) view returns (bytes32)",
]);

const TOKEN = "0x2dEC1F0FBe66ef78a7cec00cD1FD9c58345ED6Ea"; // PTEST4 on Base
const BASE_EID = 30184;
const SOLANA = 30168;
const owner = new PublicKey("<the Solana wallet that will own the tokens>");
const amount = 1_000n * 10n ** 9n;

const evm = createPublicClient({ chain: base, transport: http(process.env.BASE_RPC) });
const solana = new Connection(process.env.SOLANA_RPC, "confirmed");

// Is this token's route from Base open on Solana? It is once LayerZero's receive config exists.
const peer = await evm.readContract({ address: TOKEN, abi: OFT, functionName: "peers", args: [SOLANA] });
const store = new PublicKey(Buffer.from(peer.slice(2), "hex")); // the OFT store
const eid = Buffer.alloc(4);
eid.writeUInt32BE(BASE_EID);
const ULN = new PublicKey("7a4WjyR8VZ7yZz5XJAKm39BUGn5iT9CKcv2pmG9tdXVH");
const [receiveConfig] = PublicKey.findProgramAddressSync([Buffer.from("ReceiveConfig"), eid, store.toBuffer()], ULN);
const open = (await solana.getAccountInfo(receiveConfig)) !== null;

const sendParam = {
  dstEid: SOLANA,
  to: toHex(owner.toBytes()), // the owner's public key as 32 bytes, not a token account
  amountLD: amount,
  minAmountLD: 0n,
  extraOptions: "0x", // the token adds the gas floor and the rent of the token account
  composeMsg: "0x",
  oftCmd: "0x",
};
const [, , receipt] = await evm.readContract({ address: TOKEN, abi: OFT, functionName: "quoteOFT", args: [sendParam] });
sendParam.minAmountLD = receipt.amountReceivedLD;
const fee = await evm.readContract({ address: TOKEN, abi: OFT, functionName: "quoteSend", args: [sendParam, false] });
console.log({ store: store.toBase58(), routeOpen: open, arrives: receipt.amountReceivedLD, nativeFee: fee.nativeFee });
// Then send exactly as from EVM to EVM. With routeOpen false, the transfer waits on Solana
// until the route is opened: see the route setup below.
```

If the token’s route from that chain is not open on Solana yet, a transfer sent now waits there until it is. To open it as part of your transfer, as the Bridge page does, `POST /api/bridge/quote` returns the setup payment in `routePayment`: send it first, then report it to `POST /api/bridge/route-open`, which opens the route or refunds the payment, and then send the transfer. [API reference →](https://www.pangea.trade/app/docs#api)

## Solana to EVM, with @solana/web3.js

Out of Solana the send is LayerZero’s OFT program’s own instruction, with about thirty accounts and a lookup table. `POST /api/bridge/solana-tx` builds it, unsigned, for your wallet to sign: it names the accounts, sets the minimum to what arrives, and adds the route’s opening when a route is used for the first time. You pay LayerZero’s fee and the transaction fee in SOL; the token’s 0.2% is kept in the OFT store’s escrow for the platform.

Price and build a transfer from Solana to Base, then simulate it:

```ts
import { Connection, VersionedTransaction } from "@solana/web3.js";

const API = "https://www.pangea.trade/api";
const sender = "<the Solana wallet holding the tokens>";
const recipient = "0x000000000000000000000000000000000000bEEF"; // who receives on Base
const amount = (1_000n * 10n ** 9n).toString(); // 1,000 tokens, in base units

const post = (path, body) =>
  fetch(API + path, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) })
    .then(async (r) => ({ status: r.status, body: await r.json() }));

// The price: what arrives on Base, and LayerZero's fee in lamports.
const quote = await post("/bridge/quote", { slug: "ptest4", from: "solana", to: "base", amount, user: recipient, solanaUser: sender });
const standard = quote.body.routes?.find((r) => r.id === "canonical");
if (!standard || standard.unavailable) throw new Error(standard?.unavailable ?? quote.body.error);
console.log({ arrives: standard.amountOut, nativeFee: standard.nativeFee, setupFee: standard.setupFee ?? null });

// The unsigned transaction: the OFT send, and the route's opening if it is not open yet.
const built = await post("/bridge/solana-tx", { slug: "ptest4", to: "base", amount, sender, recipient });
if (built.status !== 200) throw new Error(built.body.error ?? `stage ${built.body.stage}`);
const tx = VersionedTransaction.deserialize(Buffer.from(built.body.transaction, "base64"));

// Check it before signing: a simulation sends nothing.
const connection = new Connection(process.env.SOLANA_RPC, "confirmed");
const sim = await connection.simulateTransaction(tx, { sigVerify: false });
console.log({ stage: built.body.stage, received: built.body.received, totalFee: built.body.totalFee, error: sim.value.err });

// Sign with the sender's wallet and send:
// const signed = await wallet.signTransaction(tx);
// await connection.sendRawTransaction(signed.serialize());
```

Run read-only on 19 October 2026 from a Solana holder, it built a transaction at stage `send` with `received: 998000000000` and a fee of at most 636,478 lamports, and the simulation passed. The stages are `send`, the transfer alone; `together`, the route’s opening and the transfer in one transaction; and `setup`, the opening alone, after which you ask again with `after` set to its signature.

## After the send

- The send emits `OFTSent(guid, ...)` on the source chain and the delivery `OFTReceived(guid, ...)` on the destination. The guid is the transfer’s id on LayerZero Scan.
- Look the guid or the transaction up on LayerZero Scan to follow it, or poll the destination for `OFTReceived` with that guid.
- Delivery needs nothing from you. If a message sits verified and undelivered, anyone may deliver it.

Part of the Pangea documentation: https://www.pangea.trade/app/docs.md
