Welcome to Agents Academy
Module 08 · Building · ~6 min
Data freshness.
By the end of this module, your agent will refuse to quote markets that have gone quiet, the cheapest safety check you can add before any kill switch, so an LLM never writes money into a market whose last real trade is minutes old.
To get there, you’ll gate every order on how recently the market actually traded, read straight from its live event feed. The cheapest risk control you can add before kill switches.
Building tier · Reference cardHow do you stop a trading agent from quoting into stale markets?
Gate every order on the age of the market’s newest real trade, read from its live event feed: if the most recent event is older than your freshness budget, the agent skips the market no matter how attractive the price looks. Every Limitless market exposes the feed at GET /markets/{slug}/events, and the CLI surfaces it as limitless orderbook events <slug>; each entry is one matched trade with a createdAt timestamp. The module wraps that into a check_freshness tool returning a safe_to_trade boolean, called before every place_limit_order. A reasonable budget is 60–120 seconds for active prediction markets. A quiet market is one where your quote sits alone and gets picked off on stale information, so this gate is the single cheapest risk control to add before kill switches.
Verified against limitless-cli@aa7ceda and the Limitless OpenAPI spec, 2026-06-09.
Section 01
What the event feed tells you.
Every market on Limitless exposes a recent-activity feed at GET /markets/{slug}/events. The limitless CLI you installed in Module 07 surfaces it as limitless orderbook events <slug>. Each entry is one real trade that happened on the book: a side, a price, a matched size, the trader, and a timestamp.
That timestamp is the whole point. The age of the newest event is your freshness signal: it answers “is anyone actually trading this market right now?” A market whose last trade was twenty minutes ago is quiet, and a quiet market is one where your quote sits alone, gets picked off on stale information, or resolves on a price that never really traded. The feed is read-only: no wallet, no signer, no order endpoints, so it is completely safe to hand an agent. The worst it can do is spend a few API requests.
Newest event
The createdAt of the top entry, the single number your gate reads
Each entry
side (0 buy / 1 sell), price, matchedSize, trader profile, txHash
Backfill
--page / --limit page back through history for a backtesting baseline
Section 02
The freshness gate tool.
Wrap limitless orderbook events <slug> --limit 1 --output json as a single agent tool. It pulls the newest event, reads its createdAt, and turns the age into a safe_to_trade boolean. Call it before every order. If the market is stale, the agent skips it even when the price looks attractive: a quiet market is where stale quotes get picked off.
How to wire this
- You already installed limitless in Module 07. Sanity-check the feed by hand: limitless orderbook events <slug> --limit 5.
- Drop the snippet into your agent runtime alongside the Module 07 wrappers, the checkFreshness tool plugs into the same registry.
- Call it before every place_limit_order. If safe_to_trade is false, the agent must pick another market or stop the loop.
- Catch the exception path explicitly, treat any failure (or an empty feed) as stale. Never trade blind.
// Module 08: Pre-trade freshness gate
//
// Backed by `limitless orderbook events <slug>` (GET /markets/{slug}/events).
// The `limitless` CLI was installed in Module 07. No extra tool needed.
import { spawn } from 'node:child_process';
// One entry of the market event feed (newest first).
// Fields per GET /markets/{slug}/events.
interface MarketEvent {
createdAt: string; // ISO timestamp of the trade
side?: number; // 0 = buy, 1 = sell
price?: number;
matchedSize?: string;
}
interface MarketEventsResponse { events: MarketEvent[]; }
// Freshness budget: skip any market whose newest trade is older than this.
const FRESHNESS_BUDGET_SEC = 90;
function fetchEvents(marketSlug: string, limit = 1): Promise<MarketEventsResponse> {
return new Promise((resolve, reject) => {
// Global flag `--output json` forces structured output.
const proc = spawn('limitless', [
'orderbook', 'events', marketSlug,
'--limit', String(limit),
'--output', 'json',
]);
let out = '';
proc.stdout.on('data', c => { out += c; });
proc.on('close', code => {
if (code !== 0) return reject(new Error(`limitless exited ${code}`));
try { resolve(JSON.parse(out)); } catch (e) { reject(e); }
});
});
}
export const checkFreshnessTool = {
name: 'check_freshness',
description: 'Check how recently a market actually traded. Returns the age of the newest event and a safe_to_trade boolean. Call this before EVERY order.',
input_schema: {
type: 'object',
properties: { market_slug: { type: 'string', description: 'Limitless market slug.' } },
required: ['market_slug'],
},
} as const;
export async function checkFreshness(input: { market_slug: string }) {
try {
const { events } = await fetchEvents(input.market_slug);
if (!events?.length) {
// No trades at all: treat as stale, not as "fine".
return JSON.stringify({ market_slug: input.market_slug, status: 'empty', safe_to_trade: false });
}
const ageSec = (Date.now() - Date.parse(events[0].createdAt)) / 1000;
return JSON.stringify({
market_slug: input.market_slug,
newest_age_sec: Math.round(ageSec),
status: ageSec <= FRESHNESS_BUDGET_SEC ? 'fresh' : 'stale',
safe_to_trade: ageSec <= FRESHNESS_BUDGET_SEC,
});
} catch (e: any) {
// Treat any failure as unsafe: never trade blind.
return JSON.stringify({
market_slug: input.market_slug,
status: 'error',
safe_to_trade: false,
error: e.message,
});
}
}
# Module 08: Pre-trade freshness gate
#
# Backed by `limitless orderbook events ` (GET /markets/{slug}/events).
# The `limitless` CLI was installed in Module 07. No extra tool needed.
import json
import subprocess
from datetime import datetime, timezone
from typing import Any
# Freshness budget: skip any market whose newest trade is older than this.
FRESHNESS_BUDGET_SEC = 90
def fetch_events(market_slug: str, limit: int = 1) -> dict[str, Any]:
# Global flag `--output json` forces structured output.
proc = subprocess.run(
["limitless", "orderbook", "events", market_slug,
"--limit", str(limit),
"--output", "json"],
capture_output = True,
text = True,
timeout = 10,
)
if proc.returncode != 0:
raise RuntimeError(f"limitless exited {proc.returncode}: {proc.stderr.strip()}")
return json.loads(proc.stdout)
check_freshness_tool: dict[str, Any] = {
"type": "function",
"function": {
"name": "check_freshness",
"description": "Check how recently a market actually traded. Returns the age of the newest event and a safe_to_trade boolean. Call this before EVERY order.",
"parameters": {
"type": "object",
"properties": {"market_slug": {"type": "string", "description": "Limitless market slug."}},
"required": ["market_slug"],
},
},
}
def check_freshness(market_slug: str) -> str:
try:
events = fetch_events(market_slug).get("events", [])
if not events:
# No trades at all: treat as stale, not as "fine".
return json.dumps({"market_slug": market_slug, "status": "empty", "safe_to_trade": False})
created = datetime.fromisoformat(events[0]["createdAt"].replace("Z", "+00:00"))
age_sec = (datetime.now(timezone.utc) - created).total_seconds()
fresh = age_sec <= FRESHNESS_BUDGET_SEC
return json.dumps({
"market_slug": market_slug,
"newest_age_sec": round(age_sec),
"status": "fresh" if fresh else "stale",
"safe_to_trade": fresh,
})
except Exception as e:
# Treat any failure as unsafe: never trade blind.
return json.dumps({
"market_slug": market_slug,
"status": "error",
"safe_to_trade": False,
"error": str(e),
})
Section 03
Decision tree.
The gate is a simple three-way branch on the age of the newest event. The agent evaluates every candidate market through this check and never proceeds past a stale or empty feed. Wire it once, forget it, trust it.
Agent considers a market
Mid-loop, LLM is about to place a trade. Call check_freshness(market_slug) first.
Fresh
Newest event within the budget. Proceed to the next risk filter (Module 14 covers hard limits). Log the age and move on.
Stale
Newest event older than the budget. Skip this market. Log the age and feed the result back to the model so it picks something else.
Empty / error
No events, or the call failed. Skip the market; if every market errors, stop the loop and alert a human, it points to an API-wide problem.
Section 04
Backfill recent history.
The same feed pages backward. limitless orderbook events <slug> --page <n> --limit <n> walks the market’s trade history, and the JSON response carries totalPages so you know when to stop. Write each event to an NDJSON file and you have a replay corpus for the backtesting you will do in later modules, plus a baseline for what “normal” trade cadence looks like on a given market.
// Module 08: Page a market's event history to NDJSON for later backtesting.
//
// Backed by `limitless orderbook events <slug> --page <n> --limit <n>`
// (GET /markets/{slug}/events?page=&limit=). The response carries
// `totalPages`, so we stop when we run out of pages.
import { execFile } from 'node:child_process';
import { promisify } from 'node:util';
import { createWriteStream, mkdirSync } from 'node:fs';
import { dirname } from 'node:path';
const run = promisify(execFile);
interface MarketEvent { createdAt: string; side?: number; price?: number; matchedSize?: string; }
interface MarketEventsResponse { events: MarketEvent[]; totalPages?: number; }
async function fetchPage(slug: string, page: number, limit: number): Promise<MarketEventsResponse> {
const { stdout } = await run('limitless', [
'orderbook', 'events', slug,
'--page', String(page),
'--limit', String(limit),
'--output', 'json',
]);
return JSON.parse(stdout);
}
async function backfill(slug: string, outPath: string, limit = 50) {
mkdirSync(dirname(outPath), { recursive: true });
const sink = createWriteStream(outPath);
let page = 1;
let totalPages = 1;
do {
const resp = await fetchPage(slug, page, limit);
for (const ev of resp.events) sink.write(JSON.stringify(ev) + '\n');
totalPages = resp.totalPages ?? page; // stop if the API omits it
console.log(`page ${page}/${totalPages}: +${resp.events.length} events`);
page += 1;
} while (page <= totalPages);
sink.end();
}
// Backfill a market's full event history to NDJSON.
backfill('btc-above-100k-weekly', './recordings/btc-weekly.ndjson');
// Later, replay offline: cat ./recordings/btc-weekly.ndjson | your-backtester
// Each line is one event (the same shape Section 02 reads for freshness).
# Module 08: Page a market's event history to NDJSON for later backtesting.
#
# Backed by `limitless orderbook events --page --limit `
# (GET /markets/{slug}/events?page=&limit=). The response carries
# `totalPages`, so we stop when we run out of pages.
import json
import subprocess
from pathlib import Path
from typing import Any
def fetch_page(slug: str, page: int, limit: int) -> dict[str, Any]:
proc = subprocess.run(
["limitless", "orderbook", "events", slug,
"--page", str(page),
"--limit", str(limit),
"--output", "json"],
capture_output = True, text = True, timeout = 10,
)
if proc.returncode != 0:
raise RuntimeError(f"limitless exited {proc.returncode}: {proc.stderr.strip()}")
return json.loads(proc.stdout)
def backfill(slug: str, out_path: str, limit: int = 50) -> None:
Path(out_path).parent.mkdir(parents=True, exist_ok=True)
with open(out_path, "w") as sink:
page, total_pages = 1, 1
while page <= total_pages:
resp = fetch_page(slug, page, limit)
for ev in resp.get("events", []):
sink.write(json.dumps(ev) + "\n")
total_pages = resp.get("totalPages", page) # stop if the API omits it
print(f"page {page}/{total_pages}: +{len(resp.get('events', []))} events")
page += 1
if __name__ == "__main__":
# Backfill a market's full event history to NDJSON.
backfill("btc-above-100k-weekly", "./recordings/btc-weekly.ndjson")
# Later, replay offline: cat ./recordings/btc-weekly.ndjson | your-backtester
# Each line is one event (the same shape Section 02 reads for freshness).
How to run this
- Confirm the CLI from Module 07 works in this shell: limitless orderbook events <slug> --limit 5 should print a table of recent trades.
- Swap the slug btc-above-100k-weekly in the last line for any active Limitless market, and lower --limit to something small while you test.
- Save the snippet above as backfill-feed.ts, then run npx tsx backfill-feed.ts.
- Save the snippet above as backfill_feed.py, then run python backfill_feed.py.
- A recordings/ folder appears with an NDJSON file, one JSON-encoded event per line. wc -l against it confirms how many events you captured across all pages.
Data freshness for agents: what people ask
Each answer also ships invisibly as schema.org FAQ data for search engines and AI assistants. Tap a question to expand.
-
What does the Limitless market event feed contain?
One entry per real trade that happened on the book, newest first: aside(0 buy / 1 sell), aprice, amatchedSize, the trader’sprofile, atxHash, and thecreatedAttimestamp your gate reads. It lives atGET /markets/{slug}/events, surfaced bylimitless orderbook events <slug>. The feed is read-only, no wallet, no signer, no order endpoints, so it is completely safe to hand an agent. -
Why is a non-empty event feed not proof a market is live?
Because a market can carry a full page of history whose newest entry is still twenty minutes old: the feed never errored and is not empty, it just stopped updating. A gate onevents.lengthalmost never fails, so the agent keeps quoting into a market nobody is trading. Gate on the age ofevents[0].createdAtinstead, never on the count. -
What freshness budget should a trading agent use?
60–120 seconds for active prediction markets; widen it for thin or long-dated ones. The module’s reference gate ships withFRESHNESS_BUDGET_SEC = 90: a newest event within budget is fresh and the loop proceeds to the next risk filter, anything older is stale and the agent skips the market, logging the age so the model picks something else. -
What should the agent do when the freshness check errors or the feed is empty?
Treat both as unsafe, never as “fine to trade”. An empty feed means no trades at all, so it counts as stale; a failed call returnssafe_to_trade: falsewith the error attached. If every market errors, stop the loop and alert a human, that points to an API-wide problem rather than one quiet market. Never trade blind. -
How do you backfill a market’s trade history?
Page the same feed backward withlimitless orderbook events <slug> --page <n> --limit <n>; the JSON response carriestotalPagesso you know when to stop. Write each event as one JSON line to an NDJSON file and you have a replay corpus for later backtesting, plus a baseline for what normal trade cadence looks like on that market.
Module checklist
Five quick confirmations.
Tick each item once you’ve actually done it. The Continue button unlocks at 5/5.
I ran limitless orderbook events <slug> --limit 5 on at least one live market and saw the recent trades
My agent calls check_freshness before placing ANY order
A stale feed (newest event older than my budget) causes the agent to skip the market and try another one
An empty feed or a failed call is treated as unsafe, never as “fine to trade”
I have backfilled at least one market’s event history to NDJSON for later replay
Module 08 complete
Freshness gated.
Your agent now has a sanity check before every trade. When a market goes quiet and stops trading, your code refuses to send orders into it, a single boolean that has saved more agents from bad fills than any prompt-engineering trick.
Concretely, you can read any market’s live event feed and gate every trade on how recently it actually traded. Three primitives that compose into the cheapest risk control in the academy.
A check_freshness tool that wraps limitless orderbook events and returns a safe_to_trade boolean from the age of the newest event, no raw feed parsing in the prompt.
A three-way decision tree, fresh proceed, stale skip, empty or error halt, that keeps the loop from quoting into a market nobody is trading.
An NDJSON backfill that pages the same feed with --page / --limit to capture a replay corpus, the same file format Module 13’s tests and any backtester you drop in will consume line-by-line.
Next up: the agents-starter repo, ready-to-run trading strategies (cross-market-mm, oracle-arb, certainty-closer) built on the official Limitless SDK, driven by a coding agent via SKILL.md.
Complete the checklist above to unlock