LP integration — Withdraw flow¶
Withdrawals require an Ed25519 signature over a network-domain-tagged payload. Every withdrawal is signature-verified server-side before it executes; the balance/nonce check runs and then USDC is moved from the vault. Withdrawals confirm automatically — no claim step.
Fogo-only destination
Withdrawals go to a Fogo wallet (USDC.s) today — set chain: "fogo" and a
base58 Fogo recipient. The signed-message layout below still carries a
chain byte (solana/ethereum/polygon are part of the wire contract for
forward-compat), but cross-chain withdraw destinations are not currently
offered — only fogo is supported.
Sign the tagged bytes
You MUST sign the tagged bytes below — a bare 81-byte (untagged) signature
fails verification with 403 invalid_signature.
Why the tag¶
A leading ASCII network tag is mixed into every cross-chain signed message (withdraw + LP delegation). It binds the signature to one network/env, so a signature minted on testnet/staging can never be replayed on mainnet/prod (and vice-versa).
| Env | Tag (ASCII, 18 bytes) |
|---|---|
| testnet / staging | parti-fogo-testnet |
| mainnet / production | parti-fogo-mainnet |
Production uses Fogo mainnet; non-production uses testnet.
Production (oracle-api.parti.com) is mainnet, so sign against the mainnet
tag.
Canonical tagged 99-byte message¶
The tag is prepended to the 81-byte body (18 + 81 = 99 bytes):
| Offset | Length | Field | Encoding |
|---|---|---|---|
[0..18) |
18 | network tag | ASCII parti-fogo-{testnet\|mainnet} |
[18..50) |
32 | user pubkey |
base58-decoded → 32 raw bytes |
[50..82) |
32 | recipient pubkey |
base58 (sol/fogo) → 32 bytes; EVM 0x… is hex-decoded and left-padded to 32 |
[82..90) |
8 | amount |
u64 little-endian, micro-USDC |
[90] |
1 | chain id |
u8: solana=0, fogo=1, ethereum=2, polygon=3 (only fogo=1 is currently supported) |
[91..99) |
8 | nonce |
u64 little-endian |
This is the exact canonical withdraw message. Both
user and recipient MUST decode to exactly 32 bytes.
const TAG = isMainnet ? 'parti-fogo-mainnet' : 'parti-fogo-testnet';
const tag = new TextEncoder().encode(TAG); // 18 bytes
const buf = new Uint8Array(tag.length + 81); // 99
let o = 0;
buf.set(tag, o); o += tag.length;
buf.set(base58ToBytes(user), o); o += 32;
buf.set(base58ToBytes(recipient), o); o += 32; // or leftPad32(hexToBytes(recip.slice(2))) for EVM
buf.set(u64LE(amountMicro), o); o += 8; // setBigUint64(..., true)
buf[o] = CHAIN_IDS[chain]; o += 1; // 0/1/2/3
buf.set(u64LE(nonce), o);
Sign buf with your Solana wallet's Ed25519 key. Signature must be exactly 64
bytes, sent as hex.
Submit — POST /v1/withdraw-signed¶
Checks run in order: trading-pause guard → session → per-IP rate limit.
{
"user": "<base58 pubkey, must equal session user>",
"amount": "25000000",
"chain": "fogo",
"recipient": "<base58 Fogo address>",
"nonce": 1750700001,
"signature": "<64-byte hex Ed25519>"
}
amountgoes over the wire as an integer string of micro-USDC to avoid float drift. The FE computesamountMicro = round(amountUsdc * 1_000_000).
Nonce rules¶
nonceis an integer chosen by the client. Use a monotonically increasing value. The FE strategy (nextWithdrawNonce) isnext = max(prevStored, Date.now_ms) + 1, persisted per-user in localStorage.- The API enforces freshness/replay protection. Reusing or regressing a nonce
is rejected (surfaced as
422 engine_rejected).
Validation order & status¶
- Schema floor →
400 invalid_shape/400 invalid_json. - Session
usermust equal bodyuser→403 session_user_mismatch. amountmust parse to a positive bigint →400 invalid_amount.- Signature: must be 64 bytes (
400 invalid_signature_format), must verify (403 invalid_signature). - Forward to the matching service: unavailable →
503 engine_binding_disabled; unreachable →502 engine_unreachable; rejected (balance/nonce) →422 { "error": "engine_rejected", "detail": "<≤200 chars>" }.
Success: the response body is passed through. On success it carries a
tx_signature (and/or ok: true) — your confirmation the vault moved funds.