> For the complete documentation index, see [llms.txt](https://sodex.com/documentation/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://sodex.com/documentation/for-developers/developers/mirror-protocol/lifecycle/withdrawals.md).

# Withdrawals

Sign a `WithdrawToken` permit, submit it through the [Sponsored Withdrawal](/documentation/for-developers/api-reference/mirror-api/sponsored-withdrawal.md) API, and track external settlement. The user signs; Gateway pays ValueChain gas. This is not the [Transaction Relayer](/documentation/for-developers/developers/valuechain-evm/relayer.md). See [Mirror Protocol Lifecycle](/documentation/for-developers/developers/mirror-protocol/lifecycle.md#withdrawals) for the funding flow.

Permit and withdrawer addresses are on [Contracts](/documentation/for-developers/developers/mirror-protocol/contracts.md): `SoDexTokenCallForPermit` and `SoDexTokenWithdrawer`.

## 1. Select the route

Query [Asset Configuration](/documentation/for-developers/api-reference/mirror-api/asset-configuration.md) and pick a `chains` entry whose `custody.allowWithdraw` or `bridge.allowWithdraw` is `true`. Use that exact `chainName`.

* Coin in `cmdData` is the canonical **`assetName`** (the same string you pass as `name=`), not `valueChainMetadata.tokenSymbol` or `sodexMetadata.name`.
* Route type is `0` for custody and `1` for bridge.
* `minWithdrawAmount` and `withdrawFee` are human-readable asset units. Convert the permit amount with `valueChainMetadata.tokenDecimals`. An explicit fee `"0"` is zero; an empty fee string is unknown — do not treat it as zero.
* The requested amount must meet `minWithdrawAmount` on the selected route.

## 2. Move funds to ValueChain EVM

The withdrawal spends the user's ValueChain EVM wallet, not a Spot or Perps balance.

| Current balance | Required route                |
| --------------- | ----------------------------- |
| Perps           | Perps → Spot → ValueChain EVM |
| Spot            | Spot → ValueChain EVM         |
| ValueChain EVM  | No trading-account transfer   |

1. **Perps → Spot:** signed [Perps transfer](/documentation/for-developers/api-reference/trading-api/rest-v1/sodex-rest-perps-api.md#transfer-asset-to-spot) with `toAccountID=999` and type `SPOT_WITHDRAW`. Wait for Spot credit. Perps cannot transfer directly to EVM.
2. **Spot → ValueChain EVM:** signed [Spot transfer](/documentation/for-developers/api-reference/trading-api/rest-v1/sodex-rest-spot-api.md#transfer-asset-to-evm-or-perps) with `toAccountID=999` and type `EVM_WITHDRAW`. Transfer amounts are human-readable decimal strings, not base-unit integers.
3. Confirm the signing wallet holds at least `WITHDRAW_RAW_AMOUNT` in base units: native SOSO with [`eth_getBalance`](/documentation/for-developers/api-reference/json-rpc/eth-getbalance.md), or the ERC-20 at `valueChainMetadata.evmAddress` with `balanceOf`.

Save each transfer request ID. A successful transfer response is not EVM credit. Reconcile status and the EVM balance before retrying.

## 3. Encode the receiver

Receiver strings are chain-specific and case-sensitive. Take the address and any required memo/tag from the **destination** wallet or exchange deposit instructions. When a tag is required, set `receiver` to `${address}:${memo/tag}` before encoding `cmdData`.

Keep the tag as a string; do not parse it as a number or strip leading zeros. The tag in `receiver` is not the sixth ABI argument (route memo). Do not move it there.

```javascript
const address = process.env.DESTINATION_ADDRESS;
const tag = process.env.DESTINATION_TAG;
const tagRequired = process.env.TAG_REQUIRED === "true";
if (!address) throw new Error("Set DESTINATION_ADDRESS");
if (tagRequired && !tag) throw new Error("Destination requires a memo/tag");
const receiver = tag ? address + ":" + tag : address;
console.log(receiver);
```

Changing the address or tag changes the signed command. Rebuild and re-sign; do not edit `cmdData` after signing.

## 4. Sign the withdrawal permit

Install `viem` and `tsx`, save as `withdraw-permit.ts`, and run `npx tsx withdraw-permit.ts`. Set `USER_PK`, `WITHDRAW_COIN` (`assetName`), `WITHDRAW_CHAIN` (`chainName`), `WITHDRAW_RECEIVER`, `WITHDRAW_RAW_AMOUNT` (base units), and `WITHDRAW_ROUTE_TYPE` (`0` custody or `1` bridge). This prints the request body; it does not submit.

This flow reads `nonces(owner, 0)`. The 15-minute `deadline` is an example, not a protocol maximum. The signature authorizes this withdrawal only; do not expose it.

```typescript
import { createPublicClient, http, parseAbi, encodeAbiParameters, parseAbiParameters } from "viem";
import { privateKeyToAccount } from "viem/accounts";

async function main() {
  const account = privateKeyToAccount(process.env.USER_PK as `0x${string}`);
  const publicClient = createPublicClient({ transport: http("https://mainnet.valuechain.xyz") });
  const CALL_FOR_PERMIT_ADDRESS = "0x890B7D142841065E64E5f94a455876e6352A7801";
  const WITHDRAW_TOKEN_TARGET = "0x441BDb33C7d6DC49f627a42c3d71671D50DC2e94";
  const CALL_FOR_PERMIT_ABI = parseAbi([
    "function nonces(address owner, uint192 key) view returns (uint256)",
    "function hashCallForPermit(address to, string cmdType, bytes cmdData, uint256 nonce, uint256 deadline) view returns (bytes32)",
  ]);
  const nonceKey = 0n;
  const routeType = Number(process.env.WITHDRAW_ROUTE_TYPE ?? "0");
  const permitNonce = await publicClient.readContract({
    address: CALL_FOR_PERMIT_ADDRESS,
    abi: CALL_FOR_PERMIT_ABI,
    functionName: "nonces",
    args: [account.address, nonceKey],
  });
  const deadline = BigInt(Math.floor(Date.now() / 1000) + 15 * 60);
  const cmdData = encodeAbiParameters(
    parseAbiParameters("string, string, string, uint256, uint8, string, bool"),
    [process.env.WITHDRAW_COIN!, process.env.WITHDRAW_CHAIN!, process.env.WITHDRAW_RECEIVER!,
      BigInt(process.env.WITHDRAW_RAW_AMOUNT!), routeType, "", true],
  );
  const digest = await publicClient.readContract({
    address: CALL_FOR_PERMIT_ADDRESS,
    abi: CALL_FOR_PERMIT_ABI,
    functionName: "hashCallForPermit",
    args: [WITHDRAW_TOKEN_TARGET, "WithdrawToken", cmdData, permitNonce, deadline],
  });
  const signature = await account.sign({ hash: digest });
  console.log(JSON.stringify({
    cmdData, nonce: permitNonce.toString(), deadline: deadline.toString(), signature,
  }, null, 2));
}
main().catch(console.error);
```

Sign the contract-returned digest. Do not use `personal_sign` / `signMessage` or prepend the Trading API typed-signature byte. Field order is documented on [Sponsored Withdrawal](/documentation/for-developers/api-reference/mirror-api/sponsored-withdrawal.md#build-the-permit).

## 5. Submit the signed request

Save the JSON as `withdrawal-request.json`. Set `USER_ADDRESS` to the signing wallet. This broadcasts a Mainnet withdrawal.

```bash
curl -sS -X POST "https://mainnet-gw.sodex.dev/api/v1/user/$USER_ADDRESS/evm-withdraw" \
  -H "Content-Type: application/json" \
  --data-binary @withdrawal-request.json
```

Check envelope `code` as well as HTTP status. On success, persist `data.txHash` (the ValueChain **request** hash), `data.senderAddress`, and `data.senderNonce`. This is not external settlement. If the response is lost, reconcile [history](/documentation/for-developers/api-reference/mirror-api/history-and-status.md) before sending another withdrawal.

## 6. Track external settlement

Query [Withdrawal status](/documentation/for-developers/api-reference/mirror-api/history-and-status.md#withdrawal-status) starting with the returned ValueChain request hash. When a record appears, switch to its `withdrawId` and keep using that. Prefer `withdrawId`. The status `txHash` parameter accepts only the ValueChain request hash, not the final external-chain hash.

```bash
curl -sS --get "https://mainnet-gw.sodex.dev/api/v1/user/withdraw/status" \
  --data-urlencode "chain=$WITHDRAW_CHAIN" \
  --data-urlencode "txHash=$TX_HASH" \
  -H "Accept: application/json"
```

After `withdrawId` is known:

```bash
curl -sS --get "https://mainnet-gw.sodex.dev/api/v1/user/withdraw/status" \
  --data-urlencode "chain=$WITHDRAW_CHAIN" \
  --data-urlencode "withdrawId=$WITHDRAW_ID" \
  -H "Accept: application/json"
```

An empty result means no matching record yet, not success. If `chain` does not match the record, the API also returns empty. Poll the same identifier; on timeout, resume — do not submit again.

Inspect `status`, `failCode`, `failReason`, `withdrawFee`, and `originTxHash` in [History & Status](/documentation/for-developers/api-reference/mirror-api/history-and-status.md). Confirm the external destination and amount before treating the withdrawal as complete.

To list in-flight withdrawals, query history:

```bash
curl -sS "https://mainnet-gw.sodex.dev/api/v1/user/$USER_ADDRESS/deposit-withdrawals?side=withdraw&pending=true&limit=10" \
  -H "Accept: application/json"
```
