Skip to main content

0009. Venue API credentials are derived at startup and held in memory

  • Status: Proposed
  • Date: 2026-09-30

Context

Phase 2 reads the account: open orders, cash and the User WS need Polymarket CLOB API credentials (key, secret, passphrase). They are created or derived through L1 auth: the signer signs a ClobAuth message and the CLOB returns the credentials. With a Session Key, the credentials belong to the Session Key. The Phase 0 .env.example named the signer POLYMARKET_PRIVATE_KEY, described it as the key that controls the account, and suggested saving derived credentials back into .env. The security review found (F1) that the name invites putting the owner key on the host, and (F8) that the signer key has no use on the geoblocked development server. Every copy of a credential on disk is one more place it can leak from.

Decision

  • The signer is POLYMARKET_SESSION_KEY: a CLOB-scoped Polymarket Session Key for the Deposit Wallet in POLYMARKET_WALLET_ADDRESS. The owner key never goes into the app.
  • If POLYMARKET_CLOB_API_KEY, _SECRET and _PASSPHRASE are set, the app uses them. Otherwise a credential actor in venue/polymarket derives them through L1 auth at startup and holds them in memory only. They are never written to .env, SQLite, logs, errors, API responses or WS frames, and they are derived again at each start.
  • The old name POLYMARKET_PRIVATE_KEY is refused at startup with a message to rename it. The loader does not read it as a fallback.
  • Optional POLYMARKET_OWNER_ADDRESS (public): the loader refuses to start when the signer’s address equals it (F1).
  • The credential actor is the single source of the credential state (PLAN §3.7). It publishes core.CredentialStatus (none, pending, ready, failed with a fixed reason code) on core.TopicCredentialStatus; the browser sees it in AccountSummary.credentials.
  • Without credentials, Data API reads (positions, activity, P&L) still work from the wallet address. Open orders, cash and the User WS report credentials_unavailable (503).
  • The signer key is not configured on the geoblocked development server (F8). Verification there uses the fake venue and the golden vectors; the user verifies with real credentials from a permitted location.

Consequences

  • No credential file to protect, back up or rotate besides the Session Key itself. Rotating the key rotates the derived credentials with it.
  • Each start makes one L1 request before account data that needs credentials is available. While it runs the state is pending; if it fails the state is failed and those endpoints answer 503 until a later attempt succeeds. The retry schedule is defined by implementation.
  • A user upgrading from Phase 1 with POLYMARKET_PRIVATE_KEY in .env must rename it before the app starts. This is deliberate: renaming is the moment to check that the value is a Session Key and not the owner key.
  • Configured POLYMARKET_CLOB_* values must belong to the configured Session Key. After a key rotation they must be cleared or replaced.
  • The in-memory credentials still sit in process memory; crash dumps stay disabled (secrets and logging policy §6).

Alternatives considered

  • Derive once and save the credentials to .env or SQLite. Rejected. It adds a second secret at rest and a copy that outlives a key rotation, and SQLite holds no secret columns by policy.
  • Accept POLYMARKET_PRIVATE_KEY as an alias. Rejected. The name does not say which key belongs there, and a silent alias is a fallback path.
  • Require the CLOB credentials in the environment. Rejected. The user would have to run L1 auth by hand, and the credentials would be stored beside the key that can derive them anyway.