diff --git a/src/hooks/use-franklin-chat.ts b/src/hooks/use-franklin-chat.ts
index 63c15be..7125aa1 100644
--- a/src/hooks/use-franklin-chat.ts
+++ b/src/hooks/use-franklin-chat.ts
@@ -372,6 +372,32 @@ const TOOL_ACTIVATION_NOTE =
"\n\nIMPORTANT: Your tools are hidden by default. To use any capability (web search, prediction markets, prices, music, phone), " +
"first call activate_tool with the names you need, then call the tool on the following turn.";
+// The web client intentionally starts with a tiny tool inventory, but an
+// explicit prediction-market request is not ambiguous: routing it through a
+// general web search loses the live, structured Predexon result and encourages
+// the model to answer from search snippets. The CLI exposes this capability
+// directly, so mirror that behaviour here for clear Polymarket/odds requests.
+// The focused-tool buttons still take precedence over this automatic routing.
+//
+// A match here forces a PAID tool call the model cannot decline, so the
+// patterns must be near-zero false positive: venue names (except "limitless",
+// a common adjective, which needs market context) and unambiguous
+// prediction-market phrases only. Bare "odds"/胜率/盘口 fire on everyday
+// language ("odds and ends", game win rates, order-book slang) and are
+// deliberately excluded; ambiguous CJK betting terms require betting context.
+function impliedToolForPrompt(prompt: string): string | undefined {
+ const text = prompt.toLowerCase();
+ if (
+ /\b(polymarket|kalshi|predict\.fun|limitless\.exchange|limitless (?:exchange|markets?))\b/.test(text) ||
+ /\b(prediction markets?|betting odds|market odds|implied probabilit(?:y|ies))\b/.test(text) ||
+ /预测市场/.test(prompt) ||
+ (/赔率|盘口|胜率/.test(prompt) && /预测|押注|下注|博彩/.test(prompt))
+ ) {
+ return "search_prediction_markets";
+ }
+ return undefined;
+}
+
// Split inline … chain-of-thought out of a model's text.
function splitThinking(raw: string): { answer: string; thinking: string } {
let thinking = "";
@@ -806,13 +832,18 @@ export function useFranklinChat(
// (mirrors Franklin CLI's src/agent/llm.ts). Tool-use / tool-result blocks
// live only in this API-local array; the user only sees their message and
// the final answer text.
- async function runChatWithTools(history: ChatMessage[], model: string, forceTool?: string) {
+ async function runChatWithTools(history: ChatMessage[], model: string, forceTool?: string, impliedTool?: boolean) {
setStatus("thinking");
type ApiMsg = Record;
// Focus mode: nudge the model to use the chosen tool (tool_choice forces it,
- // this just improves the phrasing of the answer afterwards).
+ // this just improves the phrasing of the answer afterwards). Implied routing
+ // (regex-detected, not user-selected) must NOT claim the user chose the tool:
+ // on a false-positive match the honest framing lets the model discard an
+ // irrelevant result and answer the actual question.
const focusNudge = forceTool
- ? `\n\nThe user selected the ${toolLabel(forceTool)} capability — use the ${forceTool} tool to answer this turn, then summarize the result clearly.`
+ ? impliedTool
+ ? `\n\nThe request looks like a prediction-market question, so the ${forceTool} tool has been pre-selected for this turn. If its result is irrelevant to what the user actually asked, ignore it and answer the question directly.`
+ : `\n\nThe user selected the ${toolLabel(forceTool)} capability — use the ${forceTool} tool to answer this turn, then summarize the result clearly.`
: "";
const systemPrompt = `${FRANKLIN_SYSTEM_PROMPT}\n\n${systemPromptDateLine(userTz())}\n\n${FRANKLIN_TOOLS_PROMPT}${TOOL_ACTIVATION_NOTE}${focusNudge}`;
// Convert a user-attached image to an Anthropic image content block.
@@ -991,7 +1022,12 @@ export function useFranklinChat(
let effectiveModel = base === "blockrun/auto" ? resolveAuto(lastPrompt) : base;
if (hasImage && !isVisionModel(effectiveModel)) effectiveModel = pickVisionSibling(effectiveModel);
modelRef.current = effectiveModel;
- await runChatWithTools(history, effectiveModel, forceTool);
+ // Implied routing only runs with a connected wallet: forcing a paid tool on
+ // a wallet-less visitor would replace their answer with a connect-wallet
+ // prompt. Without a wallet the model routes normally (and can still ask).
+ const implied =
+ !forceTool && isConnectedRef.current ? impliedToolForPrompt(lastPrompt) : undefined;
+ await runChatWithTools(history, effectiveModel, forceTool ?? implied, Boolean(implied));
}
// Detached, per-conversation media job (image/video). Adds the user message,