Welcome to API Academy

Module 17 · Production · ~25 min

Signals.

By the end of this module, your bot trades on actual events, earnings prints, news drops, model probabilities, instead of staring at chart shapes hoping for a pattern. The kind of input the market actually pays for.

To get there, you’ll react to events, news, and models, not chart shapes. Webhook in, sized order out, latency budgeted, and a backtest that actually resists overfitting.

Production tier · Reference card
Quick answer

How do you build a signal-based trading bot?

Wire a webhook to an order: accept the signal POST, authenticate it with a shared-secret x-signal-secret header, validate the payload, skip anything with less than 3% edge between your probability and the market price, size the trade with the capped Kelly sizer, and place an OrderType.FOK order. Every signal carries a stable id, so the order’s clientOrderId is signal-{id}: duplicate webhook deliveries return 409 Conflict instead of firing twice, and every step fails closed. The signal itself can be anything that reaches you before the market reprices: on-chain feeds, machine-readable news, your own model’s probability, macro calendars, or a TradingView alert forwarded as a webhook. Budget your latency (roughly 150 ms from signal to confirmed fill) and validate the strategy walk-forward, never on the bars it was tuned on.

Endpoints verified 2026-06-09 against the OpenAPI spec.

Section 01

Signal sources.

A signal strategy listens for an external event and reacts. The signal might be an on-chain transaction, a news headline, a model’s prediction, or a scheduled macro print. All that matters is that you see it before anyone else crossing the book does.

On-chain feeds

Whale-wallet transfers, oracle updates, large liquidations, DEX trades. Stream from your own node or a service like Alchemy.

News APIs

Machine-readable news feeds, RSS, Benzinga, Reuters JSON, plus open-source headline scrapers.

Model outputs

Your own regression, classifier, or LLM scoring an event. Publishes a probability that your bot compares to the market price.

Macro calendars

Fed decisions, CPI prints, NFP, election dates. Known in advance, trade the surprise delta.

Custom webhooks

TradingView alerts, Grafana notifications, Telegram channels forwarded through a webhook service, or a teammate’s model posting to your endpoint. The glue layer that lets anything become a tradeable signal.

Section 02

From signal to order.

A minimal dispatcher: accept the webhook, validate the payload, compute a size with your Kelly sizer from Module 14, and place the order via orderClient.createOrder. Every signal carries a stable id, so we derive a clientOrderId of the form signal-{id}, duplicate webhook deliveries return 409 Conflict instead of firing twice. Every step fails closed.

How to run this

  1. Set LIMITLESS_API_KEY, PRIVATE_KEY, and SIGNAL_SECRET (any random string), the endpoint places real FOK orders. Copy the cappedKelly / capped_kelly helper from Module 14 next to this file.
  2. Save the snippet above as ema-signal.ts, then run npx tsx ema-signal.ts.
  3. The server prints signal dispatcher listening on :8080. curl -X POST localhost:8080/signal -H "x-signal-secret: $SIGNAL_SECRET" with a valid JSON body and you’ll see orderId and settlementStatus come back. Replay the same id and you get {ok: true, duplicate: true}.
// Module 17, Signal dispatcher (webhook → validate → size → place).

import express from 'express';
import {
  HttpClient,
  MarketFetcher,
  OrderClient,
  Side,
  OrderType,
  ApiError,
} from '@limitless-exchange/sdk';
import { Wallet } from 'ethers';
import { cappedKelly } from './kelly';  // from Module 14

interface Signal {
  id:          string;        // stable per source-event → powers idempotency
  slug:        string;
  side:        'YES' | 'NO';
  probability: number;        // your estimate in [0, 1]
  marketPrice: number;        // market's current price
  source:      string;
}

const app = express();
app.use(express.json());

const httpClient    = new HttpClient({ apiKey: process.env.LIMITLESS_API_KEY });
const marketFetcher = new MarketFetcher(httpClient);
const wallet        = new Wallet(process.env.PRIVATE_KEY!);
const orderClient   = new OrderClient({ httpClient, wallet, marketFetcher });

const SHARED_SECRET = process.env.SIGNAL_SECRET ?? '';

