API Authentication
Protected endpoints on the CLOB API enforce a two-tier credential model: L1 (EIP-712 wallet signature) for bootstrapping credentials, and L2 (HMAC API key) for day-to-day trading. Builder integrations follow a parallel header scheme described below.
Open vs. Protected Endpoints
Section titled “Open vs. Protected Endpoints”- Open (no credentials needed): All Gamma API endpoints, all Data API endpoints, and CLOB read paths such as order book snapshots, price queries, and spread lookups.
- Protected (L2 headers required): CLOB write paths — order submission, cancellation, and heartbeat — which require all five
OPENFISH_*HTTP headers.
L1 Authentication
Section titled “L1 Authentication”L1 proves wallet ownership by having the caller sign an EIP-712 typed-data message with their private key. This mechanism is used exclusively for:
- Generating new API credentials (
POST /auth/api-key) - Recovering existing API credentials (
GET /auth/derive-api-key) - Locally signing order payloads before submission
Invitation Code (First-Time Setup)
Section titled “Invitation Code (First-Time Setup)”Openfish is currently invite-only. When creating your first API key via POST /auth/api-key, you must include a valid invitation code in the OPENFISH_INVITATION_CODE header.
How to get a code:
- Join the waitlist by calling
POST /waitlistwith your email (no auth required). - Once approved, you’ll receive an invitation code (format:
XXXX-XXXX, e.g.AF3K-X9M2). - Include it as a header:
OPENFISH_INVITATION_CODE: AF3K-X9M2
Codes are single-use and expire after a set period. After your first API key is created, subsequent keys (via different nonces) and key recovery (GET /auth/derive-api-key) do not require an invitation code.
L1 Headers
Section titled “L1 Headers”| Header | Description |
|---|---|
OPENFISH_ADDRESS | Polygon signer address |
OPENFISH_SIGNATURE | EIP-712 signature |
OPENFISH_TIMESTAMP | Current UNIX timestamp |
OPENFISH_NONCE | Nonce (default: 0) |
The OPENFISH_SIGNATURE is produced by signing the following EIP-712 typed-data structure:
{ "domain": { "name": "ClobAuthDomain", "version": "1", "chainId": 137 }, "types": { "ClobAuth": [ { "name": "address", "type": "address" }, { "name": "timestamp", "type": "string" }, { "name": "nonce", "type": "uint256" }, { "name": "message", "type": "string" } ] }, "value": { "address": "<signer_address>", "timestamp": "<unix_timestamp>", "nonce": 0, "message": "This message attests that I control the given wallet" }}Response
Section titled “Response”{ "apiKey": "550e8400-e29b-41d4-a716-446655440000", "secret": "base64EncodedSecretString", "passphrase": "randomPassphraseString"}L2 Authentication
Section titled “L2 Authentication”Once you hold API credentials from the L1 flow, all subsequent trading requests are authenticated with HMAC-SHA256.
L2 Signing Steps
Section titled “L2 Signing Steps”- Build the message string:
{timestamp}{METHOD}{path}{body}timestamp: current unix epoch seconds as stringMETHOD: HTTP method in uppercase (GET,POST,DELETE)path: request path with query string (e.g./order,/data/orders?market=0x...)body: raw JSON body string, or empty string""for requests without body
- Decode the
secret(returned from credential creation) from base64url to raw bytes - Compute HMAC-SHA256 using decoded bytes as key, message string as input
- Encode the HMAC output as base64url (with
=padding) — this is the signature
L2 Headers
Section titled “L2 Headers”| Header | Description |
|---|---|
OPENFISH_ADDRESS | Polygon signer address |
OPENFISH_SIGNATURE | base64url(HMAC-SHA256(base64url_decode(secret), message)) |
OPENFISH_TIMESTAMP | Current UNIX timestamp (seconds) |
OPENFISH_API_KEY | User’s API key (UUID) |
OPENFISH_PASSPHRASE | User’s API passphrase |
Code Examples
Section titled “Code Examples”Node.js:
const crypto = require('crypto');const timestamp = Math.floor(Date.now() / 1000).toString();const message = timestamp + method + path + body;
// IMPORTANT: secret must be decoded from base64url FIRST — do NOT use the raw stringconst key = Buffer.from(secret, 'base64url');const raw = crypto.createHmac('sha256', key).update(message).digest();
// IMPORTANT: use base64url (- and _), NOT standard base64 (+ and /)// Node.js base64url omits padding, but the server requires '=' paddingconst b64 = raw.toString('base64url');const signature = b64 + '='.repeat((4 - b64.length % 4) % 4);Python:
import hmac, hashlib, base64, timetimestamp = str(int(time.time()))message = timestamp + method + path + body
# IMPORTANT: decode secret from base64url first — do NOT use the raw stringkey = base64.urlsafe_b64decode(secret)# base64.urlsafe_b64encode produces base64url WITH padding (correct)signature = base64.urlsafe_b64encode( hmac.new(key, message.encode(), hashlib.sha256).digest()).decode()Common mistakes:
- Using the secret string directly as the HMAC key (must decode from base64url first)
- Using standard base64 encoding (
+,/) instead of base64url (-,_)- Missing
=padding on the signature output
Test vector: secret=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=, timestamp=1, method=GET, path=/, body="" → signature=eHaylCwqRSOa2LFD77Nt_SaTpbsxzN8eTEI3LryhEj4=
Builder Authentication
Section titled “Builder Authentication”Builder accounts authenticate through a dedicated set of headers. These apply to builder-accessible endpoints such as GET /data/orders and GET /data/trades.
Builder Headers
Section titled “Builder Headers”| Header | Description |
|---|---|
OPENFISH_BUILDER_API_KEY | Builder API key |
OPENFISH_BUILDER_PASSPHRASE | Builder passphrase |
OPENFISH_BUILDER_SIGNATURE | HMAC signature for builder auth |
OPENFISH_BUILDER_TIMESTAMP | UNIX timestamp for builder auth |
Signature Types
Section titled “Signature Types”| Signature Type | Value | Description |
|---|---|---|
| EOA | 0 | Standard Ethereum wallet. Funder is the EOA address. |
| OPENFISH_PROXY | 1 | Custom proxy wallet for Magic Link users. |
| GNOSIS_SAFE | 2 | Gnosis Safe multisig proxy wallet (most common). |
Timestamp Validation
Section titled “Timestamp Validation”Every authenticated request carries an OPENFISH_TIMESTAMP header containing the current UNIX epoch. The server will reject any request whose timestamp drifts more than 30 seconds from the server’s own clock. This check guards against replay attacks.
If you encounter a 401 Unauthorized response citing a timestamp mismatch, synchronize your system clock (or query GET /time for the server’s current epoch).
Order Signature Verification
Section titled “Order Signature Verification”On receipt of an order via POST /order, the server performs ecrecover against the EIP-712 signature embedded in the payload. The address recovered must equal the signer field (falling back to maker when signer is empty). A mismatch produces a 400 Bad Request rejection.
The EIP-712 domain used for order signatures contains:
- name:
"Openfish CTF Exchange" - version:
"1" - chainId:
137(Polygon mainnet) or80002(Amoy testnet) - verifyingContract: CTF Exchange contract address
Address Ban Enforcement
Section titled “Address Ban Enforcement”Any address that has been banned is permanently blocked from placing new orders. Submitting from a banned address returns 403 Forbidden. You can confirm ban status through GET /auth/ban-status/closed-only.
Trading Mode
Section titled “Trading Mode”The platform operates under one of three modes at any given time:
| Mode | Effect |
|---|---|
| Normal | All operations allowed |
| Cancel-only | New orders rejected with 503 Service Unavailable; cancellations still work |
| Trading disabled | All trading operations rejected with 503 Service Unavailable |
Security Best Practices
Section titled “Security Best Practices”- Keep private keys in environment variables or a secrets manager. Never check them into source control.
- Handle all request signing server-side. API secrets must never appear in browser-facing code.
- Emit heartbeats every 5 seconds to avoid automatic cancellation of your open orders.