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

# API keys

> The only way to authenticate against core.api.dev.predictstreet.sde.adifoundation.ai. Every request sends X-Api-Key; every write additionally carries an EIP-712 signature.

## Who this is for

Your **backend** calls the PredictStreet integrator API. Examples:

* Market-maker bot quoting a pre-approved wallet
* Copy-trading service mirroring strategy signals
* Data pipeline pulling your positions / fills into a warehouse
* Mobile backend that never ships secrets to the device

The integrator API (`core.api.dev.predictstreet.sde.adifoundation.ai`) is designed for
server-to-server traffic. **Retail users don't authenticate here** —
they log into `app.predictstreet.io` with SIWE (off the scope of
this documentation).

<Warning>
  Never ship an API key to a browser, mobile app bundle, public repo,
  or any environment a user can inspect. A leaked key is equivalent
  to a leaked seed phrase for your associated wallet — rotate
  immediately via the admin panel.
</Warning>

## Format

```
X-Api-Key: ps_live_0123456789abcdef_AbCdEfGhIjKlMnOpQrStUvWxYz1234567
           ├─── env ───┤├── keyId ──┤├──────── secret ─────────┤
```

| Segment                 | What it is                                                                                                    |
| ----------------------- | ------------------------------------------------------------------------------------------------------------- |
| `ps_live_` / `ps_test_` | environment prefix — `live` is production / dev; `test` reserved for sandbox                                  |
| `keyId`                 | 16 hex chars. Public handle we look up — safe to put in logs, audit trails, dashboards                        |
| `secret`                | 32 bytes of entropy, base64url-encoded (43 chars). **Never logged, never persisted in plaintext server-side** |

Only a SHA-256 hash of `secret` (peppered with a server-side salt) is
stored. If our database leaks, your secret does not.

## Partner kinds

Every partner is one of two kinds. The kind is set at partner creation
and decides where the request's *acting wallet* comes from.

| Kind                      | When ops picks it                                                                                                | Acting wallet resolution                                                           |
| ------------------------- | ---------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
| `single_wallet` (default) | One bot, one strategy, one wallet — the legacy contract                                                          | `partner.associatedWallet` is fixed at creation; every request acts on that wallet |
| `multi_wallet`            | Trading firm with N sub-accounts, copy-trading platform, custodial broker — one key fans out across many wallets | Caller declares the actor on every request via the `X-User-Wallet` header          |

Kind is stamped on the key when admin issues it. Switching kinds
post-issuance is supervised — operations refuses the flip if any active
key under the partner relies on the prior contract.

### Compliance toggle on multi\_wallet

Multi\_wallet partners come in two compliance flavours, set by the
`requirePerWalletKyc` boolean on the partner record:

| `requirePerWalletKyc`  | Compliance contract                                                                                                                                                                                                                                                        | Use case                                                                                                        |
| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
| `true` (default, safe) | Each X-User-Wallet sub-account must clear retail KYC via SumSub before the platform accepts orders/withdrawals on its behalf. KycGuard enforces tier ≥ 1 per request.                                                                                                      | Sub-account models where each wallet is a distinct human (e.g. white-label retail brokers).                     |
| `false`                | The partner has been KYB-onboarded as a legal entity and takes compliance responsibility for every sub-account it dispatches. KycGuard skips. New sub-accounts are auto-onboarded on first request — `core.users` row created, vault deploy enqueued — without retail KYC. | Trading firms / market makers / submitters whose sub-accounts are operational addresses, not retail identities. |

Single\_wallet partners always enforce KYC at API key creation time
(the partner's `associatedWallet` must be `APPROVED` tier ≥ 1 before
any write-scope key issues), so the toggle is inert there.

### single\_wallet flow

Send the API key, nothing else. The `associatedWallet` on the partner
record is the request's effective wallet for KYC, compliance, and
rate-limit buckets. Any `X-User-Wallet` header is **ignored** — the
legacy contract is preserved for SDKs that don't know about the header.

If a single\_wallet partner has no `associatedWallet` attached yet,
every authenticated endpoint rejects with 401
`api_key_no_associated_wallet` — admin must attach one before writes
or `/me` reads work.

### multi\_wallet flow

