Deposits & Withdrawals
Collateral lives in Sparky's on-chain Vault contract on each supported chain. Deposits are plain contract calls that the backend observes; withdrawals require a backend-issued EIP-712 signature so the Vault can verify the amount against your off-chain balance.
All endpoints on this page are under /api/v1 and require Authorization: Bearer <JWT> from the EIP-712 login. API-key sessions cannot withdraw (403 Forbidden).
Amounts
| Context | Format | Example |
|---|---|---|
| REST request / response | Human-readable decimal string | "100.5" |
| Contract calls | Integer in token units (USDT has 6 decimals) | 100500000 |
on-chain units = REST amount × 10^6
Only USDT is accepted as collateral today.
Deposit
Client
├─① POST /api/v1/deposit/prepare → Vault address + token address
├─② ERC-20 approve(vault_address, amount) (on-chain)
├─③ Vault.deposit(amount, referralCode) (on-chain) → emits Deposit
│ └─ backend indexer credits `available`
└─④ GET /api/v1/deposit/history → confirm it landed
1. Prepare
POST /api/v1/deposit/prepare
Content-Type: application/json
{ "token": "USDT", "amount": "100" }
| Field | Type | Required | Notes |
|---|---|---|---|
token | string | yes | Only "USDT" |
amount | string | yes | Human-readable USDT |
Response — 200
{
"contract_address": "0xVaultContractAddress",
"token_address": "0xUSDTContractAddress",
"amount": "100",
"estimated_gas": 120000
}
2 – 3. On-chain calls
const amountWei = BigInt(Math.floor(parseFloat(amount) * 1e6));
const usdt = new ethers.Contract(token_address, ERC20_ABI, signer);
await (await usdt.approve(contract_address, amountWei)).wait();
// referralCode: bytes32(0) when you have none
const vault = new ethers.Contract(contract_address, VAULT_ABI, signer);
await (await vault.deposit(amountWei, ethers.ZeroHash)).wait();
The backend polls chain events roughly every block (about 12 s cadence); the balance appears once the Deposit event is indexed.
4. History
GET /api/v1/deposit/history
{
"deposits": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"token": "USDT",
"amount": "100.000000",
"tx_hash": "0xabc123...",
"status": "confirmed",
"created_at": 1700000000
}
]
}
Most recent 100 rows, newest first. status is confirmed once credited; created_at is Unix seconds.
Withdraw
Client
├─① POST /api/v1/withdraw/request → freezes funds, returns EIP-712 signature
├─② Vault.withdraw(user, amount, nonce, expiry, backend_signature) (on-chain, 1 h validity)
├─③ POST /api/v1/withdraw/{id}/confirm { tx_hash }
└─④ backend sees Withdraw event → frozen balance released, status = confirmed
1. Request
POST /api/v1/withdraw/request
Content-Type: application/json
{ "token": "USDT", "amount": "50" }
Response — 200
{
"withdraw_id": "550e8400-e29b-41d4-a716-446655440001",
"token": "0xUSDTContractAddress",
"amount": "50000000",
"backend_signature": "0x1234...abcd",
"nonce": 3,
"expiry": 1700003600,
"vault_address": "0xVaultContractAddress"
}
| Field | Notes |
|---|---|
amount | Already in on-chain units (× 10^6) — pass straight to the contract |
nonce | Read from Vault.withdrawNonces(user); single-use |
expiry | now + 3600 s; the signature is dead after this |
backend_signature | EIP-712 signature over Withdraw(address user,uint256 amount,uint256 nonce,uint256 deadline) |
Balance effect at request time: available -= X, frozen += X.
2. On-chain call
const vault = new ethers.Contract(vault_address, VAULT_ABI, signer);
await vault.withdraw(userAddress, amount, nonce, expiry, backend_signature);
3. Confirm
POST /api/v1/withdraw/{withdraw_id}/confirm
{ "tx_hash": "0xdef456..." }
Only valid while the record is in signed state. Moves it to submitted; the indexer moves it to confirmed when the Withdraw event lands.
Cancel
DELETE /api/v1/withdraw/{withdraw_id}/cancel
Only for signed records. Releases the frozen amount immediately (frozen -= X, available += X). Unsubmitted requests also expire automatically after 1 h (a 60 s sweeper marks them expired and releases funds).
Query
GET /api/v1/withdraw/{withdraw_id}
GET /api/v1/withdraw/history
GET /api/v1/withdraw/limit
{
"withdrawals": [
{
"id": "550e8400-...",
"token": "USDT",
"amount": "50.000000",
"nonce": 3,
"expiry": 1700003600,
"backend_signature": "0x1234...abcd",
"tx_hash": "0xdef456...",
"status": "confirmed",
"created_at": 1700000000
}
]
}
| Status | Meaning |
|---|---|
signed | Signature issued, waiting for the on-chain call (1 h) |
submitted | tx_hash received, waiting for confirmation |
confirmed | Withdraw event indexed, funds left the system |
cancelled | Cancelled by the user |
failed | On-chain transaction failed |
expired | Signature expired, funds unfrozen |
Balance model
| Field | Meaning |
|---|---|
available | Usable for new orders or withdrawals |
frozen | Locked by open-order margin or an in-flight withdrawal |
total | available + frozen |
withdrawable = available + min(unrealized_pnl, 0)
Open losses reduce what you can withdraw; the request is rejected with 422 insufficient_balance (with available, frozen, unrealized_pnl, withdrawable, requested in details) if you ask for more.
Errors
| HTTP | error | Cause |
|---|---|---|
400 | — | Bad amount, unsupported token |
401 | — | Missing / expired JWT |
403 | forbidden | API-key session attempted a withdrawal |
404 | — | Unknown withdraw id |
422 | insufficient_balance | Requested more than withdrawable |
400 | withdrawal_expired | Signature past expiry |
400 | invalid_status | Confirm / cancel on a record that is not signed |
Vault interface (relevant parts)
event Deposit(address indexed user, uint256 amount, bytes32 referralCode);
event Withdraw(address indexed user, uint256 amount, uint256 nonce);
function deposit(uint256 amount, bytes32 referralCode) external;
function withdraw(address user, uint256 amount, uint256 nonce, uint256 expiry, bytes calldata signature) external;
function getBalance(address user) external view returns (uint256);
function withdrawNonces(address user) external view returns (uint256);
Notes:
- Only one
signedwithdrawal may exist at a time; wait for confirmation or cancel before requesting another. - The indexer scans at most 1000 blocks per pass; during congestion crediting can lag.
- Each
nonceis single-use, which is what makes a stale signature unreplayable.