Sandbox environment: prices, stock and coupons are synthetic test data. What this means

How do I give Claude a Cartroute search tool?

Two ways: connect Claude Code or Claude Desktop to the Cartroute MCP server with no code, or, in your own application, define a search_products tool with the Anthropic Python SDK and let the SDK's tool runner call the Cartroute API whenever Claude decides to search. The example below is complete and runs as-is with two environment variables.

What do I need?

pip install anthropic requests
export ANTHROPIC_API_KEY="sk-ant-..."
export CARTROUTE_API_KEY="cr_live_..."

How do I define the tool with the tool runner?

The tool runner turns a decorated Python function into a tool, sends it to Claude, runs the function whenever Claude calls it, and loops until Claude has a final answer.

import json
import os

import anthropic
import requests
from anthropic import beta_tool

CARTROUTE = "https://cart-route.com/api/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['CARTROUTE_API_KEY']}"}

client = anthropic.Anthropic()


@beta_tool
def search_products(query: str, max_price: float | None = None, in_stock: bool = True,
                    include_membership: bool = True) -> str:
    """Search 12 US retailers for a product and return every offer.

    Each offer has price_usd, shipping_usd, the best usable coupon and
    effective_price_usd (what the shopper pays before tax). Rank on
    effective_price_usd. price_insight.verdict says whether today's price is a
    90-day low.

    Args:
        query: What to find, in plain words, e.g. "sony noise cancelling headphones".
        max_price: Optional maximum effective price in USD.
        in_stock: Only return offers that are in stock.
        include_membership: Include Costco and Sam's Club, which need a paid membership.
    """
    params = {"q": query, "limit": 3, "in_stock": str(in_stock).lower(),
              "include_membership": str(include_membership).lower()}
    if max_price is not None:
        params["max_price"] = max_price
    resp = requests.get(f"{CARTROUTE}/search", params=params, headers=HEADERS, timeout=20)
    # Return errors to Claude as text so it can explain them (e.g. out of credits).
    return json.dumps(resp.json())


runner = client.beta.messages.tool_runner(
    model="claude-opus-5",
    max_tokens=16000,
    tools=[search_products],
    messages=[{
        "role": "user",
        "content": "Where should I buy Sony WH-1000XM6 headphones? I don't have a Costco membership.",
    }],
)

for message in runner:
    for block in message.content:
        if block.type == "text":
            print(block.text)

Claude reads the docstring as the tool description, so the facts that matter for ranking (effective price, membership) are stated there.

How do I write the loop myself, with refusal fallbacks?

If you need full control, or want server-side refusal fallbacks on the request, use a manual loop. fallbacks: "default" lets the API re-run a request that Claude Opus 5's safety classifiers decline on Anthropic's recommended fallback model, inside the same call.

import json
import os

import anthropic
import requests

CARTROUTE = "https://cart-route.com/api/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['CARTROUTE_API_KEY']}"}
client = anthropic.Anthropic()

tools = [{
    "name": "search_products",
    "description": (
        "Search 12 US retailers for a product. Returns every offer with "
        "effective_price_usd (price + shipping - best usable coupon). Rank on it."
    ),
    "input_schema": {
        "type": "object",
        "properties": {
            "query": {"type": "string", "description": "What to find, in plain words."},
            "max_price": {"type": "number", "description": "Maximum effective price in USD."},
            "include_membership": {"type": "boolean", "description": "Include Costco and Sam's Club."},
        },
        "required": ["query"],
    },
}]


def run_tool(name, args):
    if name != "search_products":
        return json.dumps({"error": f"unknown tool {name}"}), True
    params = {"q": args["query"], "limit": 3,
              "include_membership": str(args.get("include_membership", True)).lower()}
    if "max_price" in args:
        params["max_price"] = args["max_price"]
    resp = requests.get(f"{CARTROUTE}/search", params=params, headers=HEADERS, timeout=20)
    return json.dumps(resp.json()), not resp.ok


messages = [{"role": "user", "content": "Find me a 65 inch OLED TV under $2,000."}]

while True:
    response = client.beta.messages.create(
        model="claude-opus-5",
        max_tokens=16000,
        betas=["server-side-fallback-2026-07-01"],
        fallbacks="default",
        tools=tools,
        messages=messages,
    )
    if response.stop_reason == "refusal":
        print("The request was declined.")
        break
    if response.stop_reason != "tool_use":
        break

    messages.append({"role": "assistant", "content": response.content})
    results = []
    for block in response.content:
        if block.type == "tool_use":
            output, is_error = run_tool(block.name, block.input)
            results.append({"type": "tool_result", "tool_use_id": block.id,
                            "content": output, "is_error": is_error})
    # All results for one turn go back in a single user message.
    messages.append({"role": "user", "content": results})

print(next((b.text for b in response.content if b.type == "text"), ""))

What should the tool return to Claude?

The Cartroute JSON as-is. It is compact, every money field is labelled _usd, and price_insight.reason is written to be quoted. On errors, return the problem body with is_error: true; its detail is written for the model to relay, for example that the account is out of credits.

How do I keep costs down?