app.post('/signal', async (req, res) => {
  if (req.header('x-signal-secret') !== SHARED_SECRET) return res.sendStatus(401);

  const sig = req.body as Signal;
  if (!sig.id || !sig.slug) return res.sendStatus(400);
  if (sig.probability <= 0 || sig.probability >= 1) return res.sendStatus(400);
  if (Math.abs(sig.probability - sig.marketPrice) < 0.03) return res.sendStatus(204); // no edge

  const fraction = cappedKelly({
    probability: sig.probability,
    marketPrice: sig.marketPrice,
    nav:         10_000,
  });
  const makerAmount = Math.floor(fraction * 10_000);
  if (makerAmount <= 0) return res.sendStatus(204);

  const market  = await marketFetcher.getMarket(sig.slug);
  const tokenId = sig.side === 'YES' ? market.positionIds[0] : market.positionIds[1];

  try {
    const result = await orderClient.createOrder({
      marketSlug:    sig.slug,
      tokenId,
      side:          Side.BUY,
      makerAmount,
      orderType:     OrderType.FOK,
      clientOrderId: `signal-${sig.id}`,       // 409 on duplicate deliveries
    });

    res.json({
      ok:               true,
      orderId:          result.order.id,
      settlementStatus: result.execution.settlementStatus,
      txHash:           result.execution.txHash ?? null,
    });
  } catch (err) {
    if (err instanceof ApiError && err.status === 409) {
      return res.json({ ok: true, duplicate: true });
    }
    throw err;
  }
});

app.listen(8080, () => console.log('signal dispatcher listening on :8080'));

Section 03

Latency budgeting.

Every signal strategy lives or dies on how long it takes the number to reach your book. Budget each step, measure each step, and know which segment is eating your edge. The SDK’s response from orderClient.createOrder includes execution.settlementStatus (UNMATCHEDMATCHEDMINEDCONFIRMED) and a txHash once the trade is on-chain, use them as your ground-truth timing marks. Target totals are tens to low hundreds of milliseconds; you’re not chasing microseconds.

Signal arrival
~20 ms
Validate & enrich
~5 ms
Decision & sizing
~10 ms
Risk check
~3 ms
Order submit
~40 ms
Fill confirm
~80 ms

Total: roughly 150 ms from signal → confirmed fill. Above ~300 ms, assume a faster bot already fronted you.

Section 04

Walk-forward validation.

The only backtest protocol that resists overfitting: train on N bars, test on the next M, slide the window forward, repeat. Pull historical OHLCV from GET /markets/{slug}/oracle-candles (Chainlink source, intervals 1m/5m/15m/1h/4h/1d) so your model trains on the exact data the market saw. Reported performance is the concatenation of every out-of-sample window, never numbers from the same bars your strategy tuned on.

// Module 17, Walk-forward validation over oracle candles.
// train on past `trainBars`, test on next `testBars`, slide forward.

import { HttpClient } from '@limitless-exchange/sdk';

interface Candle { timestamp: number; open: number; high: number; low: number; close: number; volume: number; }
interface Model  { fit(bars: Candle[]): void; predict(bar: Candle): number; }

const httpClient = new HttpClient({ apiKey: process.env.LIMITLESS_API_KEY });

// GET /markets/{slug}/oracle-candles?interval=1h&from=X&to=Y
async function fetchCandles(slug: string, from: number, to: number): Promise<Candle[]> {
  const resp = await httpClient.get(
    `/markets/${slug}/oracle-candles?interval=1h&from=${from}&to=${to}`,
  );
  return resp.rows as Candle[];
}

function walkForward(
  history: Candle[],
  model: Model,
  trainBars = 60,
  testBars  = 7,
) {
  const out: { timestamp: number; predicted: number; actual: number }[] = [];

  for (let i = trainBars; i + testBars <= history.length; i += testBars) {
    model.fit(history.slice(i - trainBars, i));
    for (const bar of history.slice(i, i + testBars)) {
      out.push({ timestamp: bar.timestamp, predicted: model.predict(bar), actual: bar.close });
    }
  }
  return out;
}

// Example: 30 days of 1h candles for BTC weekly market
const to   = Math.floor(Date.now() / 1000);
const from = to - 30 * 24 * 3600;

