Build and deposit
POST /v2/build checks the request against its quote, signs the order, stores it in pending_deposit, and returns what the user’s wallet must send. Nothing happens on-chain until the user deposits.
The request
Section titled “The request”{ ...quote.build, // from GET /v2/quote, unchanged, terms included depositor: string, // EVM address that will send the deposit payoutAddr?: string, // receives the payout; defaults to depositor refundAddr?: string // receives any refund; defaults to depositor}depositor is the address that will sign and send the deposit. It must be lowercase or exactly EIP-55 checksummed.
Build once
Section titled “Build once”/v2/build is not idempotent. Every call that succeeds creates a new order with a new orderId. Do not retry a build that returned an orderId. Persist the orderId before you hand anything to the wallet: it keys every later call.
Request validation failures happen before order creation. A 500 or 502, however, can leave a reserved TRON deposit address or an order record whose tracking did not start. An error response does not give you a usable deposit plan. Do not fund an order recovered from a failed build without confirming it with Khalani. Back off and retry at most once, then contact Khalani with the x-request-id; repeated failures can exhaust the deposit-address pool. See Errors and retries.
A call that returned no response at all may still have created an order. Look for it with GET /v2/orders?depositor=…&from=…&to=…, which lists newest first and includes pending_deposit orders. A matching record alone does not prove the build completed or tracking started.
- EVM source: the listing confirms the order exists but does not return its transactions, so you cannot deposit into it. Build again. The unfunded order stays at
pending_depositand holds nothing. - TRON source: the entry carries
depositAddressandsrcAmountSpoke, but confirm the recovered order with Khalani before funding it when the build outcome is unknown.
Recipients
Section titled “Recipients”payoutAddr defaults to the depositor’s address on the destination chain, and refundAddr to the depositor’s address on the source chain. Only a regular (EOA) wallet is sure to control the same address on every chain.
- Smart-contract wallets must pass
payoutAddrandrefundAddrexplicitly. Depositing from one does not enable self-service cancellation: the cancel endpoint accepts ECDSA signatures, not ERC-1271. - TRON as the destination requires
payoutAddr. TRON as the source requiresrefundAddr. Neither can default to an EVM depositor. - Every TRON recipient must be an activated account: one that has received TRX or a token before. An unactivated address returns
400. Fund it once before you build.
EVM source: send the transactions
Section titled “EVM source: send the transactions”The response holds an optional approveTxs list and one depositTx:
{ orderId: string, nonce: string, approveTxs?: [{ to, data, value: "0", chainId }], depositTx: { to, from, data, value, chainId, gasLimit }}The rules for the wallet:
- Send each
approveTxsentry in order and check that its receipt succeeded, then send the deposit. With viem, requirereceipt.status === "success": waiting for a receipt alone does not rule out a revert. Usually there is one exact-amount approval. A leadingapprove(0)appears only when a non-zero allowance already exists, because USDT-style tokens reject changing one non-zero allowance to another. - Send
depositTxfromdepositor, exactly once.depositTx.fromequalsdepositor. A deposit sent from any other account does not create an order, and neither does a second deposit for the same order. Those funds are held until Khalani returns them: contact Khalani with theorderId. Re-send only if the first deposit never landed. - Send
valuewith the deposit. It is the relay fee, in hex wei. Sending less reverts withInsufficientValueForFee. - Mind the formats.
approveTxs[].valueis the decimal string"0";depositTx.valueis hex. Both are wei.gasLimitis a fixed hex value, not an estimate.
With an EIP-1193 wallet, forward the fields as they are:
await provider.request({ method: "eth_sendTransaction", params: [{ from: depositTx.from, to: depositTx.to, data: depositTx.data, value: depositTx.value, gas: depositTx.gasLimit }],});depositTx carries no nonce or gas price, so the wallet can sign it again, and a reverted deposit costs only gas and leaves the order depositable. It goes stale in three ways: the order’s deadline passes, the relay fee in value rises, or the allowance it was built against changes. If any of those happens, build again.
Non-EVM source (TRON): transfer to an address
Section titled “Non-EVM source (TRON): transfer to an address”A TRON source has no deposit contract, so the response has no transactions:
{ orderId: string, nonce: string, depositAddress: string, expectedAmount: string, deadline: number }Transfer exactly expectedAmount of the source token to depositAddress, from any wallet, before deadline (unix seconds). A transfer of any other amount is rejected rather than credited. On a TRON source the order skips deposited and its deposit field stays null.
After the deposit
Section titled “After the deposit”Track the order until delivery succeeds or fails. See the POST /v2/build reference for every field and status.