accueil

Premiers pas

Démarrage rapide

Du premier appel au premier ordre sur Seeryx : lire une manche et son carnet, se connecter avec un portefeuille Solana, passer un ordre et l’annuler.

Lire un marché

Les données de marché sont publiques. Demandez la manche BTC de 5 minutes en cours, puis son carnet d’ordres et ses dernières transactions.

quickstart.mjs
// quickstart.mjs: the live BTC 5-minute round, its order book and its last trades. node quickstart.mjs
import { apiUrl } from './api-url.mjs';
const BASE = 'https://seeryx.com';
const get = async (path) => (await fetch(apiUrl(BASE, path), { redirect: 'error' })).json();

const round = await get('/markets/BTC/5m/current');
const up = await get(`/orderbook/${round.slug}?side=up`);
const trades = await get(`/trades/${round.slug}`);

const bid = up.bids[0]?.[0], ask = up.asks[0]?.[0]; // levels are [cents, shares], best first
console.log({ round: round.slug, openedAt: Number(round.open), ends: new Date(round.end * 1000).toISOString() });
const chance = bid && ask ? `${(bid + ask) / 2}%` : 'no quotes yet'; // the middle of bid and ask
console.log({ upBid: bid ?? '-', ask: ask ?? '-', chanceOfUp: chance });
console.log({ lastTrade: trades[0] ?? 'none yet' });
Réponse
{ "slug": "btc-updown-5m-1791916200", "asset": "BTC", "tf": "5m", "start": 1791916200, "end": 1791916500,
  "open": 83082.005, "close": null, "outcome": null, "status": "live" }

{ "bids": [[40, 100], [39, 100]], "asks": [[49, 100], [50, 100]] }

[{ "id": 13, "side": "down", "action": "buy", "shares": 10, "price": 53, "time": 1791916330063 }]
La manche, le carnet Hausse sous forme de niveaux [cents, parts] en commençant par le meilleur, et les dernières transactions.

Préparer un projet

mkdir seeryx-bot && cd seeryx-bot
npm init -y && npm pkg set type=module
npm i @solana/kit && npm i -D tsx
# a wallet for the bot alone: a JSON array of 64 numbers, as solana-keygen writes it
solana-keygen new -o wallet.json && chmod 600 wallet.json

Déposez quelques USDC sur le compte Seeryx de ce portefeuille avant de négocier : sur le site (connectez le même portefeuille) ou par le code, voir Dépôts et retraits. Le portefeuille n’a pas besoin de SOL.

Se connecter

Enregistrez le client ci-dessous sous seeryx.ts (ou seeryx.py), avec api-url.mjs à côté pour TypeScript. Il se connecte avec le portefeuille et envoie le jeton avec chaque requête ; Authentification explique chaque étape.

seeryx.ts
// seeryx.ts: a small client for the Seeryx API. Node 22+, npm i @solana/kit
import { readFileSync } from 'node:fs';
import { createKeyPairFromBytes, getAddressFromPublicKey, getBase58Decoder, signBytes } from '@solana/kit';
import type { Address } from '@solana/kit';
import { apiUrl } from './api-url.mjs';

export const BASE = process.env.SEERYX_URL ?? 'https://seeryx.com';
export const ACCESS = process.env.SEERYX_ACCESS ?? ''; // only on a site with access codes
let token = '';

export interface Round {
  slug: string; start: number; end: number; open: number | null; close: number | null;
  outcome: 'up' | 'down' | null; status: 'upcoming' | 'live' | 'settled';
}
export interface Book { bids: [price: number, shares: number][]; asks: [price: number, shares: number][] }
export interface Account {
  positions: { slug: string; side: 'up' | 'down'; shares: number; cost: number }[];
  orders: {
    id: number; slug: string; side: 'up' | 'down'; action: 'buy' | 'sell'; shares: number; limit: number;
  }[];
  balance: { available: number; total: number };
}

export class ApiError extends Error {
  constructor(
    message: string, readonly status: number, readonly code?: string, readonly retryAfter?: number,
  ) {
    super(message);
  }
}

// One request. Writes get a fresh Idempotency-Key unless you pass the key of the attempt you are retrying.
export async function api<T>(path: string, init: { method?: string; body?: unknown; key?: string } = {}) {
  const method = init.method ?? 'GET';
  const headers: Record<string, string> = { 'Content-Type': 'application/json' };
  if (token) headers['X-Auth-Token'] = token;
  if (ACCESS) headers['X-Access-Token'] = ACCESS;
  if (method !== 'GET') headers['Idempotency-Key'] = init.key ?? crypto.randomUUID();
  const body = init.body === undefined ? undefined : JSON.stringify(init.body);
  const res = await fetch(apiUrl(BASE, path), { method, headers, body, redirect: 'error' });
  const data = await res.json().catch(() => ({}));
  if (!res.ok) {
    const wait = Number(res.headers.get('Retry-After')) || undefined;
    throw new ApiError(data.error ?? `HTTP ${res.status}`, res.status, data.code, wait);
  }
  return data as T;
}

