> ## 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.

# Upgrade and move USDC

> Activate the generation-2 vault with POST /api/me/vault/upgrade, then move free USDC from the generation-1 vault with one WithdrawERC20 signature.

## Upgrade the vault

```http theme={null}
POST /api/me/vault/upgrade
X-Api-Key: ps_live_<keyId>_<secret>   # needs `vault:migrate` or `vault:write` scope
Content-Type: application/json

{}
```

Idempotent. No wallet signature and no 2FA code. The upgrade moves no
money and cancels no orders, and it cannot be undone.

| Answer | Meaning | Next |
| - | - | - |
| `202` | Generation-2 vault deploy queued or running | Poll `GET /api/me/vault` until `upgrade.status` is `ready`, then call again |
| `200`, `generation: 2` | Generation-2 vault is active (also on every later call) | Done |
| `409 vault_upgrade_not_available` | Rollout not enabled for your wallet, or no vault yet | Wait for the rollout. If you have no vault yet, wait for [auto-deploy](https://docs.testnet.dev.adipredictstreet.com/concepts/vaults/auto-deploy.md) or call `POST /api/me/vault` (never call `deployVault` yourself) |
| `409 vault_upgrade_blocked`, `reason: withdrawal_in_flight_unstamped` | A withdrawal requested before the release is still in progress | Retry after it ends |
| `409 vault_upgrade_blocked`, other `reason` | The vault cannot be activated | Contact support; do not loop |
| `409 vault_deploy_failed`, `503 vault_create_unavailable` | Transient | Retry after `retryAfterSec`, a few times |
| Any other `4xx`, e.g. `403 api_key_scope_missing` | Retrying will not help | Fix the cause |

```typescript theme={null}
const sleep = (ms: number) => new Promise(r => setTimeout(r, ms));
// Retry only what the table marks as retryable; give up after a few attempts.
for (let attempt = 0; attempt < 10; attempt++) {
  const r = await fetch(`${BASE}/api/me/vault/upgrade`, { method: 'POST', headers: auth, body: '{}' });
  const body = await r.json();
  if (r.status === 200 && Number(body.generation) === 2) break;               // active
  const code = body?.error?.code;
  const retryable = r.status === 202 || r.status >= 500 || code === 'vault_deploy_failed';
  if (!retryable) throw new Error(`upgrade refused: ${r.status} ${code}`);
  await sleep((Number(body?.error?.retryAfterSec) || 5) * 1000);             // after 202: deploy still running
}
```

From activation on, deposits (`depositERC20`, relay, on-ramp) go to the
generation-2 vault. Read `vaultAddress` from `GET /api/me/vault` before
each deposit.

## Move USDC from the generation-1 vault

Moves free USDC into the generation-2 vault without leaving the
platform: no deposit-limit usage, no bonus or referral trigger, no
deposit or withdrawal notification, no 2FA code. One move can be open
per wallet.

**1. Create the move**

```http theme={null}
POST /api/me/vault/upgrade/transfer
X-Api-Key: ps_live_<keyId>_<secret>   # needs `vault:migrate` or `vault:write` scope

{ "amount": "125.5" }
```

Omit `amount` to move all free USDC (`usdcMove.freeUsdc` in
`GET /api/me/vault`; `null` for a key without `portfolio:read`). `201` is a new move; `200` returns your open
move, which may be for another amount or already signed. Every new move is **atomic** (`mode: "atomic"`) —
one platform transaction withdraws from your generation-1 vault directly to your wallet, pulls it with your USDC
permit, and deposits it into your generation-2 vault; it lands whole or not at all. The answer
carries `transfer.signRequests`: `withdraw` (WithdrawERC20 with `to` set to YOUR wallet; also returned as `signRequest`),
and `permit` (USDC EIP-2612 Permit { owner, spender = MulticallExecutor, value, nonce, deadline }):

```json theme={null}
{
  "mode": "atomic",
  "signRequests": {
    "withdraw": {
      "domain": { "name": "PredictStreetVault", "version": "1", "chainId": 99999, "verifyingContract": "0x…gen1" },
      "types": { "WithdrawERC20": [ … ] },
      "primaryType": "WithdrawERC20",
      "message": { "token": "0x…usdc", "to": "0x…yourWallet", "amount": "125500000", "salt": "…", "deadline": 1792… }
    },
    "permit": {
      "domain": { "name": "USD Coin", "version": "2", "chainId": 99999, "verifyingContract": "0x…usdc" },
      "types": { "Permit": [ … ] },
      "primaryType": "Permit",
      "message": { "owner": "0x…yourWallet", "spender": "0x…executor", "value": "125500000", "nonce": "0", "deadline": 1792… }
    }
  }
}
```

`amount` is in 6-decimal units. `signRequests` is `null` once the move is signed.

> [!NOTE]
>
> **JIT exposure warning (`warnings`):** If moving funds leaves collateral-exempt (JIT) market-maker quotes unfunded on the generation-1 vault, both `POST …/transfer` and `POST …/signature` include `warnings: [{ code: 'jit_quotes_unfunded_after_move', vaultAddress, unfundedNotional }]`. The move is not blocked, but warned so market makers can adjust resting quotes.

**2. Check and sign both**

Check that `signRequests.withdraw.domain.verifyingContract` is your generation-1 vault, `token` is
`usdc` from `GET /api/platform/contracts`, `to` is your wallet address, and `amount` matches the
move. For `signRequests.permit`, check that `verifyingContract` is the USDC contract and `value`
matches the amount. Sign both typed data payloads with your wallet and send both signatures:

```http theme={null}
POST /api/me/vault/upgrade/transfer/{id}/signature

{
  "userSig": "0x…withdrawSig",
  "permitSig": "0x…permitSig"
}
```

`202`. Sending the same signatures again returns the same answer.

**3. Follow it**

Poll `GET /api/me/vault/upgrade/transfer/{id}`, or watch
`vault_move_updated` on `user_activity`, until `completed`,
`failed`, or `withdrawn_to_wallet` (for legacy moves: `deposit_confirmed`,
`refunded`, `failed` or `needs_ops`).

```typescript theme={null}
const r = await fetch(`${BASE}/api/me/vault/upgrade/transfer`, {
  method: 'POST', headers: auth, body: JSON.stringify(amount ? { amount } : {}),
});
const { transfer: t } = await r.json();
if (r.status === 200 && amount && parseUnits(t.amount, 6) !== parseUnits(amount, 6)) {
  throw new Error(`open move ${t.id} is for ${t.amount}`);   // wait for it to end, then create a new one
}
const srs = t.signRequests;
if (srs) {
  const { withdraw: w, permit: p } = srs;
  if (w.domain.verifyingContract.toLowerCase() !== gen1Vault.toLowerCase()) throw new Error('not my generation-1 vault');
  if (w.message.token.toLowerCase() !== contracts.usdc.toLowerCase()) throw new Error('not USDC');
  if (BigInt(w.message.amount) !== parseUnits(t.amount, 6)) throw new Error('amount mismatch');

  const userSig = await eoa.signTypedData(
    { ...w.domain, chainId: Number(w.domain.chainId) },
    { WithdrawERC20: w.types.WithdrawERC20 },
    w.message,
  );

  let permitSig: string | undefined;
  if (p) {
    permitSig = await eoa.signTypedData(
      { ...p.domain, chainId: Number(p.domain.chainId) },
      { Permit: p.types.Permit },
      p.message,
    );
  }

  await fetch(`${BASE}/api/me/vault/upgrade/transfer/${t.id}/signature`, {
    method: 'POST', headers: auth, body: JSON.stringify({ userSig, permitSig }),
  });
}
// poll GET /api/me/vault/upgrade/transfer/{id} until a terminal status
```

After a crash, call the create again: with a move open it returns that
move (`200`), never a second one. If its `signRequests` is `null`, do
not sign again; poll it.

### Statuses

| Status | Mode | Meaning |
| - | - | - |
| `pending` | Atomic | Awaiting signatures; once signed, the atomic batch is prepared |
| `submitted` | Atomic | The atomic multicall transaction is in flight on chain |
| `completed` | Atomic | Done — USDC is in the generation-2 vault (`txHash`) |
| `withdrawn_to_wallet` | Atomic | Withdrawal succeeded but deposit could not be completed; USDC is in your EOA wallet (deposit it into generation 2 to use it) |
| `failed` | Atomic | The batch did not execute; nothing moved (`retryable: true`, `holdRelease`). Create a new move |
| `requested` → `withdraw_signed` → `withdraw_confirmed` → `deposit_submitted` | Legacy | Legacy two-leg move in progress |
| `deposit_confirmed` | Legacy | Done |
| `refunded` | Legacy | Deposit failed; USDC refunded to generation-1 vault |
| `needs_ops` | Legacy | Held by the relayer for manual operator resolution |

### Limits and timing

* Sign before `signRequests.withdraw.message.deadline` (or `signRequest.message.deadline`, 1 hour after creation by
  default), else `409 vault_move_expired`; create a new move.
* The withdraw half of an atomic move is locked exclusively to the migration batch and cannot be cancelled via `DELETE /api/me/withdrawals/:id` (`409 vault_move_withdrawal_not_cancellable`). If execution cannot proceed on chain, the lock is released automatically once proven dead.
* Each atomic move executes in a single multicall transaction (`txHash`).
* Only `available` USDC moves. USDC locked by open orders or pending
  operations stays (`409 insufficient_free_balance`). Cancel the
  generation-1 orders you no longer want first. Source generation-1 free
  USDC (`usdcMove.freeUsdc` on `GET /api/me/vault` and the cap on
  `POST /api/me/vault/upgrade/transfer`) uses
  `min(ownerStateAvailable, balancesRowAvailable)` when the exchange owner
  state answers (`RND-2091`). If the source generation-1 `exchange.balances`
  row cannot be read, `POST /api/me/vault/upgrade/transfer` always returns
  retryable `503 vault_move_unavailable` (`retryable: true`), whereas
  `GET /api/me/vault` falls back to the owner-state figure when owner state
  answered for that vault and degrades `usdcMove` to `null` only when owner
  state could not supply the figure either.
* While the move is open, the amount is locked on the generation-1
  vault.
* Keep enough USDC on the generation-1 vault for the legacy quotes you
  keep until the [generation-1 trading halt](https://docs.testnet.dev.adipredictstreet.com/concepts/vault-gen2/overview.md#rollout).
* **KYC withdrawal tier (RND-1873 / RND-1879).** The caller must hold minimum KYC withdrawal tier 1 (`403 withdraw_kyc_tier_required`). KYB-waived partner callers pass.
* **Scope-specific withdrawals freeze.** When platform or withdrawals-only kill-switches are active, transfer creation and signing return `503 platform_frozen` / `503 withdrawals_frozen` (`retryAfterSec`).
* **Limiting factors (`limitedBy`).** On `GET /api/me/vault`, `usdcMove.limitedBy` clarifies why `movableUsdc` is less than `freeUsdc`, reporting `bonus_lock`, `withdraw_limit`, `withdraw_blocked`, `platform_frozen`, `withdrawals_frozen`, `partner_restricted`, or `withdraw_tier`. Offer the move only when `movableUsdc` > 0.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.