Skip to content
Guides

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.

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

/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_deposit and holds nothing.
  • TRON source: the entry carries depositAddress and srcAmountSpoke, but confirm the recovered order with Khalani before funding it when the build outcome is unknown.

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 payoutAddr and refundAddr explicitly. 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 requires refundAddr. 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.

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:

  1. Send each approveTxs entry in order and check that its receipt succeeded, then send the deposit. With viem, require receipt.status === "success": waiting for a receipt alone does not rule out a revert. Usually there is one exact-amount approval. A leading approve(0) appears only when a non-zero allowance already exists, because USDT-style tokens reject changing one non-zero allowance to another.
  2. Send depositTx from depositor, exactly once. depositTx.from equals depositor. 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 the orderId. Re-send only if the first deposit never landed.
  3. Send value with the deposit. It is the relay fee, in hex wei. Sending less reverts with InsufficientValueForFee.
  4. Mind the formats. approveTxs[].value is the decimal string "0"; depositTx.value is hex. Both are wei. gasLimit is 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.

Track the order until delivery succeeds or fails. See the POST /v2/build reference for every field and status.