Skip to content

POST /api/deposit

Browser-facing deposit endpoint on evm-bank (the money bank). The UI calls it directly — game-server is never in the signing path and never sees wallet data.

See the Chain of Custody ADR and the Game-Admin Alignment ADR for the doctrine this endpoint lives under.

Auth

Authorization: Bearer <JWT>

The JWT is the same token auth-server mints and the game-server verifies on its WebSocket upgrade. evm-bank verifies via the shared JWKS endpoint (AUTH_JWKS_URL), so a valid token on one service is a valid token on the other.

The JWT's sub claim is our canonical public.users.id UUID. evm-bank uses it both to authenticate the request and to look up the user's linked wallets via auth-server.

Invalid / missing / expired tokens → 401 missing_bearer_token or 401 invalid_token.

CORS

Browser-facing. evm-bank CORS is allowlisted to the poker UI (skillspoker.com). Requests from other origins are rejected at the CORS preflight stage.

Request body

json
{
  "tableId": "room-abc-123",
  "seatIndex": 2,
  "amount": "10000",
  "permit": {
    "v": 27,
    "r": "0x…",
    "s": "0x…",
    "deadline": "1745012345"
  },
  "signerAddress": "0xabc…"
}
FieldTypeNotes
tableIdstringThe room/table identifier. evm-bank derives tableId = keccak256(tableId).
seatIndexnumberSeat the user is claiming. Forwarded to game-server's credit-chips callback.
amountstring | numberInteger chip units (what the UI/DB/credit-chips speak). Coerced to BigInt. evm-bank converts chips → SKPK base units (× 10⁶) via chipsToTokenUnits only at the on-chain registerAndDeposit boundary.
permit.v/r/s/deadlineEIP-712 signature partsThe ERC-2612 permit the UI signed for the SKPK token's nonces(signerAddress) at request time.
signerAddress0x${string}The address the UI claims signed the permit. evm-bank ecrecovers and verifies this matches.

Processing

Replay / idempotency

idempotencyKey = hashPermit({ wallet: signerAddress, amount, deadline, v, r, s }).

A second request with the same key:

  • confirmed → returns the cached { status: 'confirmed', txHash, replay: true }.
  • submitted / pending → returns the in-flight state.
  • failed → returns 409 { status: 'failed', error }.

This makes the endpoint safe to retry from the UI without double-spending the signed permit.

Credit-chips callback failure mode

If the callback to game-server fails (game-server down, 5xx, network partition), the on-chain deposit still succeeded. deposit_intents.credited_at stays null. The credit-chips-reconciler background worker runs every 30s (plus once at startup) and retries for every row where status = 'confirmed' AND credited_at IS NULL. The game-server handler is idempotent-by-idempotencyKey, so a second successful delivery is a no-op.

Response

Success

json
{
  "status": "confirmed",
  "txHash": "0x…",
  "idempotencyKey": "0x…"
}

Submitted but not yet confirmed

Returned when on-chain submission succeeded but RPC confirmation (waitForTransactionReceipt) did not land within its window. The reconciler will still credit chips once confirmation propagates.

json
{
  "status": "submitted",
  "txHash": "0x…",
  "note": "submitted on-chain; confirmation pending — reconciler will credit chips when confirmed"
}

Errors

StatuserrorMeaning
400missing_required_fieldsBody is missing one of the required fields.
400invalid_signer_addresssignerAddress is not a valid 0x-prefixed 40-hex-char address.
400signature_invalidEcrecovered signer does not match signerAddress. Usually means the permit was tampered with or the UI signed against a different domain.
400wallet_not_linkedValid permit, but signerAddress is not one of the user's linked wallets according to auth-server.
400failedOn-chain submission reverted (e.g., ERC2612InvalidSigner, insufficient balance, expired deadline). error carries the revert reason.
401missing_bearer_tokenNo Authorization: Bearer header.
401invalid_tokenJWT failed JWKS verification (expired, wrong issuer, bad signature).
409table_frozenRoom is in frozen status; deposits disabled.
409failed (replay of failed)Previous attempt with the same idempotency key failed.
503chain_not_configuredevm-bank is missing its chain RPC configuration.
503no_active_deploymentNo active contract deployment in contract_deployments for the chain.
503auth_service_not_configuredevm-bank is missing its auth-service configuration.
503auth_server_unavailableLinkage check to auth-server failed (network, 5xx, timeout).

Observability

The endpoint emits a single admin.deposit OTel span per request with the following attributes (see apps/evm-bank/src/routes/deposit.ts):

  • deposit.userId — JWT sub
  • deposit.tableId
  • deposit.amount
  • deposit.claimed_signer
  • deposit.recovered_signer — populated after ecrecover
  • auth.linked — boolean, populated after linkage check
  • deposit.idempotency_key
  • deposit.nonce — the token nonce evm-bank read for the signer
  • deposit.tx_hash — populated after enqueueAndWait
  • deposit.confirm_moderpc
  • deposit.result — one of confirmed, submitted_not_confirmed, signature_invalid, wallet_not_linked, replay_confirmed, failed, table_frozen

The credit-chips callback emits its own HTTP client span automatically via @opentelemetry/instrumentation-undici — no custom span needed on the notifier path.

See also

  • POST /settlement/deposit — the legacy game-server-forwarded path (removed).
  • Game-Admin Alignment — the doctrine the casino model enforces (game-server = table, evm-bank = cage).
  • Source: apps/evm-bank/src/routes/deposit.ts