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 card
Quick answer

What 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.

01

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.

02

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.

03

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.

04

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

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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),
  });
}

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:

1

Role

One or two sentences. “You are a Limitless trading agent that evaluates prediction markets and places limit orders when you find edge.”

2

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.”

3

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.”

4

Output format

How the model should structure its reasoning. “After each loop iteration, output a one-line JSON summary: {market, action, reasoning, amount}.”

5

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.

Common questions

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.

  1. 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.
  2. 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 SDK OrderClient, and returns one JSON summary: filled_usd, the share-weighted avg_price (a probability in 0–1, not cents), a legs count, and order_ids.
  3. What makes a good tool name and description?
    Name verbs, not nouns: browse_markets and place_limit_order, not markets or order. Descriptions should say when NOT to use the tool, e.g. use it only when you already know the market_slug and prefer browse_markets to 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.
  4. 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.
  5. 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 respect minimum/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.

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 Limitless

Tier 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.

01

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.

02

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).

03

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.

Quick recall

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.

  1. Why pass --json to limitless-cli when an agent reads the output, but not when you read it yourself?
    --json emits 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, --json for any consumer that won’t catch the diff in code review.
  2. 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.
  3. 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.
  4. 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 duplicate place_order with different code.
  5. 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 return truncated: 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 sees truncated: true and 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