Daejin Developers

Authentication

API keys, scopes, rate limits, and the app session — how requests are identified.

API keys

Create keys on the API keys page after signing in with your wallet (Sign-In with Ethereum, EIP-4361), or from code. A key looks like tp_live_… on production and tp_test_… on staging; the prefix tells you — and any secret scanner — which deployment it belongs to. A test key is refused by production before it is even looked up. A wallet may hold five enabled keys at a time.

Send it on every request as either header:

X-API-Key: tp_live_a1b2c3…
Authorization: Bearer tp_live_a1b2c3…

The full key is shown once, when it is created. Only a hash is stored. Disable a key the moment you suspect it leaked; re-enable it later if you were wrong — nothing is deleted, so usage stays attributable.

Scopes

ScopeGrants
readYour own resting and closed orders, order status, and everything public data already allows.
writePlacing and cancelling limit orders, market orders, Solana transaction builders. The wallet signature on each order is still required — the key identifies the caller, the signature authorises the trade.

What needs a credential

DataCredential
Public market data — /markets, /markets/{market}, /ticker, /book, /trades, bridge /quoteNone required. Anonymous calls count against a per-IP ceiling; with a key they count against the key's own limit instead.
A wallet's orders — /users/{user}/orders, /users/{user}/orders/closed, ?user= filtersread, and the credential must belong to that wallet.
Placing, cancelling, market orders, /tx/*write.
/settlements/report, /markets/{market}/close, /propose, /resolveOperator tokens. Not issued to integrators.

Creating keys from code

For desks running many wallets: one key per wallet, up to 100 wallets per request, no browser. Each wallet proves it consents by signing its own message; nothing else is needed, and an existing key cannot be used to create more. EVM wallets only.

  1. POST /v1/developer/keys/challenges with { "addresses": [...] } returns one message per wallet. A message works once and expires after five minutes.
  2. Each wallet signs its message with personal_sign (EIP-191) — any EVM signer, including hardware and MPC wallets.
  3. POST /v1/developer/keys/signed with { "keys": [{ message, signature, name, scopes }] } returns one result per item, in request order and labelled with the wallet's address. A key works within seconds (at most 30) of being returned, and its raw value appears in this response only.
import { Wallet } from "ethers";
import { readFileSync, writeFileSync } from "node:fs";

const API = "https://app-staging.daejin.xyz/api/v1/developer/keys";

const post = async (path, body) => {
const res = await fetch(API + path, {
  method: "POST",
  headers: { "content-type": "application/json" },
  body: JSON.stringify(body),
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
return (await res.json()).data;
};

const wallets = [];
const out = [];
// A new file per run, so keys from an earlier run are never overwritten.
const outFile = `api-keys-${Date.now()}.json`;
const save = () => writeFileSync(outFile, JSON.stringify(out, null, 2), { mode: 0o600 });

readFileSync(process.env.KEYS_FILE ?? "wallets.txt", "utf8").split("\n").forEach((line, n) => {
const pk = line.trim();
if (!pk) return;
try {
  wallets.push(new Wallet(pk));
} catch {
  out.push({ line: n + 1, error: "invalid private key" }); // never echo the value: it is a secret
}
});

for (let i = 0; i < wallets.length; i += 100) {
const batch = wallets.slice(i, i + 100);
const challenges = await post("/challenges", { addresses: batch.map((w) => w.address) });
const keys = await Promise.all(
  challenges.map(async (c, j) => ({
    message: c.message,
    signature: await batch[j].signMessage(c.message),
    name: `mm-${i + j}`,
    scopes: ["read", "write"],
  })),
);
const results = await post("/signed", { keys });
for (const r of results) {
  out.push({ address: r.address, ...(r.ok ? { apiKey: r.data.apiKey } : { error: `${r.status} ${r.error}` }) });
}
// Keys are shown once: save after every batch, so a later batch failing can't lose them.
save();
}
save();

const failed = out.filter((r) => r.error);
console.log(`${out.length - failed.length} keys written to ${outFile}, ${failed.length} failed`);
failed.forEach((r) => console.log(r.address ?? `line ${r.line}`, r.error));

Save it as create-keys.mjs, run npm install ethers@6, put one private key per line in wallets.txt (keep that file out of version control), then node create-keys.mjs on Node 18 or later. Keys land in api-keys-<timestamp>.json, readable only by you. A line that is not a valid key is reported as failed by line number, never printed, and the other wallets still go ahead.

Items succeed or fail independently. Every result carries address, the checksummed wallet its message names (the same form /challenges returns), or null if the message was not one this server issued. A failed item also carries status and error:

statusMeaning
401Message expired, already used, or signed by a different wallet. Request a new one.
409That wallet already holds five enabled keys. Disable one on the API keys page first.
400A scope other than read / write.

A malformed request (empty, more than 100 items, an invalid address, a missing name) is refused whole with 400. On production, each of the two routes accepts 30 requests per hour per IP address, so one IP can create keys for up to 3,000 wallets an hour. Keys made this way appear on the API keys page when you sign in with that wallet.

Rate limits

Limits are per key, per minute. Every call, writes included, counts against the key's own limit (default 120). Placing and cancelling orders also count against a write budget: the smaller of that limit and the venue's order budget. Every response carries the budget it was counted against (the write budget, on a write):

RateLimit-Limit: 120
RateLimit-Remaining: 117
RateLimit-Reset: 42

On refusal you get 429 with Retry-After (seconds) and a body { "error": "rate_limited", "retryAfterSeconds": 42 }. Back off for that long; a client that retries in a tight loop only extends the window.

Venue defaults, configurable per environment on the engine, all per minute:

VariableDefaultApplies to
CLOB_API_RATE_LIMIT_PUBLIC600Anonymous calls, per IP address.
CLOB_API_RATE_LIMIT_SESSION600The app's own session.
CLOB_API_RATE_LIMIT_ORDERS120Order writes. A key never exceeds this on writes even when its own limit is higher.

Migration timeline

Authentication is being introduced in three stages, switched per environment:

  1. observe — every call is identified and recorded; nothing is refused.
  2. warn — calls that will be refused receive Deprecation: true, a Sunset date and a Link header pointing here. Nothing is refused yet.
  3. required — unidentified calls to protected routes get 401 with a docs link.

If your integration sees a Deprecation header, add a key before the Sunset date.

The app session

The Daejin web app authenticates to these APIs with an httpOnly cookie it receives at login. That is for the app; integrations use API keys. There is no way to obtain the cookie outside a browser session and no reason to try.

On this page