> ## Documentation Index
> Fetch the complete documentation index at: https://docs.testnet.dev.adipredictstreet.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Deposits overview

> How USDC moves from your associated EOA into your on-chain vault, and how long the platform takes to credit available balance.

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](#gasless-deposit-relayed-permit) relayed on the user's
behalf.

There are two ways to deposit:

* **Direct (self-paid)** — the EOA `approve`s 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](#gasless-deposit-relayed-permit).

## Prerequisites

| Requirement                 | How to satisfy                                                                                                                               |
| --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| Vault deployed for your EOA | Auto-deployed by the platform — see [Backend auto-deploy](/concepts/vaults/auto-deploy). Verify with `GET /api/me/vault` (`deployed: true`). |
| USDC in the EOA             | Buy / bridge on mainnet; on testnet call `MockCollateral.mint(eoa, amount)` (public).                                                        |
| ERC-20 allowance            | Your EOA must have called `USDC.approve(vault, X)` for at least `X = depositAmount`. `MaxUint256` is the standard one-time pattern.          |
| Within deposit limits       | The platform pre-initialises per-EOA daily / weekly / monthly caps — see [Deposit limits](/concepts/deposits/limits).                        |

## End-to-end flow

```mermaid theme={null}
sequenceDiagram
    participant E as EOA
    participant V as Vault contract
    participant USDC as USDC ERC-20
    participant CORE as core-api

    E->>USDC: approve(vault, MaxUint256)        (one-time)
    Note over E: skip if already approved
    E->>V: depositERC20(USDC, amount)
    V->>USDC: transferFrom(eoa → vault, amount)
    V-->>V: ledger += amount; cap-check passes
    V-->>CORE: DepositedERC20 event indexed
    Note over CORE: ~1 block lag
    CORE-->>E: GET /api/me/balances → available += amount
```

## Step 1 — `approve` (once per vault)

```typescript theme={null}
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`

```typescript theme={null}
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](/concepts/deposits/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:

<CodeGroup>
  ```typescript Polling theme={null}
  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');
  }
  ```

  ```text Polling fallback theme={null}
  // Deposit confirmation isn't currently surfaced on the typed WS
  // channels — poll GET /api/me/balances until `available` reflects
  // the deposited amount, or watch `vault_positions` for the inflow
  // `vault_position_balance_changed` event keyed to your vault.
  ```
</CodeGroup>

## 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](https://eips.ethereum.org/EIPS/eip-2612) `USDC.permit`
off-chain and the platform broadcasts the on-chain deposit for you.

```mermaid theme={null}
sequenceDiagram
    participant U as User wallet (signs only)
    participant CORE as core-api
    participant SUB as Relayer (submitter)
    participant X as MulticallExecutor

    U->>U: sign EIP-2612 USDC.permit (spender = multicallExecutor)
    U->>CORE: POST /api/deposits/relay { amount, permitNonce, permitDeadline, v, r, s }
    CORE-->>U: { auditId, jobId, status: "PENDING_SCREENING" }
    CORE->>SUB: enqueue permit_deposit job
    SUB->>X: atomic batch: permit -> transferFrom -> approve -> depositERC20 -> cleanup
    Note over CORE: chain-watcher indexes DepositedERC20
    U->>CORE: GET /api/deposits/relay/{auditId} -> status: "CONFIRMED"
```

1. Read `multicallExecutor` from [`GET /api/platform/contracts`](/environments)
   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`).

<Note>
  The permit signature **is** the authorization — the backend `ecrecover`s
  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.
</Note>

Full request / response schema: see the **Deposits** section of the
[API Reference](/api-reference/introduction).

## 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](/concepts/withdrawals/overview).
The vault also exposes a 7-day-timelocked emergency withdraw for
recovery if the platform is unavailable; see [Vault contract
reference](/concepts/contracts/vaults).

## Common failures

| Symptom                                                                | Cause                                                                                                                                                                              | Fix                                                                                                    |
| ---------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
| `ERC20InsufficientAllowance`                                           | Step 1 skipped or insufficient cap                                                                                                                                                 | Run `usdc.approve(vault, MaxUint256)` first                                                            |
| Revert with selector `0x87138d5c`                                      | `NotInitialized()` — 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 `DepositCapExceeded`                                       | The deposit would exceed your daily / weekly / monthly cap                                                                                                                         | See [Deposit limits](/concepts/deposits/limits) — wait for the rolling window or request a higher tier |
| Tx mined but `available` still 0 in `/api/me/balances` after >1 minute | Indexer lag or transient incident                                                                                                                                                  | Check [status page](https://status.predictstreet.com); if persistent, contact support with `txHash`    |

## Next

<CardGroup cols={2}>
  <Card title="Deposit limits" icon="gauge" href="/concepts/deposits/limits">
    Daily / weekly / monthly caps and how rolling windows are applied.
  </Card>

  <Card title="Withdrawals overview" icon="arrow-up-from-bracket" href="/concepts/withdrawals/overview">
    Dual-signature flow, AML review, state machine.
  </Card>

  <Card title="Place your first order" icon="plus" href="/concepts/trading/placing-orders">
    Sign + POST a vault-backed order against any open market.
  </Card>

  <Card title="Vault contract reference" icon="file-code" href="/concepts/contracts/vaults">
    Full ABI for the vault, including the deposit entry point.
  </Card>
</CardGroup>
