Skip to main content
Withdrawal 2FA adds a time-based one-time-password (TOTP) second factor on top of the EIP-712 withdrawal signature. It is an opt-in, per-wallet control: once a wallet activates it, every subsequent withdrawal must carry a valid code. The same endpoints serve the frontend (Privy JWT) and API-key integrators, so a partner arms 2FA here and then passes the code in the withdraw request.
Opt-in. A wallet with no active 2FA can withdraw without a code (subject to the address whitelist and AML checks). 2FA and the whitelist are independent — neither implies the other.

When it is enforced

The withdraw-time gate runs only when the platform has withdrawal-security enforcement enabled (a server-side flag, off by default during rollout) and the caller is not a partner_sso integration (SSO partners are exempt — they carry their own KYB-level controls). Setup/management endpoints below are always available so a wallet can arm 2FA ahead of enforcement.

Endpoints

Method & pathScopeRate limitPurpose
GET /api/me/withdrawal-security/totp/statusportfolio:read60/minIs 2FA active?
POST /api/me/withdrawal-security/totp/setupvault:write10/minBegin setup — returns secret + backup codes once
POST /api/me/withdrawal-security/totp/confirmvault:write5/minActivate with a code
POST /api/me/withdrawal-security/totp/disablevault:write5/minRemove 2FA (step-up)
For multi_wallet API keys every call needs the X-User-Wallet header naming the acting wallet; reads need portfolio:read, all mutations need vault:write.

Setup flow

1

Begin setup

POST /api/me/withdrawal-security/totp/setup returns the provisioning URI, the base32 secret, and ten one-time backup codes — shown once, never again. The secret is stored pending (not yet active).
Response
{
  "otpauthUri": "otpauth://totp/PredictStreet:0x1234…?secret=JBSWY3DPEHPK3PXP&issuer=PredictStreet",
  "secret": "JBSWY3DPEHPK3PXP",
  "backupCodes": ["a1b2c3d4e5f60718", "…(10 total)…"]
}
Render otpauthUri as a QR code for the user’s authenticator app, or show secret for manual entry. Persist the backup codes somewhere safe — each authorises exactly one withdrawal if the authenticator is lost.
2

Confirm

POST /api/me/withdrawal-security/totp/confirm with the current 6-digit code from the app activates 2FA. Backup codes are not accepted here — you confirm with the authenticator you just scanned.
Body
{ "code": "123456" }
Returns { "configured": true }. A wrong code → 403 totp_invalid; if there is no pending setup → 403 totp_setup_not_pending.
Re-running setup while 2FA is already active is refused with 409 totp_already_configured — disable first (a hijacked session must not be able to silently rotate the secret).

Using 2FA at withdraw time

When the wallet has 2FA active, include totpCode in the withdraw request (POST /api/withdrawals/request):
{
  "amount": "100",
  "destination": "0x…",
  "salt": "<decimal-uint256>",
  "expiry": 1729511712,
  "userSig": "0x…",
  "totpCode": "123456"
}
totpCode is a 6-digit TOTP or a 16-hex backup code. It is verified only after every other compliance check passes, so a single-use code is never burned on a withdrawal that would have been rejected anyway. Codes are single-use:
  • A matched TOTP time-step is recorded, so the same code (or its clock-drift twin) cannot be replayed on a second withdrawal — even under a concurrent double-submit.
  • A backup code is consumed on first use.

Gate errors (403)

CodeWhen
totp_required2FA is active but totpCode was omitted
totp_invalidCode wrong, expired, or already used (replay)
totp_not_configured2FA is not active on this wallet
All are 403 (never 401) — the session is authenticated; only the second factor failed. A 401 would be read by the frontend as a dead session and log the user out for a mistyped code.

Disabling

POST /api/me/withdrawal-security/totp/disable deletes 2FA but is a step-up: it requires a valid current TOTP or backup code, so a hijacked session cannot silently remove the factor. After disabling, withdrawals are blocked until 2FA is set up again from scratch.
Body
{ "code": "123456" }

API-key callers

A multi_wallet partner provisions 2FA per end-user wallet via X-User-Wallet
  • vault:write. The acting wallet must already exist on the platform — on the very first request for a freshly-seen wallet, setup may return 409 account_provisioning while partner-backed onboarding finishes writing the user record. This is transient: retry after a moment (it converges in seconds, the same as the first order/vault call for a new wallet).

Next

Address whitelist

Restrict withdrawals to pre-approved destinations.

Withdrawals overview

The full dual-sig withdrawal flow.

Error codes

Every withdrawal-security code in one table.

API reference

Withdrawal Security endpoints.