(async () => {
  const bars = await fetchCandles('btc-100k-weekly', from, to);
  const model: Model = { fit() {}, predict: (b) => b.close };
  const predictions  = walkForward(bars, model);
  console.log(predictions.length, 'out-of-sample predictions');
})().catch(console.error);

How to run this

  1. Set LIMITLESS_API_KEY, this is a pure read over oracle candles, no orders placed. Swap in your own fit / predict implementation for the dummy model before the numbers mean anything.
  2. Save the snippet above as walk-forward.ts, then run npx tsx walk-forward.ts.
  3. The process prints a count like 670 out-of-sample predictions, every row was scored by a model that had never seen that bar. That’s the number your Sharpe and drawdown from Module 13 should be computed on.
Common questions

Signal-based trading: what people ask

Each answer also ships invisibly as schema.org FAQ data for search engines and AI assistants. Tap a question to expand.

  1. How do you secure a webhook that can place trades?
    Authenticate every request with a shared secret before acting: the dispatcher rejects anything whose x-signal-secret header doesn’t match the SIGNAL_SECRET env var with a 401. Then validate the payload, a stable id, a market slug, and a probability strictly between 0 and 1, returning 400 on anything malformed. Every step fails closed: no valid secret, no valid payload, no order.
  2. How do you stop duplicate webhook deliveries from double-trading?
    Derive the order’s idempotency key from the signal: a clientOrderId of the form signal-{id}, where the id is stable per source event. When a webhook is delivered twice, the second submission returns 409 Conflict and the dispatcher answers {ok: true, duplicate: true} instead of placing a second trade. Replay the same id on purpose to prove the dedupe works before going live.
  3. How fast does a signal strategy need to be?
    Tens to low hundreds of milliseconds end-to-end; you’re not chasing microseconds. The budget: ~20 ms signal arrival, ~5 ms validate and enrich, ~10 ms decision and sizing, ~3 ms risk check, ~40 ms order submit, ~80 ms fill confirm, roughly 150 ms total. Above ~300 ms, assume a faster bot already fronted you. Use the order response’s settlementStatus (UNMATCHEDMATCHEDMINEDCONFIRMED) and txHash as ground-truth timing marks.
  4. What is walk-forward validation?
    The only backtest protocol that resists overfitting: train on N bars, test on the next M, slide the window forward, repeat (the example uses 60 train bars and 7 test bars over hourly candles from GET /markets/{slug}/oracle-candles). Reported performance is the concatenation of every out-of-sample window, never numbers from the bars the strategy tuned on. Compute your Sharpe and drawdown on those out-of-sample predictions only.
  5. Why do signal strategies that backtest well fail live?
    Usually signal lag. A backtest treats the signal’s computed-at timestamp as the moment it was actionable; live, you can’t act until it reaches your server, and an 8-second median lag turns “buy on signal” into “buy after the move”, which erases the edge in news-driven strategies where the move happens in the first minute. Backtest with the arrival timestamp and add a deliberate delay equal to your live feed’s p95 latency; if the strategy survives that handicap, you have something.

Section 05

Module checklist.

Tick each item once you’ve actually done it. The Continue button unlocks at 5/5.

Module 17 complete

Signals wired.

Your bot acts on what’s happening in the world, not on what a candle looks like. Webhook fires, signal validates, position sizes itself, order goes out, all under a latency budget you control. The way real systematic desks make money.

Concretely, webhooks fire, your bot reacts, and your backtest actually resists overfitting. Three things you walk away with:

01

A webhook signal endpoint that authenticates on x-signal-secret, skips trades with less than 3% edge, sizes with cappedKelly, and dedupes replays through a signal-{id} clientOrderId.

02

A latency budget that names every stage from signal arrival through confirmed fill, roughly 150 ms end-to-end, and the settlementStatus / txHash fields you use as ground-truth timestamps.

03

A walkForward() loop over /markets/{slug}/oracle-candles that trains on N bars, tests on the next M, slides forward, and reports only concatenated out-of-sample predictions.

Next up: wrapping everything you’ve built into a deployable production bot, config, health checks, graceful shutdown, Docker, and the kill switches that separate a hobby script from a service.

Complete the checklist above to unlock