Welcome to Agents Academy
Module 10 · Building · ~8 min
Custom skills.
By the end of this module, your agent can do anything the SDK can do, but in a single tool call. Multi-step trading actions wrapped behind one skill the LLM can use without micromanaging every step.
To get there, you’ll learn how to write custom agent skills that compose multiple SDK calls behind a single tool, wire them alongside your CLI wrappers, and author a system prompt that behaves like a contract instead of a pep talk. This closes out the Building tier with an agent that mixes read-side CLI tools, write-side native skills, and a prompt the model actually follows, the full scaffold Production-tier modules deploy, monitor, and harden.
Building tier · Reference cardWhat is a custom skill for an LLM trading agent?
A custom skill is a tool you write that composes multiple SDK calls into one semantic action, so the LLM makes a single call instead of micromanaging every step. The worked example is a smart order router, smart_buy_yes: it fetches the market and orderbook, walks the asks within a basis-point slippage budget (max_slippage_bp, default 50 = 0.5%), splits the requested USD across multiple GTC limit orders via the official @limitless-exchange/sdk OrderClient, and returns one JSON summary; the LLM never sees the orderbook. Reach for a custom skill when CLI wrappers fall short: multi-step workflows, low-latency paths (subprocess spawns cost 20–60ms), custom risk filters that belong in code, or a proprietary signal computed offline. The end state is a hybrid registry, CLI wrappers on the read path and native SDK skills on the write path, plus a five-section system prompt that behaves like a contract instead of a pep talk.
Limitless surface verified 2026-06-09 against the SDK + API.
Prerequisite: the smart-order-router example in Section 02 borrows market-making concepts; API Academy Module 15 walks through the quoting mechanics in ~10 minutes. Optional but recommended.
Section 01
When built-in tools fall short.
The four default tools from Module 09 will get you surprisingly far. But every agent eventually hits a wall where the CLI wrappers are too coarse, you need something that composes multiple SDK calls into a single semantic action. That is what custom skills are for.
Multi-step workflows
“Close half my position if it is up more than 30%” is four CLI calls. Let the skill do the arithmetic; the LLM should not.
Low-latency requirements
Subprocess spawns cost 20–60ms. If your skill runs inside the loop ten times, that adds up. Native SDK calls are faster.
Custom risk filters
Your own rules: “never buy NO above 0.75”, “skip markets with less than $10k open interest”. These belong in code, not the prompt.
Proprietary strategy
If the edge is a signal you computed offline, expose it as a tool. Do not try to explain it to the LLM in the prompt.
Section 02
A custom smart order router.
A concrete example. The LLM says “buy $5 of YES on market btc-above-100k-2026-06-01 with at most 50bp slippage.” The skill fetches the market and orderbook, walks the asks within the slippage budget, splits the size into several GTC limit orders, fires them in parallel via the official @limitless-exchange/sdk OrderClient, and returns a single summary. The LLM never sees the orderbook, it just gets the outcome.
How to run this
- Install: npm install @limitless-exchange/sdk ethers dotenv. Set LIMITLESS_API_KEY and PRIVATE_KEY in your .env. Use a dedicated trading wallet, never your main.
- Install: pip install limitless-sdk eth-account. Set LIMITLESS_API_KEY and PRIVATE_KEY in your environment. Python 3.9+ required; the SDK is async-first.
- Approve the venue once before the first live order. Fetch any active market and approve its venue.exchange for both USDC (BUYs) and CTF (SELLs). NegRisk markets also require approving venue.adapter. See docs.limitless.exchange → Trading & Orders → Token Approvals for the one-time setup.
- Pull a real CLOB slug for the smoke test from marketFetcher.getActiveMarkets({ limit: 5 }), slugs look like btc-above-100k-2026-06-01. Pick one with both bids and asks.
- Save the snippet as smart-buy.ts wherever your runtime LLM agent project lives. Register smartBuyYesTool in your agent’s tool list (the Module 04–06 pattern). Smoke test: await smartBuyYes({ market_slug: '<slug>', target_size_usd: 5 }). Start with a tiny size while you watch fills.
- Save the snippet as smart_buy.py, register smart_buy_yes_tool in your agent’s tool list, then run asyncio.run(smart_buy_yes('<slug>', 5)). Remember to await http_client.close() on shutdown.
- Returned JSON includes ok: true, a filled_usd amount, the share-weighted avg_price (probability in 0–1), a legs count, and the order_ids array, proof the router split the order across levels without exceeding your slippage budget.
// Module 10: smart-buy.ts (a custom skill for your runtime LLM agent)
//
// Built on the official @limitless-exchange/sdk so it runs anywhere: no
// agents-starter clone required. Walks the asks, splits the requested USD
// across multiple GTC limit orders within a basis-point slippage budget,
// and returns a single JSON summary the LLM can act on.
//
// Verified against docs.limitless.exchange/developers/sdk/typescript/orders.
import { ethers } from 'ethers';
import {
HttpClient,
MarketFetcher,
OrderClient,
OrderType,
Side,
} from '@limitless-exchange/sdk';
import 'dotenv/config';
const httpClient = new HttpClient({
baseURL: 'https://api.limitless.exchange',
apiKey: process.env.LIMITLESS_API_KEY,
});
const wallet = new ethers.Wallet(process.env.PRIVATE_KEY!);
const marketFetcher = new MarketFetcher(httpClient);
const orderClient = new OrderClient({ httpClient, wallet, marketFetcher });
export const smartBuyYesTool = {
name: 'smart_buy_yes',
description: 'Buy YES on a Limitless CLOB market with slippage control. Fetches the market and orderbook, walks the asks within a basis-point budget, splits the requested USD across multiple GTC limit orders. Use only when you already know the market_slug, prefer browse_markets to discover new ones. A mirror smart_buy_no skill targets the NO token.',
input_schema: {
type: 'object',
properties: {
market_slug: { type: 'string' },
target_size_usd: { type: 'number', minimum: 1 },
max_slippage_bp: { type: 'number', default: 50, description: 'Max slippage in basis points (50 = 0.5%)' },
},
required: ['market_slug', 'target_size_usd'],
},
} as const;
interface SmartBuyYesArgs {
market_slug: string;
target_size_usd: number;
max_slippage_bp?: number;
}
export async function smartBuyYes(args: SmartBuyYesArgs): Promise<string> {
const maxBp = args.max_slippage_bp ?? 50;
// Fetch the market once. MarketFetcher caches venue + token IDs internally
// so the OrderClient signing path stays fast across the split legs.
const market = await marketFetcher.getMarket(args.market_slug);
// OrderbookLevel: { price: number (0–1, probability), size: number (shares) }.
const book = await marketFetcher.getOrderBook(args.market_slug);
const levels = book.asks;
if (!levels?.length) {
return JSON.stringify({ ok: false, reason: 'empty asks' });
}
const refPrice = levels[0].price;
const worstPrice = refPrice * (1 + maxBp / 10_000);
// Walk the asks. usd-at-level = price × shares.
const plan: { price: number; shares: number }[] = [];
let remainingUsd = args.target_size_usd;
for (const lvl of levels) {
if (remainingUsd <= 0) break;
if (lvl.price > worstPrice) break;
const usdAtLevel = lvl.price * lvl.size;
const takeUsd = Math.min(remainingUsd, usdAtLevel);
plan.push({ price: lvl.price, shares: takeUsd / lvl.price });
remainingUsd -= takeUsd;
}
if (plan.length === 0) {
return JSON.stringify({ ok: false, reason: 'no liquidity within slippage budget', ref_price: refPrice });
}
// Each leg is a GTC limit order. OrderClient handles EIP-712 signing,
// profile-id resolution, and venue caching.
const results = await Promise.all(plan.map(p =>
orderClient.createOrder({
marketSlug: args.market_slug,
tokenId: market.positionIds[0], // YES token
side: Side.BUY,
price: p.price, // probability 0–1 (NOT cents)
size: p.shares, // number of shares
orderType: OrderType.GTC,
})
));
const filledUsd = args.target_size_usd - remainingUsd;
const filledShares = plan.reduce((s, p) => s + p.shares, 0);
return JSON.stringify({
ok: true,
filled_usd: filledUsd,
unfilled: remainingUsd,
avg_price: filledShares > 0 ? filledUsd / filledShares : 0, // VWAP
legs: results.length,
order_ids: results.map(r => r.order.id),
});
}
# Module 10: smart_buy.py (a custom skill for your runtime LLM agent)
#
# Async-first via the official limitless-sdk Python package.
# Verified against docs.limitless.exchange/developers/sdk/python/orders.
import asyncio
import json
import os
from typing import Any
from eth_account import Account
from limitless_sdk.api import HttpClient
from limitless_sdk.markets import MarketFetcher
from limitless_sdk.orders import OrderClient
from limitless_sdk.types import Side, OrderType
http_client = HttpClient() # auto-loads LIMITLESS_API_KEY from env
account = Account.from_key(os.environ["PRIVATE_KEY"])
market_fetcher = MarketFetcher(http_client)
order_client = OrderClient(http_client, account)
smart_buy_yes_tool: dict[str, Any] = {
"type": "function",
"function": {
"name": "smart_buy_yes",
"description": (
"Buy YES on a Limitless CLOB market with slippage control. Fetches "
"the market and orderbook, walks the asks within a basis-point "
"budget, splits the requested USD across multiple GTC limit "
"orders. Use only when you already know the market_slug: prefer "
"browse_markets to discover new ones."
),
"parameters": {
"type": "object",
"properties": {
"market_slug": {"type": "string"},
"target_size_usd": {"type": "number", "minimum": 1},
"max_slippage_bp": {"type": "number", "default": 50,
"description": "Max slippage in basis points (50 = 0.5%)"},
},
"required": ["market_slug", "target_size_usd"],
},
},
}
async def smart_buy_yes(
market_slug: str,
target_size_usd: float,
max_slippage_bp: float = 50,
) -> str:
# Fetch the market once. MarketFetcher caches venue + token IDs.
market = await market_fetcher.get_market(market_slug)
# Orderbook level: { price: float (0–1), size: float (shares) }
book = await market_fetcher.get_order_book(market_slug)
levels = book.asks
if not levels:
return json.dumps({"ok": False, "reason": "empty asks"})
ref_price = levels[0].price
worst_price = ref_price * (1 + max_slippage_bp / 10_000)
# Walk the asks. usd-at-level = price × shares.
plan: list[tuple[float, float]] = [] # [(price, shares), ...]
remaining_usd = target_size_usd
for lvl in levels:
if remaining_usd <= 0:
break
if lvl.price > worst_price:
break
usd_at_level = lvl.price * lvl.size
take_usd = min(remaining_usd, usd_at_level)
plan.append((lvl.price, take_usd / lvl.price))
remaining_usd -= take_usd
if not plan:
return json.dumps({"ok": False, "reason": "no liquidity within slippage budget",
"ref_price": ref_price})
# Each leg is a GTC BUY on the YES token.
receipts = await asyncio.gather(*[
order_client.create_order(
token_id = market.tokens.yes,
price = price,
size = shares,
side = Side.BUY,
order_type = OrderType.GTC,
market_slug = market_slug,
)
for price, shares in plan
])
filled_usd = target_size_usd - remaining_usd
filled_shares = sum(shares for _, shares in plan)
return json.dumps({
"ok": True,
"filled_usd": filled_usd,
"unfilled": remaining_usd,
"avg_price": filled_usd / filled_shares if filled_shares > 0 else 0, # VWAP
"legs": len(receipts),
})
Section 03
Combining CLI + custom tools.
The hybrid pattern: wrap read-heavy operations with the CLI (cheap, stable, no SDK setup) and write custom SDK-backed tools for write operations where you need precise control. Your tool registry ends up looking like a bilingual dictionary, CLI wrappers for observation, native SDK code for action.
CLI (Modules 07 + 08)
- · browse_markets (M07)
- · place_limit_order (M07)
- · check_freshness (M08)
- · get_market_details, get_orderbook, get_positions, cancel_order (yours, by analogy)
Read path. Stable, cheap, easy to replace.
Custom SDK
- • smart_buy (this module)
- • partial_exit
- • rebalance_to_target
- • cancel_all_on_market
- • proprietary_signal
Write path. Fast, typed, you own every line.
The LLM does not care which path a tool takes. From its perspective it is just calling smart_buy or browse_markets, the implementation is your problem.
Section 04
Tool library organisation.
As your tool count grows, the LLM starts struggling to pick the right one. A library of 25 tools with overlapping descriptions will produce worse decisions than a library of 8 sharp ones. Organise aggressively.
Name verbs, not nouns
browse_markets, place_limit_order. Not markets or order.
Descriptions include when NOT to use
“Use this only when you already know the market_slug, prefer browse_markets to discover new ones.”
Validate inputs in the handler
Don’t trust the LLM to respect minimum/maximum. Re-check and reject invalid calls with a helpful error string.
Scope tools per agent
A research agent does not need write tools. Pass only the tools each agent role actually needs, smaller surface, fewer mistakes.
Section 05
The system prompt is a contract.
Your system prompt is not a pep talk. It is a contract between you and the model that defines, in terms the model will read every single run, what the agent is, what it cares about, and what it will not do. Most failing agents have a failing system prompt, too long, too vague, too hopeful.
A well-structured agent prompt has five sections, in this order, and rarely exceeds 600–800 words:
Role
One or two sentences. “You are a Limitless trading agent that evaluates prediction markets and places limit orders when you find edge.”
Invariants
Rules the model must never violate. “Never place an order larger than $50. Never buy NO above 0.75. Never act on markets you have not browsed first.”
Decision boundaries
When to act, when to skip, when to escalate. “If your confidence is below 60%, log the market and move on. If an order would exceed daily risk budget, stop the loop and report.”
Output format
How the model should structure its reasoning. “After each loop iteration, output a one-line JSON summary: {market, action, reasoning, amount}.”
Stop conditions
When the agent is done. “Stop after 5 iterations or when daily risk budget is exhausted, whichever comes first.”
Anti-pattern: The God Prompt
A 3,000-word system prompt that tries to handle every edge case in prose. The model reads the whole thing once and then forgets most of it. Put edge-case logic in tool code (where it is enforced), not in the prompt (where it is begged for). If you find yourself adding a sixth section, ask where it really belongs, it is almost never the prompt.
Custom agent skills: what people ask
Each answer also ships invisibly as schema.org FAQ data for search engines and AI assistants. Tap a question to expand.
-
When does your agent need a custom skill instead of a CLI wrapper?
Four triggers. Multi-step workflows: “close half my position if it is up more than 30%” is four CLI calls, and the skill should do the arithmetic, not the LLM. Low latency: subprocess spawns cost 20–60ms, which adds up inside a loop. Custom risk filters: rules like “never buy NO above 0.75” belong in code, not the prompt. Proprietary strategy: if the edge is a signal you computed offline, expose it as a tool rather than explaining it in the prompt. -
How does the smart_buy_yes order-router skill work?
The LLM asks for, say, $5 of YES with at most 50bp slippage. The skill fetches the market and orderbook, computes a worst price from the best ask plus the budget, walks the ask levels, splits the size into several GTC limit orders fired in parallel through the SDKOrderClient, and returns one JSON summary:filled_usd, the share-weightedavg_price(a probability in 0–1, not cents), alegscount, andorder_ids. -
What makes a good tool name and description?
Name verbs, not nouns:browse_marketsandplace_limit_order, notmarketsororder. Descriptions should say when NOT to use the tool, e.g. use it only when you already know themarket_slugand preferbrowse_marketsto discover new ones. And never let the description drift from the implementation: when behaviour changes, change the description first, because it is the model’s only handle on the tool and the model always believes it. -
What goes in an agent’s system prompt?
Five sections, in order, rarely exceeding 600–800 words: role (one or two sentences), invariants (rules never to violate, like an order-size cap), decision boundaries (when to act, skip, or escalate), output format (a one-line JSON summary per iteration), and stop conditions. The anti-pattern is the 3,000-word God Prompt that handles every edge case in prose: put edge-case logic in tool code, where it is enforced, not in the prompt, where it is begged for. -
Should read tools and write tools be built the same way?
No. The hybrid pattern wraps read-heavy operations with the CLI (cheap, stable, no SDK setup) and writes custom SDK-backed tools for write operations needing precise control:smart_buy,partial_exit,cancel_all_on_market. The LLM does not care which path a tool takes. Two extra rules: validate inputs in the handler, because the LLM will not reliably respectminimum/maximum, and scope tools per agent, a research agent gets no write tools.
Module checklist
Five quick confirmations.
Tick each item once you’ve actually done it. The Continue button unlocks at 5/5.
I can identify when the CLI wrappers fall short and a custom skill is needed
I wrote at least one custom skill that composes multiple SDK calls
My tool library mixes CLI wrappers (read) and custom SDK calls (write)
Every custom skill validates its inputs and returns a readable error on failure
Tool names are verbs, descriptions say when NOT to use them
Take it live
Build with what you’ve learned.
You’ve moved from runtime tool-use loops through coding-agent-orchestrated strategies to your own custom skills. The full Building-tier scaffold is ready to deploy on real markets, start small, dry-run first, and let the Production tier harden it.
Start trading on LimitlessTier 2 complete · Building
Building complete.
Your agent has its own playbook. Instead of asking the LLM to compose four SDK calls in the right order, you hand it one skill that already knows how to split a position across the book, manage slippage, and report a clean answer, the difference between a chatty agent and a working one.
Concretely, your agent can do anything the SDK can, through CLI wrappers for read paths and native SDK skills for write paths.
A smart_buy_yes skill that fetches the market and orderbook, walks the asks within a basis-point slippage budget, splits the size across multiple GTC limit orders via the official @limitless-exchange/sdk, and returns a single JSON summary the LLM can chain off, no per-level prompting required.
A hybrid tool registry: CLI wrappers from Modules 07 + 08 covering the read path (browse_markets, place_limit_order, check_freshness) sitting next to native SDK skills on the write path (smart_buy_yes, partial_exit, cancel_all_on_market).
A five-section system prompt, role, invariants, decision boundaries, output format, stop conditions, that behaves like a contract instead of a 3,000-word pep talk.
Without scrolling back, can you answer these?
Five questions across the Building tier. Click each to reveal, the test is whether you can answer first.
-
Why pass
--jsonto limitless-cli when an agent reads the output, but not when you read it yourself?--jsonemits parseable structured output (objects, arrays); the human format reformats with version bumps and breaks parsers silently. Your eye reads field labels and column headers; an agent’s parser assumes a fixed shape and silently mis-extracts when the human format shifts a column. Human format for humans,--jsonfor any consumer that won’t catch the diff in code review. -
The freshness gate reports a market’s newest event is 20 minutes old. What does your loop do?
Skip the market; don’t place new orders into it. Don’t try to be clever, a stale feed means the price you’d quote against is unreliable, and new entries on a market nobody is trading are how you accumulate phantom positions and get picked off. Existing positions stay in place (you still need price refreshes to manage them), but no new size until the market starts trading again. Freshness is a gate, not a hint. -
Agent state lives in a file, not in the prompt. Why?
Prompts have a token budget; state grows unboundedly. Stuffing position state into the prompt corrupts on the first mismatch (the model can’t reliably edit its own context), bloats every call (wasted tokens), and disappears between conversations. A file with atomic-rename writes is queryable, replayable, and survives restarts. The model reads from it via a tool and writes back via a tool, never inline in the chat. -
Two skills in different files share the name
place_order. What does the model do?Picks at random, sometimes one, sometimes the other. Skill names are the model’s only handle on which tool to call; when two skills share a name, there’s nothing left to disambiguate on. Use one name per skill, one impl. If you need versions, namespace them:place_order_v2, not duplicateplace_orderwith different code. -
A tool result returns 50 KB of market data. The agent’s context starts to drown. What’s the fix?
Cap output at ~4 kB and returntruncated: true. Anything bigger drowns attention and slows the loop. Truncate large lists by relevance (top-N by some score), not by first-N, and emit a follow-up tool the model can call to fetch detail on demand. The model seestruncated: trueand learns to ask for more, silent truncation just makes it work on a partial view it doesn’t know is partial.
Next up: Production tier. Deployment, monitoring, tests, kill switches, and prompt-injection defenses that keep the agent alive once it’s out of your terminal.
Complete the checklist above to unlock