Welcome to Agents Academy
Module 07 · Building · ~6 min
Limitless-cli as a tool.
By the end of this module, your agent can browse markets, read positions, and place orders by running the same shell commands a human trader uses, the fastest possible way to give an LLM hands on Limitless, with no SDK wrapper of your own to maintain.
To get there, you’ll take the fastest path to giving an agent hands on Limitless. Spawn the binary as a subprocess, parse --output json, and wrap each subcommand as a typed tool.
Building tier · Reference cardWhat is limitless-cli?
limitless-cli is a Rust command-line front end to the Limitless API; the installed binary is called limitless, and it can list markets, fetch orderbooks, place and cancel trades, read portfolio state, manage token approvals, and create wallets. Every subcommand supports --output json, returning structured stdout and a non-zero exit code on error, which is exactly the shape an agent runtime wants: spawn the binary as a subprocess, parse the JSON, and wrap each subcommand as a typed tool. Credentials resolve in priority order, CLI flags, then the LIMITLESS_API_KEY and LIMITLESS_PRIVATE_KEY env vars, then ~/.config/limitless/config.json, and the chain is Base mainnet (chainId 8453). For an agent builder it is the lowest-friction way to give an LLM hands on Limitless: a shared, language-agnostic surface with no SDK wrapper of your own to maintain.
Verified 2026-06-09 where it touches Limitless; the rest is illustrative agent-runtime teaching.
Prerequisites: the CLI’s markets list and trading create wrap the endpoints covered in API Academy Module 04 and Module 05, ~10 minutes each. Optional but recommended.
Section 01
What limitless-cli is.
github.com/limitless-labs-group/limitless-cli is a Rust command-line front end to the Limitless API. The binary is called limitless (not limitless-cli). It can list markets, fetch orderbooks, place and cancel trades, read portfolio state, manage token approvals, and create wallets. Every subcommand supports --output json, which returns structured output on stdout and exits non-zero on error, the two shapes an agent runtime actually wants.
Install from Homebrew or build from source. Credentials resolve in priority order: CLI flags > env vars (LIMITLESS_API_KEY, LIMITLESS_PRIVATE_KEY) > config file at ~/.config/limitless/config.json. The chain is Base mainnet (chainId 8453); the binary is self-contained, so your container image stays small. If you are wiring an LLM to Limitless for the first time, this is the lowest-friction path.
Read
limitless markets list | search | get | categories
limitless orderbook book | price | monitor
limitless portfolio positions | trades | pnl
limitless trading orders
Write
limitless trading create …
limitless trading cancel <order-id>
limitless approve set --slug <slug>
limitless wallet create | show
Meta
limitless setup (interactive wizard)
limitless shell (REPL)
--output table|json
--api-key, --private-key
Section 02
Install & first command.
Install once via Homebrew (brew install limitless-labs-group/limitless-cli/limitless) or build from source with cargo build --release. The CLI reads credentials from the LIMITLESS_API_KEY and LIMITLESS_PRIVATE_KEY environment variables, the same key Module 03 used. You can also run limitless setup to configure them interactively. Once the key is set, any process that inherits the environment can spawn subcommands.
How to run this
- Install the binary (brew install limitless-labs-group/limitless-cli/limitless) and confirm limitless --version prints a version from the same shell you’ll run the script in.
- Set LIMITLESS_API_KEY in your environment so the spawned subprocess inherits it, the CLI looks for it there.
- Save the snippet as cli-wrapper.ts, then run npx tsx cli-wrapper.ts.
- Save the snippet as run_cli_tool.py, then run python run_cli_tool.py.
- Terminal prints Markets: N with a positive integer. That JSON payload is exactly what your agent will pipe back into a tool response.
// Module 07: Spawn the `limitless` binary from TypeScript.
//
// Install once: brew install limitless-labs-group/limitless-cli/limitless
// Or from source: git clone + cargo build --release
// Verify: limitless --version
import { spawn } from 'node:child_process';
interface CliResult<T> {
ok: boolean;
data?: T;
error?: string;
}
export async function runCli<T>(args: string[]): Promise<CliResult<T>> {
return new Promise(resolve => {
// --output json forces machine-readable output on every subcommand.
const proc = spawn('limitless', [...args, '--output', 'json'], {
env: { ...process.env }, // inherits LIMITLESS_API_KEY / LIMITLESS_PRIVATE_KEY
});
let stdout = '';
let stderr = '';
proc.stdout.on('data', chunk => { stdout += chunk; });
proc.stderr.on('data', chunk => { stderr += chunk; });
proc.on('close', code => {
if (code !== 0) return resolve({ ok: false, error: stderr.trim() });
try {
resolve({ ok: true, data: JSON.parse(stdout) as T });
} catch (e) {
resolve({ ok: false, error: `Bad JSON from CLI: ${stdout.slice(0, 200)}` });
}
});
});
}
// First command: list active markets (sort options: trending, newest, ending-soon,
// high-value, lp-rewards: see `limitless markets list --help`).
const result = await runCli<unknown[]>(['markets', 'list', '--sort', 'trending']);
if (result.ok) console.log('Markets:', result.data?.length);
else console.error('CLI failed:', result.error);
# Module 07: Spawn the `limitless` binary from Python.
#
# Install once: brew install limitless-labs-group/limitless-cli/limitless
# Or from source: git clone + cargo build --release
# Verify: limitless --version
import json
import os
import subprocess
from dataclasses import dataclass
from typing import Any
@dataclass
class CliResult:
ok: bool
data: Any = None
error: str = ""
def run_cli(args: list[str]) -> CliResult:
try:
# --output json forces machine-readable output on every subcommand.
proc = subprocess.run(
["limitless", *args, "--output", "json"],
env = os.environ, # inherits LIMITLESS_API_KEY / LIMITLESS_PRIVATE_KEY
capture_output = True,
text = True,
timeout = 15,
)
except subprocess.TimeoutExpired:
return CliResult(ok=False, error="CLI timed out after 15s")
if proc.returncode != 0:
return CliResult(ok=False, error=proc.stderr.strip())
try:
return CliResult(ok=True, data=json.loads(proc.stdout))
except json.JSONDecodeError:
return CliResult(ok=False, error=f"Bad JSON from CLI: {proc.stdout[:200]}")
# First command: list active markets (sort options: trending, newest, ending-soon,
# high-value, lp-rewards: see `limitless markets list --help`).
result = run_cli(["markets", "list", "--sort", "trending"])
if result.ok:
print("Markets:", len(result.data))
else:
print("CLI failed:", result.error)
If Markets: N prints a positive integer, your CLI install is healthy. That same JSON payload is what your agent pipes back into a tool response in Section 03.
Section 03
Wrapping CLI commands as tools.
Once you can spawn the CLI reliably, wrapping it as agent tools is mechanical. Each tool is one subcommand plus a JSON schema describing its inputs. The descriptions matter, the LLM reads them to pick the right tool. Be literal: browse active markets, not browse markets (you can filter them).
// Module 07: Wrap CLI commands as Claude tools
import { runCli } from './cli.js';
export const browseMarketsTool = {
name: 'browse_markets',
description: 'List active Limitless markets. Supports sort order "trending", "newest", "ending-soon", "high-value", or "lp-rewards".',
input_schema: {
type: 'object',
properties: {
sort: { type: 'string', enum: ['trending', 'newest', 'ending-soon', 'high-value', 'lp-rewards'], default: 'trending' },
},
},
} as const;
export async function browseMarkets(input: { sort?: string }) {
const r = await runCli<unknown[]>(['markets', 'list', '--sort', input.sort ?? 'trending']);
return r.ok ? JSON.stringify(r.data) : `Error: ${r.error}`;
}
export const placeOrderTool = {
name: 'place_limit_order',
description: 'Place a GTC limit order on a Limitless market. Use this ONLY after confirming the market slug, outcome, price, and size.',
input_schema: {
type: 'object',
properties: {
slug: { type: 'string', description: 'Market slug from browse_markets.' },
outcome: { type: 'string', enum: ['yes', 'no'], description: 'Which outcome token to trade.' },
side: { type: 'string', enum: ['buy', 'sell'], description: 'Trade direction.' },
price: { type: 'number', minimum: 0.01, maximum: 0.99, description: 'Limit price in dollars (0.01–0.99).' },
size: { type: 'number', minimum: 1, description: 'Number of shares.' },
},
required: ['slug', 'outcome', 'side', 'price', 'size'],
},
} as const;
export async function placeOrder(input: {
slug: string; outcome: 'yes' | 'no'; side: 'buy' | 'sell'; price: number; size: number;
}) {
// limitless trading create --slug X --side buy --outcome yes --price 0.65 --size 100
const r = await runCli(['trading', 'create',
'--slug', input.slug,
'--side', input.side,
'--outcome', input.outcome,
'--price', String(input.price),
'--size', String(input.size),
'--order-type', 'GTC',
]);
return r.ok ? JSON.stringify(r.data) : `Error: ${r.error}`;
}
# Module 07: Wrap CLI commands as OpenAI function tools
import json
from typing import Any
from cli import run_cli
browse_markets_tool: dict[str, Any] = {
"type": "function",
"function": {
"name": "browse_markets",
"description": (
"List active Limitless markets. Supports sort order "
"'trending', 'newest', 'ending-soon', 'high-value', or 'lp-rewards'."
),
"parameters": {
"type": "object",
"properties": {
"sort": {
"type": "string",
"enum": ["trending", "newest", "ending-soon", "high-value", "lp-rewards"],
"default": "trending",
},
},
},
},
}
def browse_markets(sort: str = "trending") -> str:
r = run_cli(["markets", "list", "--sort", sort])
return json.dumps(r.data) if r.ok else f"Error: {r.error}"
place_order_tool: dict[str, Any] = {
"type": "function",
"function": {
"name": "place_limit_order",
"description": (
"Place a GTC limit order on a Limitless market. Use this ONLY after "
"confirming the market slug, outcome, price, and size."
),
"parameters": {
"type": "object",
"properties": {
"slug": {"type": "string", "description": "Market slug from browse_markets."},
"outcome": {"type": "string", "enum": ["yes", "no"], "description": "Which outcome token to trade."},
"side": {"type": "string", "enum": ["buy", "sell"], "description": "Trade direction."},
"price": {"type": "number", "minimum": 0.01, "maximum": 0.99, "description": "Limit price in dollars (0.01-0.99)."},
"size": {"type": "number", "minimum": 1, "description": "Number of shares."},
},
"required": ["slug", "outcome", "side", "price", "size"],
},
},
}
def place_limit_order(slug: str, outcome: str, side: str, price: float, size: float) -> str:
# limitless trading create --slug X --side buy --outcome yes --price 0.65 --size 100
r = run_cli([
"trading", "create",
"--slug", slug,
"--side", side,
"--outcome", outcome,
"--price", str(price),
"--size", str(size),
"--order-type", "GTC",
])
return json.dumps(r.data) if r.ok else f"Error: {r.error}"
Section 04
Why CLI vs direct SDK.
You’ll hit a fork: keep wrapping the CLI, or migrate hot paths to the SDK directly. There’s no universal answer, it depends on what’s slow, what’s typed, and what your runtime supports. The two columns below sort the decision into concrete signals. Read the left side first; if any bullet matches your situation, stay on the CLI. Drop to the SDK only when a right-side bullet starts costing you real latency or real money. Most agents live happily on the CLI forever, the migration is worth it only when you can name what specifically forced it.
Wrap the CLI when…
- You are prototyping and want zero SDK setup overhead.
- Your agent runtime is a language with no official SDK (Ruby, Elixir, Zig).
- You want a shared tool surface across multiple agents in different languages.
- You need the CLI’s built-in retry, env switching, and rate-limit backoff for free.
Runtimes that take this path: openclaw, the default agents-starter from Module 09, most MCP servers.
Call the SDK directly when…
- Latency matters, subprocess spawn is 20–60ms per call on a laptop.
- You need type safety on the response shape (TS interfaces, Python TypedDicts).
- Your workflow requires websockets or streaming that the CLI does not expose.
- You are building a market maker (Module 10 covers the hybrid pattern).
Rule of thumb: start with the CLI, migrate individual tools to the SDK only when a specific constraint forces your hand.
limitless-cli as an agent tool: what people ask
Each answer also ships invisibly as schema.org FAQ data for search engines and AI assistants. Tap a question to expand.
-
How do you install the limitless binary?
Via Homebrew,brew install limitless-labs-group/limitless-cli/limitless, or build from source withcargo build --releasefromgithub.com/limitless-labs-group/limitless-cli. Confirm withlimitless --versionin the same shell your agent will run in. The binary is self-contained, so your container image stays small, andlimitless setupconfigures credentials interactively. -
Why must an agent always pass --output json?
Because the default output is human-formatted, aligned columns, headers, sometimes ANSI colour codes, and that is presentation, not a contract. The next CLI release can reformat a column and your parser silently mis-extracts while most calls still parse, so the bad ones blend in.--output jsongives structured stdout plus a non-zero exit on error; pin the CLI version so a reformat breaks during your upgrade, not in production. -
How does an agent place an order through the CLI?
Wraplimitless trading createas aplace_limit_ordertool: pass--slug,--side,--outcome,--price,--size, and--order-type GTC, with a JSON schema constraining price to 0.01–0.99 and enums for side and outcome. Cancel withlimitless trading cancel <order-id>. The tool description should tell the model to confirm slug, outcome, price, and size before calling. -
What can an agent read through the CLI?
Markets vialimitless markets list | search | get | categories, with sort optionstrending,newest,ending-soon,high-value, andlp-rewards; the book vialimitless orderbook book | price | monitor; its own account vialimitless portfolio positions | trades | pnlandlimitless trading orders. Each read returns JSON your wrapper pipes straight back into a tool response. -
When should you migrate from the CLI to the SDK?
Only when a specific constraint forces it: latency (a subprocess spawn costs 20–60ms per call), type safety on response shapes, websockets or streaming the CLI does not expose, or a market-making workload. Stay on the CLI while prototyping, when your runtime has no official SDK (Ruby, Elixir, Zig), or when you want one shared tool surface across agents in different languages. Most agents live happily on the CLI forever.
Module checklist
Five quick confirmations.
Tick each item once you’ve actually done it. The Continue button unlocks at 5/5.
limitless is installed and limitless --version works from my shell
I ran limitless markets list --output json and got valid JSON back
I wrote a runCli helper in my agent’s runtime language
At least two CLI commands are exposed as agent tools with clear descriptions
I can articulate when to use the CLI vs the direct SDK
Module 07 complete
Hands on Limitless.
Your agent now does real work on Limitless. When the LLM decides to look at a market or place an order, it shells out to the same CLI a human trader would use, gets the same JSON back, makes the next decision, and keeps moving.
Concretely, your agent can browse and trade through the CLI. One reusable wrapper, two tool definitions, and a clear rule for when to drop to the SDK.
A reusable runCli helper that spawns the limitless binary with --output json and returns a typed CliResult<T> your agent loop can branch on.
Two working tool definitions, browse_markets and place_limit_order, complete with JSON schemas the LLM can read to pick the right subcommand.
A decision rubric for when to stay on the CLI vs. migrate a hot path to the SDK directly, the same rubric Modules 07 and 08 reuse.
Next up: turning the same CLI’s orderbook events feed into a data-freshness gate, so your agent refuses to trade markets that have gone quiet.
Complete the checklist above to unlock