gridscoot
guides

Agent recipes

How to compose the tools for real shopper tasks — and the contract every recipe obeys, so you never present a stale catalogue row as a live fact or a spec downgrade as a deal.

Overview

The tool reference tells you each tool’s schema. This page tells you how to chainthem, which serving mode to pick, and how to read the disclosure fields a response carries so you can pass them through to the person you’re shopping for. Every recipe here is built on one rule: Gridscoot would rather tell you “nothing genuinely cheaper exists”than recommend a spec downgrade or paper over a price it isn’t sure about.

Gridscoot is designed to sit behind a local-first agent. The recommended wiring, cheapest first:

  • Extraction — local model. When you need to turn a product page into structured specs, run a small local model (we calibrate against qwen2.5:3b via Ollama — 100% on our garden-hose fixtures, ~3.5s/page, $0). Reach for a cloud model only when no local one is available.
  • Comparison — the MCP server. Hand the structured specs to find_alternatives. The meets-or-exceeds predicate, delivered-total maths, and honest ranking live server-side so every agent gets the same verdict.
  • Discovery + history + alerts — the other six tools. search_products, compare_product, get_price_history, check_availability, list_retailers, track_price. Six are on the free tier; find_alternatives needs a Bearer key.

This keeps the expensive, repetitive work (spec extraction) on free local compute and reserves the network round-trip for the one thing that genuinely benefits from a shared server: a consistent, auditable comparison verdict.

Recipe: find a genuinely cheaper equal-or-better product

The flagship flow. The user has a product in mind; you want the cheapest option that is not a downgrade. Two steps: find the baseline, then ask for alternatives.

1 — locate the baseline

tools/call → search_products
{
  "name": "search_products",
  "arguments": { "query": "20m garden hose with fittings, ships to Germany" }
}

Express jurisdiction, budget, and delivery deadline in the natural-language query (or via max_price_aud / location_postcode for AU). Pick the row the user means and note its product id for catalogue mode, or gather the specs yourself from a page you may read for inline mode.

2 — ask for alternatives (catalogue mode)

tools/call → find_alternatives
{
  "name": "find_alternatives",
  "arguments": {
    "baselineProductId": 7242,
    "buyerCountry": "DE",
    "includeSkipped": true,
    "maxResults": 10
  }
}

The response is shaped so you can be honest about both outcomes — qualifying alternatives and the ones that were rejected, with reasons:

response (trimmed)
{
  "baseline":   { "product": { "id": 7242 }, "deliveredTotal": { "deliveredTotal": 16.00 } },
  "schema":     { "slug": "garden/hose", "label": "Garden hose" },
  "alternatives": [
    { "candidate": { "id": "bauhaus:23494512" },
      "deliveredTotal": { "deliveredTotal": 14.99 } }
  ],
  "skipped": [],
  "surveyedCount": 1,
  "summary": "Surveyed 1 candidate; 1 qualified."
}

Surface summary verbatim to the user — it already says how many were surveyed, how many qualified, and why the rest were dropped. Read the delivered total from deliveredTotal.deliveredTotal (the outer object also carries the shipping + VAT breakdown).

Recipe: keep my spec floors (no silent downgrades)

The predicate treats every spec the baseline declares as a hard floor. A candidate that is cheaper but weaker on any declared spec is skipped, not ranked. This is the difference between Gridscoot and a price grid. Real example, same hose as above but with a baseline that declares fittings + UV resistance + working pressure:

response — strict baseline (trimmed)
{
  "alternatives": [],
  "skipped": [
    { "candidate": { "id": "bauhaus:23494512" },
      "reasonCategory": "doesnt_qualify",
      "reasons": [
        "Spec working_pressure_bar: candidate does not declare it — cannot verify meets-or-exceeds",
        "Spec uv_resistant: baseline requires true; candidate is false",
        "Spec fittings_included: baseline requires true; candidate is false"
      ] }
  ],
  "summary": "Surveyed 1 candidate; 0 qualified (1 failed price-or-spec floor)."
}

That candidate is €1 cheaper and the engine still refuses it, because it lacks fittings, doesn’t claim UV resistance, and doesn’t declare a working pressure to verify. Two design choices worth knowing:

  • An undeclared spec disqualifies — it is not assumed.“Cannot verify” is treated as a fail, not a pass. Honest unknowns lose.
  • You control the floor by controlling the baseline. If the user only cares about length and diameter, pass a baseline that declares only those (inline mode), and the same candidate qualifies. The predicate never decides for the user which specs matter — you do, by what you put in rawSpecs.