export interface Wallet { pair: CryptoKeyPair; address: Address }

// A wallet file as solana-keygen writes it: a JSON array of 64 numbers.
export async function loadWallet(path = process.env.SEERYX_WALLET ?? 'wallet.json'): Promise<Wallet> {
  const pair = await createKeyPairFromBytes(new Uint8Array(JSON.parse(readFileSync(path, 'utf8'))));
  return { pair, address: await getAddressFromPublicKey(pair.publicKey) };
}

// Sign-In With Solana: ask for the message, check it, sign its exact bytes, trade the signature for a token.
export async function signIn(wallet: Wallet) {
  const { address } = wallet;
  const { message } = await api<{ message: string }>('/auth/nonce', { method: 'POST', body: { address } });
  const [first, second] = message.split('\n');
  const site = `${new URL(BASE).hostname} wants you to sign in with your Solana account:`;
  if (first !== site || second !== address) {
    throw new Error('Unexpected sign-in message: not this site or not this wallet');
  }
  const bytes = await signBytes(wallet.pair.privateKey, new TextEncoder().encode(message));
  const signature = getBase58Decoder().decode(bytes);
  const body = { address, message, signature };
  ({ token } = await api<{ token: string }>('/auth/verify', { method: 'POST', body }));
  return token; // valid for 7 days
}
JavaScriptapi-url.mjs
// Keep server-supplied identifiers inside the configured API. Never follow redirects with account headers.
export function apiUrl(base, path) {
  const origin = new URL(base);
  if (!['http:', 'https:'].includes(origin.protocol) || origin.username || origin.password || origin.pathname !== '/' || origin.search || origin.hash) {
    throw new Error('API base must be an HTTP(S) origin without credentials');
  }
  const pathname = path.split('?')[0];
  if (!/^\/[A-Za-z0-9_-]+(?:\/[A-Za-z0-9_-]+)*\/?$/.test(pathname) || /[\u0000-\u0020\u007f#\\]/.test(path)) {
    throw new Error('Invalid API path');
  }
  const url = new URL(`/api${path}`, origin);
  if (url.origin !== origin.origin || !url.pathname.startsWith('/api/')) throw new Error('Invalid API destination');
  return url;
}

// JSON escapes terminal controls; explicitly remove log entry separators as well.
export const logValue = (value) => JSON.stringify(value)?.replace(/[\n\r]/g, '_').replace(/[\u2028\u2029]/g, '_');
Maintient chaque requête dans le périmètre de l’API et échappe les valeurs du serveur avant de les journaliser.

Passer un ordre

Sur seeryx.com, les ordres sont signés par une clé de session de trading. Enregistrez chain.ts et message.ts (ou chain.py et message.py) depuis Signature des ordres, puis lancez ce code : il ouvre une session (une signature du portefeuille, sans SOL), place dans le carnet un achat de 5 parts sous le prix du marché, puis l’annule.

first-order.ts
// first-order.ts: sign in, rest one small buy order below the market, then cancel it. npx tsx first-order.ts
import { api, loadWallet, signIn, type Book, type Round } from './seeryx';
import { signOrder, tradingSession, type Funds, type Order } from './chain';

const wallet = await loadWallet();
await signIn(wallet);
const funds = await api<Funds>('/funds');
// one wallet signature opens a trading session; its key then signs every order
const session = funds.onChainTrading ? await tradingSession(funds, wallet) : null;

const round = await api<Round>('/markets/BTC/5m/current');
const book = await api<Book>(`/orderbook/${round.slug}?side=up`);
const bid = book.bids[0]?.[0] ?? 10; // the best Up bid in cents
// 5 Up shares 5c under the best bid: it rests in the book (postOnly makes sure it never trades at once)
const order: Order = {
  slug: round.slug, side: 'up', action: 'buy', type: 'limit', shares: 5, limit: Math.max(1, Math.floor(bid) - 5), postOnly: true,
};
const body = session ? await signOrder(funds, session, wallet.address, order) : order;
const placed = await api<{ order: number | null }>('/orders', { method: 'POST', body });
console.log('resting order', placed.order, 'at', order.limit, 'c');

console.log((await api<{ orders: { id: number }[] }>('/account')).orders);
if (placed.order) console.log(await api(`/orders/${placed.order}`, { method: 'DELETE' })); // { success: true }

L’annuler

curl
# one order
curl -s -X DELETE https://seeryx.com/api/orders/65365 -H "X-Auth-Token: $TOKEN" -H "Idempotency-Key: $(uuidgen)"
# {"success":true}

# everything still resting in one round
curl -s -X DELETE "https://seeryx.com/api/orders?slug=btc-updown-5m-1791916200" -H "X-Auth-Token: $TOKEN" -H "Idempotency-Key: $(uuidgen)"
# {"success":true,"cancelled":[65366]}

Étapes suivantes