Skip to main content
The llm strategy hands each tick to a real model. The model researches via tools, forms a thesis, and submits a structured decision. The runtime (not the model) owns execution and enforces hard guardrails (budget, real strikes, valid oracles).

Files

agent-service/
├── src/
│   ├── llmAgent.ts                        # tool loop + structured decision
│   └── llm.ts                             # OpenAI-compatible fetch client
└── prompts/
    ├── momentum.md
    └── contrarian.md

Tool loop

llmAgent.ts gives the model four research tools plus a terminal submit_decision:
tools = [
  // Live BTC markets (deduped via oracles.ts so the model never sees a dead twin)
  { name: 'list_btc_markets', description: 'List live BTC oracles by expiry' },

  // Strike book per oracle
  { name: 'list_strikes', description: 'List traded strikes + live asks for a given oracle' },

  // Recent fills tape per oracle
  { name: 'recent_fills', description: 'Last N fills on a given oracle' },

  // Pyth BTC candles
  { name: 'btc_candles', description: 'Last N minutes of BTC/USD 1-min closes from Pyth Hermes' },

  // Terminal tool. The model MUST call this on the last loop
  {
    name: 'submit_decision',
    description: 'Submit a mint or hold decision. Return values: side, strike, spend_dusdc, confidence_0_1, reasoning',
  },
];
The loop runs up to 8 steps, forcing submit_decision on the last. Returns a typed LlmDecision.

Decision shape

type LlmDecision =
  | { action: 'mint'; side: 'YES' | 'NO'; strike: number; spendDusdc: number; confidence: number; reasoning: string }
  | { action: 'hold'; reasoning: string };

Runtime guardrails

if (decision.action === 'hold') skip();
else {
  // 1. Re-quote on chain. Strike must still be quoteable.
  const ask = await quoteStrike(oracle, decision.strike, decision.side);
  if (!ask) skip("strike not in book at execution");

  // 2. Spend cap. Hard ceiling at AGENT_TRADE_DUSDC.
  const spend = Math.min(decision.spendDusdc, AGENT_TRADE_DUSDC);

  // 3. Mint PTB. Standard predict::mint via PredictManager.
  await mintBinary({ oracle, strike: decision.strike, isUp: decision.side === 'YES', spend });
}

llm.ts. OpenAI-compatible fetch client

export async function chatCompletion({
  baseUrl: string,
  apiKey: string,
  model: string,
  messages: ChatMessage[],
  tools?: ToolDef[],
  toolChoice?: 'auto' | 'required' | { type: 'function'; function: { name: string } },
  temperature?: number,
  timeoutMs?: number,    // default 45_000
}): Promise<ChatCompletionResponse>;
Works with Groq (default), OpenAI, Anthropic’s OpenAI-compat (api.anthropic.com/v1), Ollama, Atoma. Anything that speaks /v1/chat/completions. Zero new deps.

Provider presets

LLM_PROVIDER (groq | anthropic | openai | ollama) picks base URL + default model:
ProviderDefault Base URLDefault Model
groqhttps://api.groq.com/openai/v1llama-3.3-70b-versatile
anthropichttps://api.anthropic.com/v1claude-haiku-4-5
openaihttps://api.openai.com/v1gpt-4o-mini
ollamahttp://localhost:11434/v1llama3.1:8b
Explicit LLM_BASE_URL / LLM_MODEL override.

Example tick (Groq + momentum mandate)

[agent btc-brain] tick 1
  list_btc_markets → [Hourly 17:00Z, Daily Jun 30, Weekly Jul 5]
  list_strikes(0xabc…) → [… $63000 27¢ … $63500 14¢ …]
  btc_candles(60) → last 60 min: spot up 0.8%, vol ↑ 1.4x
  submit_decision(
    action=‘mint’, side=‘YES’, strike=63500, spend=2, confidence=0.7,
    reasoning=“BTC up 0.8% over 60min with rising vol; YES @ $63500 ask 14¢ implies 14% odds vs ~25% momentum-adjusted”
  )
  [runtime] re-quote ok @ 14¢, mint 2 DUSDC → tx 0x… ok

Limits

  • Tool loop is bounded at 8 steps. The model must converge or hold.
  • Spend per tick is bounded at AGENT_TRADE_DUSDC. Runtime caps, not model honor system.
  • Strikes must exist in the book at execution time. The model can never “invent” a strike.