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>
13 KiB
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 azipORlat+lng(not both) with optionalradius_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 withdistance_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 addscenter({lat,lng,zip,source}),radius_miles, and (when out of range)nearest.bestnow carriesdistance_miles,city,state.GET /api/data/latest?...&zip=&lat=&lng=&radius_miles=— same params; rows carrydistance_milesand sourcecity/state/latitude/longitude; response addscenter+radius_mileswhen a location is given.- Errors (HTTP 400): zip and GPS together, partial GPS,
radius_mileswith 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)—itemnow accepts six fertilizers in addition todiesel: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 toCornbelt. 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). dieselis unchanged — national U.S.$/gal, weekly;geois ignored for it.
- New optional
input_cost_series(item, geo?)— raw monthly$/tonseries for any fertilizer (or the diesel$/galweekly 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=—geoadded.GET /api/data/input-cost-series?item=&geo=—geoadded.GET /api/data/input-cost-geographies?item=— new: lists the regions a given input has data for (plus itsdefault_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 |
2026-05-30 — Grain price-received trends (real $ + change + seasonal)
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).geoisUSor 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. Currentlyitem="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/sourcesnow returns location geo on every source:city, state, zip, county, latitude, longitude(any may benull). The AgriCharts-fed elevators carry exact coordinates; smaller sites carry city/state/zip.- MCP
list_sourcesgained a Location column (City, ST ZIP).source_healthis unchanged in shape. - Sources that are national or non-physical (CBOT futures, USDA AMS, DTN
fertilizer, hedge-to-arrive / "Direct" bid buckets) legitimately have
nullgeo. ~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). Withdelivery(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 }wherequoteis{ settle_date, open_cents, last_cents, prev_close_cents, change_since_open_cents, change_on_day_cents, fetched_at }(ornullif no data yet for that contract).commoditymust becorn/soy/wheat.futures_quotestable gained anopen_centscolumn; the futures scraper now stores the session Open alongside the close. Rows captured before this change haveopen_cents = NULL, sochange_since_open_centsisnullfor 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
lastand 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 toavg basis first → lastover 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 becorn/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_detailto see per-elevator splits.
-
basis_detail(commodity?, source?, delivery?, days=30)— the drill-down forbasis_movement. One row per (elevator, crop, delivery) series with basisfirst → lastand 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_history—commodityis 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 thekindfilter (grain|fertilizer) for parity with the API.commodity,source,delivery,kindall optional.
API changes (ag-monitor)
GET /api/data/history—commodityquery param is now optional. When omitted, returns history across all crops.source_id,delivery, anddays(1–730, default 30) remain optional. Response shape is unchanged:{ "commodity": <str|null>, "days": <int>, "rows": [...] }. Each row carriesfetched_at, source_id, source_name, commodity, delivery, bid_cents, basis_cents, futures_centsso callers can pivot client-side.GET /api/data/latestalready supported optionalcommodity,source,delivery,kind— unchanged.
Conventions / gotchas
- All money fields are integer cents.
bid_cents/futures_centsrender as$/bu(4 dp) for grain and$/ton(2 dp) for fertilizer;basis_centsrenders 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.
sourcefiltering is by exact elevator display name (e.g."Mercer Landmark — Minster"). Uselist_sourcesto 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") |