USDT0 Routes
USDT0 routes use the USDT0 bridge — a LayerZero OFT — to extend Harbor's swap surface across chains where Harbor doesn't have a native deposit vault.
A USDT0 route is composed of two legs:
- The USDT0 leg: USDT moves between a source and destination chain over LayerZero.
- The Harbor leg: a swap on the Harbor CLOB.
The legs run in either order, giving two integration directions:
| Direction | Example | First leg | Second leg |
|---|---|---|---|
| USDT0 → Harbor | ARB.USDT → BTC.BTC | USDT0 (ARB.USDT → ETH.USDT) | Harbor (ETH.USDT → BTC.BTC) |
| Harbor → USDT0 | BTC.BTC → ARB.USDT | Harbor (BTC.BTC → ETH.USDT) | USDT0 (ETH.USDT → ARB.USDT) |
The two directions differ in what the user signs. In USDT0 → Harbor the user broadcasts an OFT send() on the source chain. In Harbor → USDT0 the user broadcasts a standard Harbor deposit and the USDT0 leg is executed by Harbor.
Supported routes
At launch, USDT0 routes are paired with BTC.BTC on the Harbor side.
| Route | USDT0 leg | Source chain type |
|---|---|---|
ARB.USDT ↔ BTC.BTC | ARB.USDT ↔ ETH.USDT | EVM |
TRON.USDT ↔ BTC.BTC | TRON.USDT ↔ ETH.USDT | TRON |
Supported routes change as new chains and pairs are onboarded. Use the Swap API to discover routes — a successful quote means the route is live.
Architecture
- USDT0 → Harbor
- Harbor → USDT0
The user signs one transaction on the source chain. LayerZero delivers the message to the Composer on Ethereum, which calls depositWithExpiry on the Harbor router with a Harbor swap memo. From that point the flow is identical to a standard Harbor swap.
The user signs a standard Harbor deposit (BTC OP_RETURN, EVM depositWithExpiry, etc. — see Cross-chain Swaps). Harbor executes the CLOB leg, then bridges the resulting USDT to the user's destination chain via USDT0. The LayerZero messaging fee is deducted from the swap output and surfaced in the quote.
Contracts
| Contract | Role | Address |
|---|---|---|
| Composer | Receives the LayerZero message on Ethereum and calls depositWithExpiry on the Harbor Router (USDT0 → Harbor) | 0x3Bd27233114b374BEC0BBBF17F81bEF7bafcE01A |
| Harbor Executor | Signs OFT.send() from Ethereum to bridge USDT to the destination chain after the Harbor leg fills (Harbor → USDT0) | 0x9E3F3F7F3B619D33d0195ea7a51cDe1128D204be |
| USDT0 OFTs | Source and destination OFT contracts on each supported chain | USDT0 deployments |
Requesting a quote
Both directions use the standard Swap API GET /swap/v1/quote endpoint with the same parameter set described in Cross-chain Swaps.
- USDT0 → Harbor
- Harbor → USDT0
sourceAddress is required for USDT0 → Harbor quotes. Without it the response omits inboundTx and the integrator has no way to construct the OFT call.
curl -G "https://quote.harbor.xyz/swap/v1/quote" \
--data-urlencode "fromAsset=ARB.USDT" \
--data-urlencode "toAsset=BTC.BTC" \
--data-urlencode "amount=10000000000" \
--data-urlencode "destination=bc1q6csj53wdrzyzextj2syljvhh0vhynyrx904gp8" \
--data-urlencode "toleranceBps=300" \
--data-urlencode "sourceAddress=0xb38e8c17e38363af6ebdcb3dae12e0243582891d"
The response carries an extra inboundTx field describing the unsigned source-chain OFT call:
{
"expectedAmountOut": "29006947",
"fees": { "asset": "BTC.BTC", "outbound": "684", "liquidity": "23225", "total": "23909", "totalBps": "8" },
"expiry": "1768843381",
"memo": "=:b:bc1q6csj53wdrzyzextj2syljvhh0vhynyrx904gp8:29006947/300",
"inboundTx": {
"chain": "ARB",
"evmTx": {
"to": "0x14E4A1B13bf7F943c8ff7C51fb60FA964A298D92", // Source OFT
"from": "0xb38e8c17e38363af6ebdcb3dae12e0243582891d",
"data": "0xc7c7f5b3...", // send() calldata
"value": "243000000000000", // LayerZero native fee (wei)
"gas": "0x0",
"functionName": "send",
"functionSignature": "send((uint32,bytes32,uint256,uint256,bytes,bytes,bytes),(uint256,uint256),address)"
}
},
"route": {
"legs": [
{ "venue": "usdt0", "fromAsset": "ARB.USDT", "toAsset": "ETH.USDT", "fees": [...] },
{ "venue": "harbor", "fromAsset": "ETH.USDT", "toAsset": "BTC.BTC", "fees": [...] }
]
}
}
For TRON sources, inboundTx.evmTx is replaced with inboundTx.tronTx:
{
"inboundTx": {
"chain": "TRON",
"tronTx": {
"contractAddress": "TFG4wBaDQ8sHWWP1ACeSGnoNR6RRzevLPt",
"ownerAddress": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
"functionSelector": "send((uint32,bytes32,uint256,uint256,bytes,bytes,bytes),(uint256,uint256),address)",
"parameter": "0x...",
"callValue": "0",
"feeLimit": "30000000",
},
},
}
Use the standard quote request — the destination is a USDT0-supported asset:
curl -G "https://quote.harbor.xyz/swap/v1/quote" \
--data-urlencode "fromAsset=BTC.BTC" \
--data-urlencode "toAsset=ARB.USDT" \
--data-urlencode "amount=10000000" \
--data-urlencode "destination=0xb38e8c17e38363af6ebdcb3dae12e0243582891d" \
--data-urlencode "toleranceBps=300"
The response is the standard quote shape. Two things to note:
route.legs[]contains both the Harbor leg and the USDT0 outbound leg.- The LayerZero messaging fee is included in
fees.outboundand reducesexpectedAmountOut.
The inboundAddress and memo are used as in any other Harbor swap.
Try it out
Decode the calldata
Before signing, verify what the OFT contract will actually do by decoding the send() calldata returned in the quote. Paste the calldata or the full inboundTx JSON.
Broadcasting the source transaction
- USDT0 → Harbor
- Harbor → USDT0
1. Approve USDT for the OFT (one-time per wallet)
const usdt = new ethers.Contract(USDT_ADDRESS, ERC20_ABI, signer);
await usdt.approve(quote.inboundTx.evmTx.to, ethers.constants.MaxUint256);
2. Broadcast the OFT send()
Use the unsigned tx returned in inboundTx.evmTx verbatim — the calldata, the recipient, and the native value are coupled and must be submitted as one unit.
const tx = await signer.sendTransaction({
to: quote.inboundTx.evmTx.to,
data: quote.inboundTx.evmTx.data,
value: quote.inboundTx.evmTx.value,
});
const receipt = await tx.wait(1);
For TRON, use TronWeb's triggerSmartContract with the matching fields from inboundTx.tronTx.
The calldata encodes the destination Composer, the LayerZero options, the slippage-protected MinAmountLD, and the Harbor swap memo. If any of these change, the route will no longer settle. If you need to change the amount, destination, or tolerance — request a new quote.
Broadcast the deposit using the standard chain-specific procedure — see Deposit Transactions. Nothing changes from the integrator's perspective: the bridge step is invisible.
Monitoring
Use the source-chain transaction hash as the swapId for status polling:
curl "https://quote.harbor.xyz/swap/v1/tx/${TX_HASH}"
The status field cycles through queued → processing → complete (or refunded / failed). When the status reaches complete, outboundTxs[0] contains the destination-chain transaction.
Additional cross-chain observability for USDT0 routes:
| Layer | What to watch | Used in |
|---|---|---|
| Source receipt | Confirmation of your signed tx | Both directions |
| LayerZero Scan | https://layerzeroscan.com/tx/{txHash} — bridge delivery, typically 30–180s | Both directions |
| Composer event | ComposedDeposit(guid, srcEid, vault, amountLD, expiration, memo) on the destination Composer | USDT0 → Harbor |
| Router event | Deposit(to, asset, amount, memo) on the Harbor Router | USDT0 → Harbor |
| Swap API | GET /swap/v1/tx/{swapId} — Harbor leg lifecycle and outbound tx | Both directions |
For USDT0 → Harbor, Composer and Router events fire before the swap appears in the Swap API (the API is keyed off the Router Deposit). Subscribing to those events gives a faster signal than API polling.
For Harbor → USDT0, the LayerZero Scan link is the most accurate signal that the destination delivery is in flight after the Swap API reports complete.
Related
- Cross-chain Swaps — base swap lifecycle for the Harbor leg
- Memos — memo format reference
- Deposit Transactions — chain-specific deposit formatting (used in Harbor → USDT0)
- Refunds — refund behavior