Files
justin fb50c103d3
CI / test (push) Successful in 17s
CI / build-push (push) Successful in 6s
Location params on best_local_bid / latest_prices (zip / GPS + radius)
Thread zip/lat/lng/radius_miles through the client and both tools; friendly
guard for the zip-XOR-gps rule. Formatters surface distance, the searched
center, and the nearest-source hint when nothing is in range.

- client: best()/latest() take zip/lat/lng/radius_miles
- server: location params + docstrings (note Ohio-concentrated coverage)
- format: distance column + center/nearest rendering
- README + CHANGELOG + advisor prompt library updated
- tests: location formatting cases

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 07:51:54 -04:00

13 KiB
Raw Permalink Blame History

Changelog

Notes for clients/agents that consume the ag-bids MCP tools and the underlying ag-monitor /api/data/* HTTP API. Newest first.

2026-05-31 — Location-aware bid lookup (zip / GPS + radius)

best_local_bid and latest_prices can now be scoped to a farmer's location, so "where should I haul today" means near me, not "across every elevator we scrape." Filtering is offline (vendored US Census ZIP centroids — no geocoding API).

Changed MCP tools

  • best_local_bid(commodity, zip?, lat?, lng?, radius_miles?) — pass a zip OR lat+lng (not both) with optional radius_miles (default 50) to restrict to nearby elevators; the result shows the distance to the winner. With no location it behaves as before (all sources). If nothing is in range it returns no winner and reports the nearest elevator + its distance, so a coverage gap reads as a gap, not an error.
  • latest_prices(..., zip?, lat?, lng?, radius_miles?) — same location filter; rows are limited to the radius, annotated with distance_miles, and sorted nearest-first (adds a Distance column).

GPS input is reverse-labeled to its nearest ZIP for display; only zip → coord is used for the actual distance math.

Coverage note: scraped cash-bid sources are currently concentrated in Ohio. A zip/GPS far from Ohio (e.g. Iowa) will correctly return "nothing in range" with the nearest-source hint until more regions are added. (Reference data — price_trend, input_cost_trend — is already national.)

API

  • GET /api/data/best?commodity=&zip=&lat=&lng=&radius_miles= — new location params. Response adds center ({lat,lng,zip,source}), radius_miles, and (when out of range) nearest. best now carries distance_miles, city, state.
  • GET /api/data/latest?...&zip=&lat=&lng=&radius_miles= — same params; rows carry distance_miles and source city/state/latitude/longitude; response adds center + radius_miles when a location is given.
  • Errors (HTTP 400): zip and GPS together, partial GPS, radius_miles with no location, or an unknown zip.

Example questions → tool calls

Ask Call
Best corn bid within 40 mi of my zip best_local_bid(commodity="corn", zip="45810", radius_miles=40)
Best soybean bid near my coordinates best_local_bid(commodity="soy", lat=40.79, lng=-83.81)
Elevators near me, nearest first latest_prices(commodity="corn", zip="45810", radius_miles=60)

2026-05-30 — Regional fertilizer input costs (real $/ton + change)

Real U.S. regional retail fertilizer prices now feed the input-cost tools, so the advisor can reason about fertilizer the same way it does diesel and grain — actual dollars and the move, not an index. Source: USDA AgTransport (monthly, $/ton, history back to 2023).

Changed MCP tools (fertilizer added to the existing input-cost tools)

  • input_cost_trend(item, geo?, years=10)item now accepts six fertilizers in addition to diesel: urea, uan, anhydrous (anhydrous ammonia), dap, map, potash. Returns the latest real $/ton, month-over-month and year-over-year change ($ and %), and seasonal context.
    • New optional geo = AgTransport region; defaults to Cornbelt. Other regions: U.S. Gulf NOLA, Northern Plains, Southern Plains, Southeast, Northeast, California, Pacific Northwest, South Central, Central Florida, Tampa (not every product is published for every region).
    • diesel is unchanged — national U.S. $/gal, weekly; geo is ignored for it.
  • input_cost_series(item, geo?) — raw monthly $/ton series for any fertilizer (or the diesel $/gal weekly series), region-selectable.

Seasonal percentile for fertilizer is computed over a short history (2023+), so it deepens over time; the price and YoY/MoM change are solid now.

API

  • GET /api/data/input-cost-trend?item=&geo=&years=geo added.
  • GET /api/data/input-cost-series?item=&geo=geo added.
  • GET /api/data/input-cost-geographies?item=new: lists the regions a given input has data for (plus its default_geo).

Example questions → tool calls

Ask Call
Cornbelt urea price and how it's moved input_cost_trend(item="urea")
Anhydrous ammonia in the Southern Plains input_cost_trend(item="anhydrous", geo="Southern Plains")
Potash $/ton history input_cost_series(item="potash")
Which regions have DAP prices (API) GET /api/data/input-cost-geographies?item=dap

New national/seasonal reference layer (USDA NASS, corn back to 1908) — the macro benchmark to compare local cash bids against. Real prices and the change, not an index.

New MCP tools

  • price_trend(commodity, geo="US", years=10) — monthly price received by farmers ($/bu) with the move: latest real price, month-over-month and year-over-year change ($ and %), and seasonal context (percentile vs the same month over the last N years, normal, and range). geo is US or any 2-letter state. Conclusions, not rows.
  • price_series(commodity, geo="US", start_year?, end_year?) — raw monthly series ($/bu) for charting/drill-down (defaults to the recent window).

API

  • GET /api/data/price-trend?commodity=&geo=&years= — computed trend (cents + change_cents/change_pct/yoy_* + seasonal).
  • GET /api/data/price-series?commodity=&geo=&start_year=&end_year= — raw series.
  • GET /api/data/price-geographies?commodity= — which geos (US + states) exist.

All grain: corn/soy/wheat, all 50 states + US.

Input-cost tools (real $ + change)

  • input_cost_trend(item, years=10) — real input price with the move. Currently item="diesel" (EIA U.S. retail $/gal, weekly, back to 1994): latest price + week-over-week and year-over-year change + seasonal percentile/range. (For current fertilizer $/ton, current_input_price (DTN) still applies; more inputs extend this same tool.)
  • input_cost_series(item) — raw historical series for an input.
  • API: GET /api/data/input-cost-trend?item=&years=, GET /api/data/input-cost-series?item=.

USDA stopped publishing dollar input prices in 2014, so these use real-dollar sources (EIA) rather than an index.

2026-05-30 — Source geo + many more locations

Per-source geo (API + MCP)

  • GET /api/data/sources now returns location geo on every source: city, state, zip, county, latitude, longitude (any may be null). The AgriCharts-fed elevators carry exact coordinates; smaller sites carry city/state/zip.
  • MCP list_sources gained a Location column (City, ST ZIP). source_health is unchanged in shape.
  • Sources that are national or non-physical (CBOT futures, USDA AMS, DTN fertilizer, hedge-to-arrive / "Direct" bid buckets) legitimately have null geo. ~43 of ~51 sources are geo-tagged.

Many more elevator locations

These flow through every data tool/endpoint automatically (latest, history, best, basis_movement/basis_detail, sources, deliveries). No tool signatures changed — just far more sources, all named "<Co-op> — <Location>":

  • Heritage Cooperative — new co-op, 23 central/eastern-Ohio locations (corn/soy/wheat).
  • Mercer Landmark — expanded from 2 to all ~16 locations. Note: the two old source names were renamed for continuity — "Mercer Landmark — St Henry""Mercer Landmark — MPS St Henry", "Mercer Landmark — Minster""Mercer Landmark — Heartland Minster".
  • Bambauer — now both locations incl. Pemberton ("Bambauer — Jackson Center / New Knoxville", "Bambauer — Pemberton"); the old "Bambauer Jackson Center / New Knoxville, OH" source was renamed to the first of those.

If you cache source names, refresh them — several changed and many were added.

2026-05-29 — Futures price + change tool

New MCP tool

  • futures_quote(commodity, delivery?) — CBOT futures price and change for a grain. Reports the latest price, today's session open, the prior day's close, and both moves: change since open and change on the day (vs the previous settle). With delivery (e.g. "Jul 2026") it resolves the listed contract that month prices against (e.g. ZCN26); without it, the continuous nearby.

API changes (ag-monitor)

  • GET /api/data/futures?commodity=<grain>&delivery=<label?> — new endpoint. Returns { commodity, delivery, contract, symbol, quote } where quote is { settle_date, open_cents, last_cents, prev_close_cents, change_since_open_cents, change_on_day_cents, fetched_at } (or null if no data yet for that contract). commodity must be corn/soy/wheat.
  • futures_quotes table gained an open_cents column; the futures scraper now stores the session Open alongside the close. Rows captured before this change have open_cents = NULL, so change_since_open_cents is null for those until the next session is scraped.
  • Futures now also pull at :30 during the CBOT day session (on top of the hourly :00), so last and the changes track the session every ~30 min.

Example questions → tool calls

Ask Call
Corn Jul futures and how far it's moved today futures_quote(commodity="corn", delivery="Jul 2026")
Soy nearby futures, change on the day futures_quote(commodity="soy")

2026-05-29 — Flexible history + basis movement

New MCP tools

  • basis_movement(commodity?, source?, delivery?, days=30) — aggregated basis trend, one headline line per crop. Averages every matching (elevator × delivery) series to avg basis first → last over the window and reports how far it moved. This is the cheap "how is basis moving overall" view. All filters optional; commodity (if given) must be corn/soy/wheat.

    • Positive move = cash strengthened vs futures (basis up); negative = weaker.
    • Aggregation is intentional: divergent elevators can net to "flat". When the aggregate looks flat or surprising, call basis_detail to see per-elevator splits.
  • basis_detail(commodity?, source?, delivery?, days=30) — the drill-down for basis_movement. One row per (elevator, crop, delivery) series with basis first → last and the move. Same optional filters.

Both tools do the aggregation server-side (in the MCP) and return compact markdown — they do not dump every raw sample — to keep token usage low. Pattern: call basis_movement first, then basis_detail (optionally filtered) only when you need the breakdown.

Changed MCP tools

  • price_historycommodity is now optional (was required). Omit it to span every crop. Output now groups by (elevator, crop, delivery), surfaces basis first → last in each series summary, and adds a Futures column to the raw points table. Every filter (commodity, source, delivery, days) is optional and AND'd — pivot per elevator, per crop, per delivery, or any combination.

    • Raw per-row points are still only included when the window has ≤ ~60 samples; wider queries return the per-series summaries only.
  • latest_prices — added the kind filter (grain | fertilizer) for parity with the API. commodity, source, delivery, kind all optional.

API changes (ag-monitor)

  • GET /api/data/historycommodity query param is now optional. When omitted, returns history across all crops. source_id, delivery, and days (1730, default 30) remain optional. Response shape is unchanged: { "commodity": <str|null>, "days": <int>, "rows": [...] }. Each row carries fetched_at, source_id, source_name, commodity, delivery, bid_cents, basis_cents, futures_cents so callers can pivot client-side.
    • GET /api/data/latest already supported optional commodity, source, delivery, kind — unchanged.

Conventions / gotchas

  • All money fields are integer cents. bid_cents/futures_cents render as $/bu (4 dp) for grain and $/ton (2 dp) for fertilizer; basis_cents renders as a signed dollar value (2 dp), e.g. -0.14.
  • Basis is only meaningful for grain — the basis tools skip non-grain rows and any series with no basis on file.
  • source filtering is by exact elevator display name (e.g. "Mercer Landmark — Minster"). Use list_sources to get exact names.

Example questions → tool calls

Ask Call
Basis movement overall, last 7 days basis_movement(days=7)
Corn basis movement across all elevators basis_movement(commodity="corn")
Basis movement at one elevator basis_movement(source="Mercer Landmark — Minster")
Per-elevator basis breakdown for corn basis_detail(commodity="corn")
Last 7 days of history from elevator X for corn price_history(commodity="corn", source="…", days=7)
All-crop price history at one elevator price_history(source="…")
Latest grain bids only latest_prices(kind="grain")