Send the API key **plus** `X-User-Wallet: 0x…` on every request:

```bash theme={null}
curl -X POST https://core.api.dev.predictstreet.sde.adifoundation.ai/api/orders/place \
  -H "X-Api-Key: ps_live_0123456789abcdef_AbCdEfGhIjKlMnOpQrStUvWxYz1234567" \
  -H "X-User-Wallet: 0x1234567890AbCdEf1234567890aBcDeF12345678" \
  -H "Content-Type: application/json" \
  -d '{ "maker": "0x1234…", "signature": "0x…", … }'
```

The header value is lower-cased server-side and becomes the
request's effective wallet for **balances, positions, trades, fees,
rate-limit buckets, and audit trails** — exactly as if a single\_wallet
key were attached to that wallet.

Rules:

* Header missing → 401 `api_key_user_wallet_required`.
* Header malformed (not `0x` + 40 hex) → 401 `api_key_user_wallet_invalid`.
* KYC enforcement on the header wallet is controlled by the partner's
  `requirePerWalletKyc` flag (see [Partner kinds](#partner-kinds)
  table above). Two options:
  * `requirePerWalletKyc: true` (default) — every X-User-Wallet
    sub-account must clear retail KYC (status `APPROVED`, tier ≥ 1)
    via the same SumSub flow a retail user goes through. Used for
    sub-account models where each underlying wallet is a distinct
    person (Coinbase Prime style).
  * `requirePerWalletKyc: false` — the partner is KYB-onboarded as a
    legal entity and takes compliance responsibility for its
    sub-accounts. KycGuard skips. Used for trading firms / market
    makers / submitters whose addresses are operational, not retail
    identities. The first request through the key for a previously
    unseen X-User-Wallet auto-enqueues:

    1. **vault deploy** (`VaultFactory` on-chain, \~10 sec to confirm);
    2. **on-chain cap-opening** — `DepositLimitRegistry.setCustomCaps`
       to **100,000 USDC** across singleDeposit / 30d / 12m, plus
       `setDepositsRestricted(false)` / `setTradingRestricted(false)` /
       `setWithdrawalsRestricted(false)`. Match-submitter broadcasts
       all four ops; the sub-account is fully usable end-to-end
       (deposit + trade + withdraw) within \~30 sec of the first
       request. Without this auto-step the wallet would otherwise be
       stuck at retail-tier 0 caps (`0/0/0`) which would block every
       `vault.depositERC20` with `DepositCapExceeded` and every
       `placeOrder` with the trading-restricted flag.

    Need a different cap envelope (above the 100k default)? Contact
    your integration manager — ops can lift per-sub-account.
* Each acting wallet still needs its own private key to sign the EIP-712
  payload on writes. The on-chain `CTFExchange` and `Vault` contracts
  verify signer ↔ vault cryptographically — looser API-layer attribution
  costs nothing structurally because the chain is the floor.
* The wallet you pass in `X-User-Wallet` must match the `maker` field
  inside the order payload (and the EIP-712 signer for that order).
  Mismatches are caught at the matcher / on-chain layer with
  `bad_signature`.

## Getting a key

Keys are issued by PredictStreet ops via the admin panel, never
self-service. Contact your integration manager with:

1. **Partner name** — internal label.
2. **Contact email** — for rotation notices, incident pages, and
   SLA comms.
3. **Partner kind** — `single_wallet` (default) or `multi_wallet`.
   Pick `multi_wallet` if one key needs to act across many end-user
   wallets (trading desk with N sub-accounts, copy-trading platform,
   custodial broker). See [Partner kinds](#partner-kinds) above.
4. **Associated wallet** — required for `single_wallet` partners that
   need write scopes; **must** have completed SIWE once and cleared
   KYC tier 1. For `multi_wallet` partners this field is optional and
   informational — KYC moves to the partner legal entity.
5. **IP allowlist** (optional, recommended). Your egress IPs — if set,
   any request from a different IP is rejected at the edge.
6. **Requested scopes** — what endpoints you plan to hit (see table
   below).

You will receive the **plaintext key exactly once** on creation. Copy
it into a secrets manager (AWS Secrets Manager, HashiCorp Vault,
Kubernetes Secret, 1Password) immediately. We cannot show it to you
again — if lost, we revoke and issue a new one.

## Scopes

Each key carries a set of scopes. Requests are rejected with 403
`api_key_scope_missing` if the endpoint requires a scope the key
doesn't carry.

| Scope            | Grants                                                                                                                                                                                                                                                  |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `markets:read`   | public market & orderbook reads (raises your rate-limit ceiling; unauthenticated callers can still hit these)                                                                                                                                           |
| `events:read`    | public events reads                                                                                                                                                                                                                                     |
| `matches:read`   | public matches reads                                                                                                                                                                                                                                    |
| `portfolio:read` | `/api/me/balances`, `/api/me/positions`, `/api/me/trades`, `/api/me/fees`, `/api/me/fee-tier`, `/api/me/vault`, `/api/me/vault/emergency`, `/api/me/portfolio`, `/api/me/deposit-limits`, `/api/me/withdrawals` (list / detail / fee / deposit-sources) |
| `orders:read`    | `/api/orders/open`, `/api/orders/history`, `/api/orders/{id}`, `/api/orders/{id}/fills`                                                                                                                                                                 |
| `orders:write`   | `/api/orders/place`, `/api/orders/cancel`, `/api/orders/cancel-all` — for `single_wallet` partners requires `associatedWallet` with KYC tier ≥ 1; for `multi_wallet` partners KYC is at the partner level                                               |
| `vault:write`    | `/api/vault/split-signature`, `/api/vault/merge-signature`, `/api/withdrawals/request`, `/api/me/withdrawals/{id}/cancel` — same KYC rule as `orders:write`                                                                                             |

Multiple scopes listed on an endpoint are **all required** (intersection,
not union). For most partners, `orders:write` + `orders:read` +
`portfolio:read` covers the day-one scope.

## Endpoint coverage

### Authenticated endpoints

Require `X-Api-Key` with the listed scope.

| Endpoint                              | Method | Scope            |
| ------------------------------------- | ------ | ---------------- |
| `/api/orders/place`                   | POST   | `orders:write`   |
| `/api/orders/cancel`                  | POST   | `orders:write`   |
| `/api/orders/cancel-all`              | POST   | `orders:write`   |
| `/api/orders/open`                    | GET    | `orders:read`    |
| `/api/orders/history`                 | GET    | `orders:read`    |
| `/api/orders/{id}`                    | GET    | `orders:read`    |
| `/api/orders/{id}/fills`              | GET    | `orders:read`    |
| `/api/me/vault`                       | GET    | `portfolio:read` |
| `/api/me/vault/emergency`             | GET    | `portfolio:read` |
| `/api/me/balances`                    | GET    | `portfolio:read` |
| `/api/me/positions`                   | GET    | `portfolio:read` |
| `/api/me/trades`                      | GET    | `portfolio:read` |
| `/api/me/fees`                        | GET    | `portfolio:read` |
| `/api/me/fee-tier`                    | GET    | `portfolio:read` |
| `/api/me/portfolio`                   | GET    | `portfolio:read` |
| `/api/vault/split-signature`          | POST   | `vault:write`    |
| `/api/vault/merge-signature`          | POST   | `vault:write`    |
| `/api/withdrawals/request`            | POST   | `vault:write`    |
| `/api/me/withdrawals`                 | GET    | `portfolio:read` |
| `/api/me/withdrawals/{id}`            | GET    | `portfolio:read` |
| `/api/me/withdrawals/fee`             | GET    | `portfolio:read` |
| `/api/me/withdrawals/deposit-sources` | GET    | `portfolio:read` |
| `/api/me/withdrawals/{id}/cancel`     | POST   | `vault:write`    |
| `/api/me/deposit-limits`              | GET    | `portfolio:read` |

### Public endpoints

No authentication required. Sending an `X-Api-Key` is still allowed
and raises your per-partner rate-limit ceiling:

* `GET /api/markets`, `/api/markets/{slug}`, `/api/markets/{symbol}/orderbook`, `/api/markets/{symbol}/trades`, `/api/markets/{symbol}/ohlc`
* `GET /api/events`, `/api/events/{id}`
* `GET /api/matches`, `/api/matches/{id}`
* `GET /api/tags`
* `GET /api/platform/notices`, `/api/platform/status`
* `GET /api/compliance/geo`
* `GET /health`, `/ready`

### WebSocket

Both WebSocket gateways authenticate with the same `X-Api-Key` as
HTTP — send either the `X-Api-Key` header on the upgrade or a
`?key=<token>` query parameter (for browser clients that can't set
headers on the WebSocket upgrade).

| Gateway                                         | URL                                                                 | Required scope                           |
| ----------------------------------------------- | ------------------------------------------------------------------- | ---------------------------------------- |
| [`/ws/user`](/api-reference/websocket/user)     | `wss://ws-gateway.dev.predictstreet.sde.adifoundation.ai/ws/user`   | `portfolio:read` (every private channel) |
| [`/ws/market`](/api-reference/websocket/market) | `wss://ws-gateway.dev.predictstreet.sde.adifoundation.ai/ws/market` | — (any active key)                       |

| WS channel                                       | Gateway      | Required scope   |
| ------------------------------------------------ | ------------ | ---------------- |
| `user_orders`, `user_fills`, `vault_positions`   | `/ws/user`   | `portfolio:read` |
| `token_trade_matches`, `token_trade_settlements` | `/ws/market` | —                |
| `token_book`, `token_ohlc`                       | `/ws/market` | —                |
| `condition_lifecycle`, `system`                  | `/ws/market` | —                |

### Not available via API key

A small number of surfaces are intentionally **not** exposed to
integrators — they require retail-user SIWE identity and live on
`app.predictstreet.io`. As of the current release these include the
self-service profile / consent / KYC submission flows: partners
shouldn't drive end-user identity capture from server-side because
SUMSUB and the geo-fingerprinting layer expect the live retail
session.

Withdrawals **are** now fully partner-driven via API key (request,
list, detail, cancel) — the vault-owner EIP-712 signature still gates
funds movement, so an API key alone cannot move money out of a vault.
See [Withdrawals overview](/concepts/withdrawals/overview) for the
dual-signature flow.

If you have a use case that needs something currently outside the
API-key scope, reach out — some can be unlocked with a stricter scope
set, others are architecturally out of scope.

## Making a request

**single\_wallet partner:**

```bash theme={null}
curl -X POST https://core.api.dev.predictstreet.sde.adifoundation.ai/api/orders/place \
  -H "X-Api-Key: ps_live_0123456789abcdef_AbCdEfGhIjKlMnOpQrStUvWxYz1234567" \
  -H "Content-Type: application/json" \
  -d '{
    "marketId": "UAE-CUP-FINAL-20260425",
    "side": "buy",
    "outcome": "0",
    "price": "0.55",
    "quantity": "100",
    "nonce": "1730289600000000",
    "expiry": 1730376000,
    "maker": "0xYOUR_ASSOCIATED_WALLET",
    "signature": "0x…EIP-712 signature over the order…"
  }'
```

**multi\_wallet partner — add `X-User-Wallet`:**

```bash theme={null}
curl -X POST https://core.api.dev.predictstreet.sde.adifoundation.ai/api/orders/place \
  -H "X-Api-Key: ps_live_0123456789abcdef_AbCdEfGhIjKlMnOpQrStUvWxYz1234567" \
  -H "X-User-Wallet: 0x1234567890AbCdEf1234567890aBcDeF12345678" \
  -H "Content-Type: application/json" \
  -d '{
    "marketId": "UAE-CUP-FINAL-20260425",
    "side": "buy",
    "outcome": "0",
    "price": "0.55",
    "quantity": "100",
    "nonce": "1730289600000000",
    "expiry": 1730376000,
    "maker": "0x1234567890AbCdEf1234567890aBcDeF12345678",
    "signature": "0x…EIP-712 signed by that maker's key…"
  }'
```

<Warning>
  `maker` **must** equal the request's effective wallet (the
  `associatedWallet` for single\_wallet, or the `X-User-Wallet` header
  value for multi\_wallet) or its VaultFactory-derived vault, and the
  EIP-712 `signature` must be signed by that wallet's private key. Your
  API key authenticates the HTTP request; the EIP-712 signature
  authorises the on-chain trade. Neither alone is sufficient — on-chain
  `CTFExchange` verifies the signature cryptographically before the tx
  settles.
</Warning>

## What happens on every request

1. **Parse** the `X-Api-Key` header into `(env, keyId, secret)`. Bad
   shape → 401 `api_key_bad_format`.
2. **Lookup** the key by `keyId` (Redis cache, 60s TTL; falls through
   to Postgres on miss). Unknown → 401 `api_key_unknown_key`.
3. **Verify** the hash in constant time. Mismatch → 401
   `api_key_bad_secret`.
4. **Lifecycle** checks: revoked → 401 `api_key_revoked`; expired →
   401 `api_key_expired`; partner suspended → 401
   `api_key_suspended`; source IP not in allowlist (if set) → 401
   `api_key_ip_denied`.
5. **Scope** check. Missing → 403 `api_key_scope_missing` with the
   required scope in the response body.
6. **Resolve effective wallet** — depending on partner kind:
   * `single_wallet` → `partner.associatedWallet`. Not attached → 401
     `api_key_no_associated_wallet`.
   * `multi_wallet` → the `X-User-Wallet` header (lower-cased). Header
     missing → 401 `api_key_user_wallet_required`; not a 0x-prefixed
     40-hex address → 401 `api_key_user_wallet_invalid`.
7. **Set identity** — the resolved wallet becomes the request's
   effective wallet for all downstream checks (KYC, compliance,
   rate-limit buckets, audit trails).

## Rate limits

Every API-key request passes through **two** independent buckets:

* **IP bucket** — shared edge defence (same as anonymous traffic).
* **Wallet bucket** — per effective wallet (the `associatedWallet` for
  single\_wallet, or the `X-User-Wallet` header value for
  multi\_wallet). Same ceiling regardless of call volume distribution
  across your keys.

Both must pass. Response headers expose the tightest applicable
bucket's state (`X-RateLimit-Limit`, `-Remaining`, `-Reset`). On
429, `Retry-After` is set to the bucket-reset delta.

Per-partner organisation-wide bucket exists in the partner record
(`rateLimitPerMin`) but is not currently enforced at the request
boundary. Contact your integration manager if you need a
partner-wide ceiling — operations can lower the per-wallet cap as
a stop-gap.

## Geo handling

Geographic blocking (FATF blacklist / greylist) **does not apply to
API-key requests** — integrator egress IPs are data-centre / VPN
class and blocking them isn't a meaningful control (you could proxy
trivially). Compliance stays in force at the wallet level via
KYC / AML / RG gates on the effective wallet — for `single_wallet`
that's the partner's `associatedWallet`, for `multi_wallet` that's
the partner legal entity that took on the regulated counterparty
obligation.

