Skip to content

Authentication (SIWS)

The Order Manager’s HTTP API uses SIWS (Sign-In with Solana). There is no API key to provision and no server-side session: the token carries its own proof, and the server verifies it on every request.

  1. Your wallet signs the literal message Sign in with <aud>, where aud is the API client id (CLOB_SIWS_AUD, default GoMarket API).
  2. You build an unsecured JWT whose claims carry that proof.
  3. You send it as Authorization: Bearer <token>.
  4. The server verifies the ed25519 signature against the sub claim, checks aud / iss / exp and the token age, and uses sub as your identity.

The JWT’s third segment is ignored — trust comes entirely from the embedded ed25519 signature, not from a JWT signing key.

ClaimValue
subYour Solana address, base58 — this is your identity
audThe API client id; must match the server’s CLOB_SIWS_AUD
issIssuer; checked only if the server configures CLOB_SIWS_ISS
iat / expIssued-at and expiry, unix seconds
jtiToken id
signatureBase58 ed25519 signature of Sign in with <aud>

The get-token utility in the CLOB repo:

Terminal window
# From a Solana keypair file
export CLOB_TOKEN=$(cargo run -p get-token --quiet -- --keypair ~/.config/solana/id.json)
# From a 12-word BIP39 seed phrase
export CLOB_SEED_PHRASE="word1 word2 … word12"
export CLOB_TOKEN=$(cargo run -p get-token --quiet)

Relevant environment:

VariableDefaultMeaning
CLOB_SIWS_AUDGoMarket APIThe aud claim; the wallet signs Sign in with <aud>
CLOB_SIWS_ISS(empty)Expected issuer; empty means unchecked
CLOB_TOKEN_TTL_SECONDS300Token lifetime (exp = iat + ttl)

Key derivation from a seed phrase follows the Solana tooling exactly: BIP39 mnemonic → PBKDF2 seed → SLIP-0010 at m/44'/501'/0'.

Terminal window
curl -s http://127.0.0.1:8080/v1/markets \
-H "Authorization: Bearer $CLOB_TOKEN" | jq

GET /health is the only public route. Everything else under /v1/ requires a token.

Your sub is not advisory — it is the authorization boundary:

OperationEffect of sub
Insert orderBecomes the order’s owner. A client-supplied owner is accepted but ignored.
CancelYou can only cancel orders you own (case-insensitive match).
GET /v1/ordersAlways scoped to you.
GET /v1/orders/value-rangeAlways scoped to you.
GET /v1/open-ordersScoped to you.
Any owner filterMust equal sub, or the request is rejected.

Other traders’ order hashes and owner addresses are never published. You see aggregate book depth, not participants.

Tokens expire after CLOB_SIWS_MAX_AGE_SECONDS (default 300 seconds). The server checks both exp and the absolute age, so a long-lived exp won’t extend a token past the server’s limit.

Mint a fresh token per session. For a long-running bot, regenerate on a timer comfortably inside the window — signing is local and cheap.

POST /v1/orders/size-update is not user-facing. The Executor reports on-chain partial fills and cancels for any owner, so it authenticates with a shared secret instead:

X-Internal-Token: <CLOB_INTERNAL_TOKEN>

The comparison is constant-time and the route fails closed when no token is configured. Only the Executor holds that secret. If you’re building a client, you will never call this route.

The gRPC transport (clob.OrderManagerService) is an internal, in-cluster surface. External clients use the HTTP API with SIWS.

SymptomCause
401 on every requestaud mismatch — the signed message must be Sign in with <aud> verbatim
Worked, then stoppedToken older than CLOB_SIWS_MAX_AGE_SECONDS; mint a new one
Order rejected on ownerAn owner filter that isn’t sub
Empty order listGET /v1/orders only ever returns your own orders