Withdraw
DF withdrawals are a two-step flow: the backend signs an EIP-712 release authorization, then the user submits the signed message on-chain to pull funds from the vault.
1. POST /spot/withdraw/request
───────────────▶ Backend
│
▼ (atomic)
spot_balances: available -= amount, frozen += amount
spot_withdrawals: status = "signed", store signature + nonce
│
▼ returns { signature, nonce, deadline, vault_address, ... }
2. User submits on-chain
user.wallet.signAndSend(ZtdxSpotVault.withdraw(token, amount, deadline, signature))
│
▼ vault verifies signature, transfers tokens
SpotWithdrawal event emitted
│
▼ backend listener (≥ 20 confirmations)
spot_withdrawals.status = "confirmed"
spot_balances.frozen -= amount
If the user does not submit on-chain within SPOT_WITHDRAW_NONCE_TTL_SECS (currently 24 h), a reaper task marks the row expired and returns the frozen funds to available. The signature is then unusable on-chain (deadline has passed).
Status Values
status | Meaning |
|---|---|
signed | Signature issued by the backend; funds are frozen. Awaiting on-chain submission. |
confirmed | On-chain SpotWithdrawal observed. Funds left the vault. |
expired | Deadline elapsed before submission. Funds returned to available. |
Request a Withdrawal Signature
Reserves the funds, signs the release authorization, and returns the data the user needs to call the vault.
JWT only. API-key callers receive 403 API Key permission denied.
Authorization: Bearer <JWT>
POST /spot/withdraw/request
Request Body
{ "token": "DF", "amount": "100" }
| Field | Type | Required | Description |
|---|---|---|---|
token | string | Yes | MVP only supports DF. |
amount | string / number | Yes | Decimal amount. Must be ≥ SPOT_WITHDRAW_MIN_AMOUNT_DF (currently 1). |
Response — 200 OK
{
"id": "9f2a-...-uuid",
"nonce": 42,
"signature": "0x...65 bytes hex...",
"deadline": 1778402000,
"vault_address": "0x4Fe0b354c5865ee9deb979a99030d757ae47664a",
"chain_id": 97,
"amount": "100"
}
| Field | Type | Description |
|---|---|---|
id | string | UUID of the withdrawal row (use with GET /spot/withdrawals/:id). |
nonce | number | Per-(user, chain) monotonic nonce to prevent replay. |
signature | string | 65-byte 0x-prefixed EIP-712 signature. Pass to withdraw() verbatim. |
deadline | number | Unix seconds. The on-chain call must execute strictly before this. |
vault_address | string | Contract to call withdraw() on. |
chain_id | number | Network the signature targets (97 = BSC Testnet). |
amount | string | Echoes the requested amount. |
Error Responses
| HTTP | error | Cause |
|---|---|---|
400 | unsupported token: <symbol> | Token other than DF. |
400 | amount below minimum 1 | Below SPOT_WITHDRAW_MIN_AMOUNT_DF. |
400 | insufficient balance | Spot available < amount. |
400 | invalid user address | Authenticated address fails parsing (should not happen in practice). |
403 | API Key permission denied: withdraws not allowed | Caller authenticated via API Key. |
503 | spot subsystem disabled | Server has SPOT_ENABLED=false. |
500 | internal / sign failed / signer unavailable | Server error. |
On-Chain Submission
The user calls the vault's withdraw method with the data returned above:
function withdraw(
address token, // DF token address
uint256 amount, // wei (DF = 18 decimals)
uint256 deadline, // from response, unix seconds
bytes calldata signature
) external;
The vault verifies the signature against the EIP-712 typed data:
SpotReleaseFunds(address account, address token, uint256 value, uint256 nonce, uint256 deadline)
with the domain ("ZTDX Spot Vault", "1", chainId=97, verifyingContract=vaultAddress). The account field equals the calling msg.sender, so a third party cannot replay the signature for a different recipient.
List My Withdrawals
GET /spot/withdrawals
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
status | string | (all) | Filter: signed / confirmed / expired. |
limit | number | 50 | 1–200; clamped server-side. |
Response — 200 OK
[
{
"id": "9f2a-...-uuid",
"token": "DF",
"amount": "100",
"fee": "0",
"chain_id": 97,
"nonce": 42,
"status": "confirmed",
"deadline": 1778402000,
"tx_hash": "0xabc...",
"block_number": 106400000,
"requested_at": 1778315530,
"confirmed_at": 1778316040
}
]
| Field | Type | Description |
|---|---|---|
id | string | Withdrawal UUID. |
token | string | Always DF in MVP. |
amount | string | Requested amount. |
fee | string | Withdrawal fee (currently 0). |
chain_id | number | Target chain. |
nonce | number | EIP-712 nonce. |
status | string | signed / confirmed / expired. |
deadline | number | Unix seconds — signature validity. |
tx_hash | string | null | Set once status = confirmed. |
block_number | number | null | Set once status = confirmed. |
requested_at | number | Unix seconds — backend signed the message. |
confirmed_at | number | null | Unix seconds — listener observed the on-chain event. |
Get One Withdrawal
GET /spot/withdrawals/:id
Returns the same WithdrawalView shape as above, scoped to the authenticated user's row. Returns 404 not found if the id does not belong to the caller.
Code Example
import requests
BASE = "https://api-sepolia.p99.world/api/v1"
JWT = "your_jwt_token"
# 1. Reserve + sign
sig = requests.post(
f"{BASE}/spot/withdraw/request",
headers={"Authorization": f"Bearer {JWT}", "Content-Type": "application/json"},
json={"token": "DF", "amount": "100"},
).json()
print(sig)
# { "id": "...", "nonce": 42, "signature": "0x...", "deadline": 1778402000, ... }
# 2. Submit on-chain (pseudo-code; use ethers/web3.py)
# vault.withdraw(DF_TOKEN, parse_wei(sig["amount"], 18), sig["deadline"], sig["signature"])
# 3. Poll status
import time
for _ in range(60):
row = requests.get(
f"{BASE}/spot/withdrawals/{sig['id']}",
headers={"Authorization": f"Bearer {JWT}"},
).json()
print(row["status"])
if row["status"] in ("confirmed", "expired"):
break
time.sleep(5)
Operational Notes
- Each user has a per-chain monotonic nonce. Concurrent withdraws from the same user serialize on the spot-balance row lock (
FOR UPDATE). feeis currently always0. The schema reserves the column for a future fee policy.- Native-token (BNB) withdrawals are not supported in MVP.