## Rotation

Rotate keys pre-emptively on any of:

* Employee who had access leaves.
* Secrets-manager misconfiguration suspected.
* Source-code leak.
* Routine 6-month rotation hygiene.

Process:

1. **Issue** the replacement key via admin. Both keys now work.
2. **Roll** your services to the new key. No downtime.
3. **Revoke** the old key via admin. Propagation is near-instant
   (Redis pub-sub invalidation); worst-case lag 60s.

A revoked key rejects every subsequent request with 401
`api_key_revoked`. Re-enabling is not supported — issue a new key.

## Security invariants

* **X-Api-Key is the only session scheme.** No cookies, no bearer
  tokens, no basic-auth fallback on `core.api.dev.predictstreet.sde.adifoundation.ai`. Missing
  or malformed header on an authenticated endpoint → 401.
* **Scopes are enforced server-side.** Adding a scope to your key's
  stored list is the only way to access a gated endpoint —
  client-side claims are ignored.
* **Writing still requires EIP-712.** The API key proves you're a
  pre-approved partner; the EIP-712 order signature proves you
  authorised the specific trade. `CTFExchange` verifies this
  cryptographically on-chain — no compromise of our infrastructure
  lets anyone move your funds without your private key.
* **HMAC request signing** (adding an `X-Api-Signature` header over
  `timestamp + method + path + body`) is on the roadmap as an
  optional hardening for production partners; not required today.