To relax the floors, drop the specs the user doesn’t care about (inline mode):

tools/call → find_alternatives (inline, core specs only)
{
  "name": "find_alternatives",
  "arguments": {
    "buyerCountry": "DE",
    "categorySchemaSlug": "garden/hose",
    "baseline": {
      "id": "ref:meister-20m", "displayName": "Reference 20m hose", "currency": "EUR",
      "listPriceInclSellerVat": 16.00,
      "shippingQuotes": [{ "carrierSlug": "std", "costMajor": 0, "shipsToCountries": ["DE"] }],
      "sellerCountry": "DE", "sellerOssStatus": "oss_registered",
      "rawSpecs": { "length_m": 20, "diameter_inch": 0.5, "burst_pressure_bar": 24 }
    },
    "candidates": [ /* … specs you extracted from pages you may read … */ ]
  }
}

Recipe: buy from another country (delivered total, not sticker)

A Hungarian buyer looking at a German listing pays more than the sticker: shipping to HU, plus the VAT differential if the seller is OSS-registered (DE 19% vs HU 27%). Set buyerCountry and the engine ranks on the delivered total, not the list price:

tools/call → find_alternatives
{
  "name": "find_alternatives",
  "arguments": { "baselineProductId": 7242, "buyerCountry": "HU", "includeSkipped": true }
}
  • A candidate whose carriers don’t reach the buyer’s country is skipped with reasonCategory: "doesnt_ship"— a cheaper sticker you can’t actually receive is not a deal.
  • The voucher/promo price is deliberately ignored. Ranking is on the list price + delivery + VAT the buyer can count on, never a coupon that may have expired or be account-bound.

Recipe: watch a price and get alerted

Two tools: find the product, then subscribe with the user’s token (from /account/keys). Alerts go to that account’s email — you don’t pass an address.

tools/call → track_price
{
  "name": "track_price",
  "arguments": {
    "product_id": "GS-HOSE-20M",
    "target_price_aud": 18.50,
    "user_token": "gs_live_…"
  }
}

Gridscoot stores the subscription and emails the account when an offer crosses the target — so you don’t poll get_price_history in a loop. Two honest caveats: only subscribe when the user has clearly asked to be alerted, and note that the price target is currently AUD-denominated (target_price_aud) even for non-AU products — a known limitation while multi-currency alerts are built out.

Inline vs catalogue — which mode to use

find_alternatives runs in two modes. The difference is where the candidate data comes from, and it has direct honesty consequences.

Inline (agent-native) — pass baseline + candidates

  • Freshness: exactly as fresh as the data you pass. You read the pages now.
  • Coverage: anything you can legally read — the whole web, not our catalogue.
  • Cost: your local extraction compute; $0 if you run a local model.
  • Use when: the user gives you specific listings, or you have a browser tool and permission to read the pages.

Catalogue (DB-lookup) — pass baselineProductId

  • Freshness: as fresh as our last verification — stale by design, disclosed with a timestamp + TTL. The response carries catalogResolution.synthesizedFields[] naming any value we inferred rather than observed.
  • Coverage: only SKUs we have seeded for that category + jurisdiction.
  • Cost: a single API round-trip; no extraction on your side.
  • Use when: you want a fast lookup against a vertical we already cover and the disclosed staleness is acceptable.

The honesty contract

Every recipe above obeys the same promises. Hold us to them; pass them through to your user.

What Gridscoot promises

  • No silent downgrades. A cheaper option that’s weaker on a declared spec is skipped with a reason, never quietly ranked.
  • Delivered total, not sticker. Ranking is list price + shipping-to-you + VAT differential. A price you can’t receive isn’t a deal.
  • List price only. Coupons and account-bound vouchers never move the ranking.
  • No thumb on the scale. Default ranking is pure-cheapest (trust weight 0). Any merchant-trust weighting is opt-in and disclosed (see /rankings + ADR-0007).
  • Honest empty. “Nothing genuinely cheaper-or-better exists” is a real answer we’ll give rather than fabricate one.
  • Disclosed staleness. Catalogue rows carry a verification timestamp; synthesized fields are flagged so you know what was inferred vs observed.

What Gridscoot does not do

  • It does not crawl pages on your behalf, or ask you to crawl pages a retailer forbids.
  • It does not guarantee the catalogue reflects the retailer’s price right now in DB-lookup mode — that’s what the timestamp is for, and what inline mode is for when you need live.
  • It does not rank by affiliate payout. Attribution is disclosed separately (see affiliate attribution) and never reorders results.