Skip to main content

CEX Lane (Exchange-to-Exchange Transfer)

What is a CEX Lane?

A CEX (Centralized Exchange) Lane is a 3-step pipeline for sending coins from Exchange A to Exchange B. Think of it like a bank wire transfer:
  1. Withdrawal request: “Please send $1,000 from Bank A to Bank B” (withdrawal action)
  2. Withdrawal confirmation: Bank A says “Transfer processed, reference TXN-1234” (withdrawal observe)
  3. Deposit confirmation: Bank B confirms “$1,000 received” (deposit observe)
The most battle-tested combination is the Upbit ↔ other exchange path.

3-Node Flow

Data propagation between nodes (provider_context)

Each node’s output becomes the next node’s input — like passing a relay baton:

Node 1: Withdrawal Action

The executor responsible for submitting a withdrawal request. Registered under the key exec.cex.withdrawal_action.

What preflight checks

Why the whitelist check comes first: if the destination address is wrong, there is no way to recover the funds. That is why it is the strictest check and runs first. Both destination_address and destination_memo must match (when a memo is present).
In code:

What submit does

Calls the exchange API to actually execute the withdrawal:
The exchange_withdrawal_id in the return value is the key piece of data — it gets propagated to the next node (Withdrawal Observe).

Error handling

Timeout = UNKNOWN is the key point: if a withdrawal API call times out, the call may have been received by the exchange but the response was lost. In that case UNKNOWN is returned instead of FAILED. Later, recover() determines the real outcome by checking whether an exchange_withdrawal_id exists.

Node 2: Withdrawal Observe

The executor that periodically checks the withdrawal status. Registered under the key exec.cex.withdrawal_observe.

Preflight

Simple — just verifies that exchange_withdrawal_id is present in the provider_context:

Observe (core logic)

Queries the exchange API for withdrawal status and branches based on the result:
State mapping:
Why txid only appears at COMPLETED: the blockchain transaction ID (txid) is not finalized until the exchange has actually completed the on-chain transfer. Before that point, the transaction is still being processed internally by the exchange, so no txid is available yet.

Recover

recover() simply calls observe() again:
Querying withdrawal status is idempotent — calling it multiple times causes no issues.

Node 3: Deposit Observe

The executor that confirms the deposit has been credited at the destination exchange. Registered under the key exec.cex.deposit_observe.

Why txid matching is necessary

Multiple parties deposit to an exchange simultaneously: the destination exchange’s deposit list contains deposits from other users mixed in with yours. That is why exact txid matching is required to confirm “this is the deposit we sent.”
Why normalize_txid is needed: different exchanges may return txids in different formats. One exchange might return lowercase while another returns uppercase. Normalizing with strip().lower() ensures accurate matching.

Deposit states

CREDITED vs ACCEPTED: different exchanges use different names for a completed deposit (CREDITED, ACCEPTED, DEPOSIT_ACCEPTED, etc.). The code handles all of these variants because exchange response formats differ.

Error Handling and Route Pause

Error scenario overview

When Route Auto-Pause triggers: when certain errors (wallet maintenance, network unavailable, etc.) recur, the State Machine automatically pauses the affected route. The reason is straightforward — repeating the same failure is pointless and risky.A paused route requires an operator to explicitly resume it. There is no automatic recovery — a human must assess the situation and make a decision.

CEX Lane full error flow