fb50c103d3
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>
267 lines
13 KiB
Markdown
267 lines
13 KiB
Markdown
# 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` |
|
||
|
||
## 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). `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_history`** — `commodity` 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/history`** — `commodity` query param is now **optional**.
|
||
When omitted, returns history across all crops. `source_id`, `delivery`, and
|
||
`days` (1–730, 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")` |
|