Skip to main content
A deposit moves USDC from your associated EOA into your on-chain vault, where it becomes spendable as available balance and is required collateral for placing BUY orders. It is always authorised by the EOA owner — neither the platform nor a partner API key can move funds out of an EOA without the owner’s authorisation, whether that’s a user-sent depositERC20 transaction or a signed EIP-2612 permit relayed on the user’s behalf. There are two ways to deposit:
  • Direct (self-paid) — the EOA approves the vault once, then sends depositERC20 itself. The user pays gas. Walkthrough below.
  • Gasless (relayed permit) — the user signs an EIP-2612 USDC.permit off-chain and the platform broadcasts the deposit; no gas, no separate approve. See Gasless deposit.

Prerequisites

RequirementHow to satisfy
Vault deployed for your EOAAuto-deployed by the platform — see Backend auto-deploy. Verify with GET /api/me/vault (deployed: true).
USDC in the EOABuy / bridge on mainnet; on testnet call MockCollateral.mint(eoa, amount) (public).
ERC-20 allowanceYour EOA must have called USDC.approve(vault, X) for at least X = depositAmount. MaxUint256 is the standard one-time pattern.
Within deposit limitsThe platform pre-initialises per-EOA daily / weekly / monthly caps — see Deposit limits.

End-to-end flow

Step 1 — approve (once per vault)

import { Contract, MaxUint256 } from 'ethers';

const usdc = new Contract(
  '0x979696A1B62d4c0F0390124447c065798ee4c70c',
  ['function approve(address,uint256) returns (bool)'],
  eoa,
);

await (await usdc.approve(vault, MaxUint256, { gasLimit: 100_000n })).wait();
MaxUint256 saves gas on every future deposit. Tighten to a per-deposit allowance if your security model requires it — the trade-off is one extra approve per deposit.

Step 2 — depositERC20

import { Contract, parseUnits } from 'ethers';

const v = new Contract(
  vault,
  ['function depositERC20(address token, uint256 amount)'],
  eoa,
);

await (await v.depositERC20(
  '0x979696A1B62d4c0F0390124447c065798ee4c70c',
  parseUnits('500', 6),                          // 500 USDC (6 decimals)
  { gasLimit: 600_000n },
)).wait();
Inside the tx the vault calls transferFrom(eoa → vault), increments its internal collateral ledger by amount, and checks the deposit-cap windows in DepositLimitRegistry before settling. If the cap would be exceeded, the tx reverts with DepositCapExceeded() — see Deposit limits.

Step 3 — wait for the platform to credit

After the deposit tx mines, the platform indexes the on-chain event and credits exchange.balances.available for your associated EOA. End-to-end latency from mined tx to credited balance is typically well under 1 second on testnet (chain-watcher streams events sub-second) and a few seconds on mainnet (indexer batches confirmations into the ledger). Plan UI for the mainnet ceiling — the testnet path is faster than partners typically expect, so build for the slower target and treat the testnet behaviour as the optimistic case. Two ways to observe it:
async function waitForDeposit(amountUsdc: string, timeoutMs = 30_000) {
  const start = Date.now();
  while (Date.now() - start < timeoutMs) {
    const r = await fetch(`${BASE}/api/me/balances`, {
      headers: { 'X-Api-Key': process.env.PS_API_KEY! },
    });
    const balances = await r.json();
    const usdc = balances.find((b: any) => b.token === 'USDC');
    if (usdc && Number(usdc.available) >= Number(amountUsdc)) return usdc;
    await new Promise(r => setTimeout(r, 1000));
  }
  throw new Error('deposit not credited within timeout');
}

Gasless deposit (relayed permit)

Retail / embedded-wallet users — and partners holding the vault:write scope — can deposit without native gas and without a separate approve. Instead of sending depositERC20 yourself, you sign an EIP-2612 USDC.permit off-chain and the platform broadcasts the on-chain deposit for you.
  1. Read multicallExecutor from GET /api/platform/contracts and the user’s USDC.nonces(owner) from chain.
  2. Build the Permit TypedData — domain is the deployed USDC contract (name, version, chainId, verifyingContract = USDC address); message is { owner, spender: multicallExecutor, value: amount, nonce, deadline }. Sign with eth_signTypedData_v4.
  3. POST /api/deposits/relay with the raw amount (6-decimal units), permitNonce, a permitDeadline that leaves the relayer a comfortable buffer, and the split signature (v, r, s). Response: { auditId, jobId, status }.
  4. Poll GET /api/deposits/relay/{auditId} until status: "CONFIRMED" (PENDING_SCREENING → SUBMITTED → CONFIRMED; terminal states REJECTED / REVERTED / EXPIRED).
The permit signature is the authorization — the backend ecrecovers it and requires the signer to equal the authenticated wallet, so no on-chain approve is needed and a partner cannot forge a user’s deposit. Common rejections: permit_deadline_passed, permit_deadline_too_soon, permit_nonce_mismatch, permit_signer_mismatch, tier_cap_exceeded, restriction_active. The auditId is returned even on failure so you can reference it in support tickets.
Full request / response schema: see the Deposits section of the API Reference.

Idempotency and replays

Deposits are uniquely keyed by their on-chain (txHash, logIndex). If you receive the same vault.deposit_confirmed event twice (network retry, WS reconnect with replay), the platform credits the deposit exactly once — replays are cheap no-ops. Safe to handle naively.

Withdrawing later

Funds in the vault stay liquid — withdraw any time via the dual-signed flow documented in Withdrawals overview. The vault also exposes a 7-day-timelocked emergency withdraw for recovery if the platform is unavailable; see Vault contract reference.

Common failures

SymptomCauseFix
ERC20InsufficientAllowanceStep 1 skipped or insufficient capRun usdc.approve(vault, MaxUint256) first
Revert with selector 0x87138d5cNotInitialized() — DepositLimitRegistry init lags vault deploy (separate platform job; observed lag 30 s – 4 min). Gating on GET /api/me/vault.deployed alone is insufficient.Poll GET /api/me/deposit-limits until initialized: true before calling depositERC20.
Revert with DepositCapExceededThe deposit would exceed your daily / weekly / monthly capSee Deposit limits — wait for the rolling window or request a higher tier
Tx mined but available still 0 in /api/me/balances after >1 minuteIndexer lag or transient incidentCheck status page; if persistent, contact support with txHash

Next

Deposit limits

Daily / weekly / monthly caps and how rolling windows are applied.

Withdrawals overview

Dual-signature flow, AML review, state machine.

Place your first order

Sign + POST a vault-backed order against any open market.

Vault contract reference

Full ABI for the vault, including the deposit entry point.