Skip to main content
A vault is a per-user on-chain custody contract — a full, non-upgradeable Vault contract deployed by VaultFactory. Every trader who places signed orders on PredictStreet has exactly one vault, and that vault is the address that actually holds your USDC and outcome ERC-1155 tokens on chain.

Why vaults

The exchange does not custody funds for you. Three things only the vault model gives you:
  • You own the keys. Vault exposes initiateEmergencyWithdraw + emergencyWithdrawERC20/ERC1155 after a 7-day timelock — even if PredictStreet goes dark, you pull your funds out yourself.
  • Per-user isolation. A bug or compromise affecting one vault cannot drain another. There is no pooled treasury.
  • Dual-signature withdrawals. USDC leaves the vault through withdrawERC20, which requires both your EIP-712 signature and a backend co-signature, so a stolen API key alone cannot move funds out — neither replaces the vault owner’s private key that signs the EIP-712 authorisation. There is no ERC-1155 withdraw: outcome tokens leave the vault only by trading, by being merged or redeemed into USDC, or through the emergency withdraw.

How vaults connect to trading

Every order on PredictStreet is vault-backed. The struct you sign must always carry:
On-chain, CTFExchange._verifyVault enforces vaultFactory.vaultOf(signer) == maker, so the only way the maker field matches is if it is the vault VaultFactory deployed for your EOA. The platform always verifies your signature against this shape, and the exchange rejects any other signatureType. signatureType = 0 (EOA, maker = signer) is deprecated and never validates: an order signed that way recovers a different address and is rejected with bad_signature. The REST body maker may be your EOA or your vault (any other address is rejected with maker_mismatch: HTTP 401 on /api/orders/place, a per-entry rejection on /api/orders/place-batch), but either way the struct you sign must use the vault as maker. This is also why SELL needs the outcome ERC-1155 to live in the vault (not the EOA): the platform looks up positions keyed by maker = vault. Splitting USDC into a YES + NO outcome set therefore goes through vault.splitPosition(...) rather than the EOA calling ConditionalTokens directly. See Vault contract reference for the on-chain function shapes.

Lifecycle, in five lines

Two ways to get a vault

The platform deploys every vault automatically. The trigger differs by partner kind:
  • single_wallet and multi_wallet with requirePerWalletKyc=true — the back-office wallet calls VaultFactory.deployVault(eoa) as soon as the wallet has a platform account: a deploy is queued on its first sign-in and again on KYC approval, and a catch-up job queues one every minute for any account still without a vault. Vault deploy does not wait for KYC; deposits do — deposit limits are set separately, see Limits initialisation.
  • multi_wallet with requirePerWalletKyc=false (KYB-onboarded prime brokerage) — the deploy job is enqueued on the first authenticated request that names a previously-unseen sub-account in X-User-Wallet, with no retail KYC required for the sub-account.
Partners do not call VaultFactory.deployVault themselves and do not pay deploy gas. See Backend auto-deploy for the trigger detail, polling pattern, and the seven deployStatus values. VaultFactory.deployVault(eoa) is a public function callable by any address — operations can call it from a treasury wallet in a true emergency, the on-chain result is identical to auto-deploy. But this is exclusively a recovery path; partner integration code should never include it.