Appearance
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…"
}| Field | Type | Notes |
|---|---|---|
tableId | string | The room/table identifier. evm-bank derives tableId = keccak256(tableId). |
seatIndex | number | Seat the user is claiming. Forwarded to game-server's credit-chips callback. |
amount | string | number | Integer 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/deadline | EIP-712 signature parts | The ERC-2612 permit the UI signed for the SKPK token's nonces(signerAddress) at request time. |
signerAddress | 0x${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→ returns409 { 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
| Status | error | Meaning |
|---|---|---|
| 400 | missing_required_fields | Body is missing one of the required fields. |
| 400 | invalid_signer_address | signerAddress is not a valid 0x-prefixed 40-hex-char address. |
| 400 | signature_invalid | Ecrecovered signer does not match signerAddress. Usually means the permit was tampered with or the UI signed against a different domain. |
| 400 | wallet_not_linked | Valid permit, but signerAddress is not one of the user's linked wallets according to auth-server. |
| 400 | failed | On-chain submission reverted (e.g., ERC2612InvalidSigner, insufficient balance, expired deadline). error carries the revert reason. |
| 401 | missing_bearer_token | No Authorization: Bearer header. |
| 401 | invalid_token | JWT failed JWKS verification (expired, wrong issuer, bad signature). |
| 409 | table_frozen | Room is in frozen status; deposits disabled. |
| 409 | failed (replay of failed) | Previous attempt with the same idempotency key failed. |
| 503 | chain_not_configured | evm-bank is missing its chain RPC configuration. |
| 503 | no_active_deployment | No active contract deployment in contract_deployments for the chain. |
| 503 | auth_service_not_configured | evm-bank is missing its auth-service configuration. |
| 503 | auth_server_unavailable | Linkage 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— JWTsubdeposit.tableIddeposit.amountdeposit.claimed_signerdeposit.recovered_signer— populated after ecrecoverauth.linked— boolean, populated after linkage checkdeposit.idempotency_keydeposit.nonce— the token nonce evm-bank read for the signerdeposit.tx_hash— populated after enqueueAndWaitdeposit.confirm_mode—rpcdeposit.result— one ofconfirmed,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