Add fertilizer to input-cost tools (regional $/ton, USDA AgTransport)
CI / test (push) Successful in 17s
CI / build-push (push) Successful in 30s

input_cost_trend/input_cost_series now accept six fertilizers (urea, uan,
anhydrous, dap, map, potash) alongside diesel, with an optional `geo` region
(default Cornbelt). Real $/ton + MoM/YoY change + seasonal context.

- client: pass geo through; add input_cost_geographies
- server: expand VALID_INPUTS; geo param + docstrings
- format already unit-aware ($/ton) and geo-aware
- README tools table now lists the reference/trend + input-cost tools
- CHANGELOG: regional fertilizer input-cost release notes
- tests: fertilizer $/ton + region formatting

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-30 15:59:33 -04:00
parent a0e36996c4
commit 03c6c540ef
5 changed files with 104 additions and 19 deletions
+40
View File
@@ -3,6 +3,46 @@
Notes for clients/agents that consume the ag-bids MCP tools and the underlying
`ag-monitor` `/api/data/*` HTTP API. Newest first.
## 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
+8 -3
View File
@@ -25,8 +25,9 @@ ag-bids-mcp ── X-API-Key ─► https://agbids.paul.farm/api/data/*
```
Endpoints used: `/api/data/latest`, `/history`, `/futures`, `/best`, `/inputs`,
`/sources`, `/deliveries`. See the [ag-monitor source](https://git.jpaul.io/justin/ag-bids)
for the contract.
`/sources`, `/deliveries`, `/price-trend`, `/price-series`, `/price-geographies`,
`/input-cost-trend`, `/input-cost-series`, `/input-cost-geographies`. See the
[ag-monitor source](https://git.jpaul.io/justin/ag-bids) for the contract.
## Authentication
@@ -77,7 +78,11 @@ auto-updates it every 5 minutes.
| `best_local_bid(commodity)` | Where to sell `corn`, `soy`, or `wheat` for this month's delivery — markdown one-liner + table |
| `futures_quote(commodity, delivery?)` | CBOT futures price + change since open + change on the day. With a delivery month it resolves the listed contract; without it, the continuous nearby |
| `current_lime_price()` | Latest lime quotes across all manual-entry sources |
| `current_input_price(product?)` | MAP / Potash / Lime — all three or one |
| `current_input_price(product?)` | MAP / Potash / Lime — all three or one (local DTN dealer feed) |
| `price_trend(commodity, geo?, years?)` | USDA NASS monthly price *received* ($/bu) + MoM/YoY change + seasonal context. `geo` = `US` or a state (corn back to 1908) |
| `price_series(commodity, geo?, start_year?, end_year?)` | Raw monthly price-received series for charting |
| `input_cost_trend(item, geo?, years?)` | Real input cost + change. `item` = `diesel` (U.S. `$/gal`, weekly, EIA) or a fertilizer `$/ton` (`urea`/`uan`/`anhydrous`/`dap`/`map`/`potash`, monthly, USDA AgTransport); `geo` selects the fertilizer region (default `Cornbelt`) |
| `input_cost_series(item, geo?)` | Raw historical series for an input cost (diesel `$/gal` or fertilizer `$/ton`, region-selectable) |
| `latest_prices(commodity?, source?, delivery?, kind?)` | Live snapshot table; every filter optional (`kind` = `grain`/`fertilizer`) |
| `price_history(commodity?, source?, delivery?, days?)` | Time series per (elevator, crop, delivery); **every filter optional** — omit `commodity` to span all crops. Shows bid + basis trend + futures |
| `basis_movement(commodity?, source?, delivery?, days?)` | Aggregated basis trend, one headline line per crop (the cheap overview) |
+8 -4
View File
@@ -80,12 +80,16 @@ def price_series(commodity: str, geo: str = "US",
start_year=start_year, end_year=end_year)
def input_cost_trend(item: str, years: int = 10) -> dict:
return _get("/api/data/input-cost-trend", item=item, years=years)
def input_cost_trend(item: str, years: int = 10, geo: str | None = None) -> dict:
return _get("/api/data/input-cost-trend", item=item, years=years, geo=geo)
def input_cost_series(item: str) -> dict:
return _get("/api/data/input-cost-series", item=item)
def input_cost_series(item: str, geo: str | None = None) -> dict:
return _get("/api/data/input-cost-series", item=item, geo=geo)
def input_cost_geographies(item: str) -> dict:
return _get("/api/data/input-cost-geographies", item=item)
def futures(commodity: str, delivery: str | None = None) -> dict:
+33 -12
View File
@@ -303,40 +303,61 @@ def price_series(
return fmt.fmt_price_series(payload)
VALID_INPUTS = {"diesel"}
# Fuel is national (US); fertilizer is regional ($/ton, USDA AgTransport).
VALID_INPUTS = {"diesel", "urea", "uan", "anhydrous", "dap", "map", "potash"}
_FERT_INPUTS = {"urea", "uan", "anhydrous", "dap", "map", "potash"}
@mcp.tool()
def input_cost_trend(
item: Annotated[
str, Field(description="Input to price. Currently: 'diesel' (U.S. retail $/gal).")
str,
Field(description="Input to price: 'diesel' (U.S. retail $/gal) or a fertilizer "
"($/ton) — 'urea', 'uan', 'anhydrous', 'dap', 'map', 'potash'."),
],
geo: Annotated[
str | None,
Field(description="Fertilizer region (default 'Cornbelt'). Other regions: "
"'U.S. Gulf NOLA', 'Northern Plains', 'Southern Plains', "
"'Southeast', 'Northeast', 'California', 'Pacific Northwest', "
"'South Central', 'Central Florida', 'Tampa'. Ignored for diesel (US)."),
] = None,
years: Annotated[
int, Field(ge=1, le=120, description="Baseline window for the seasonal normal/percentile."),
] = 10,
) -> str:
"""Real input cost with the change — e.g. U.S. retail diesel ($/gal).
"""Real input cost with the change — retail diesel ($/gal) or fertilizer ($/ton).
Latest real price + week-over-week and year-over-year moves + seasonal
percentile/range. Fills the input-cost side for the advisor (fuel). For
current fertilizer $/ton use current_input_price."""
Latest real price + period-over-period and year-over-year moves + seasonal
percentile/range. Diesel is U.S. weekly (back to 1994); fertilizer is monthly
regional (USDA AgTransport, Cornbelt default, back to 2023). This is the
input-cost side for the advisor. For a one-off current fertilizer quote from a
local dealer feed, current_input_price (DTN) still applies."""
it = item.strip().lower()
with track("input_cost_trend", item=it, years=years):
g = geo.strip() if geo else None
with track("input_cost_trend", item=it, geo=g or "", years=years):
if it not in VALID_INPUTS:
return f"`item` must be one of: {sorted(VALID_INPUTS)}"
return fmt.fmt_price_trend(client.input_cost_trend(item=it, years=years))
return fmt.fmt_price_trend(client.input_cost_trend(item=it, years=years, geo=g))
@mcp.tool()
def input_cost_series(
item: Annotated[str, Field(description="Input: 'diesel'.")],
item: Annotated[
str, Field(description="Input: 'diesel' or a fertilizer ('urea', 'uan', "
"'anhydrous', 'dap', 'map', 'potash').")],
geo: Annotated[
str | None,
Field(description="Fertilizer region (default 'Cornbelt'). Ignored for diesel."),
] = None,
) -> str:
"""Raw historical series for a tracked input cost (diesel, $/gal)."""
"""Raw historical series for a tracked input cost (diesel $/gal or fertilizer $/ton)."""
it = item.strip().lower()
with track("input_cost_series", item=it):
g = geo.strip() if geo else None
with track("input_cost_series", item=it, geo=g or ""):
if it not in VALID_INPUTS:
return f"`item` must be one of: {sorted(VALID_INPUTS)}"
return fmt.fmt_price_series(client.input_cost_series(item=it))
return fmt.fmt_price_series(client.input_cost_series(item=it, geo=g))
@mcp.tool()
+15
View File
@@ -131,6 +131,21 @@ def test_fmt_input_cost_payload_shape():
assert "+58.2%" in out # YoY surfaced
def test_fmt_input_cost_fertilizer_ton_and_region():
# Fertilizer payload carries item + geo (region) + $/ton.
payload = {"item": "urea", "geo": "Cornbelt", "label": "urea", "unit": "$/ton",
"source": "USDA AgTransport",
"trend": {"period": "2026-03-01", "value_cents": 69812, "prev_cents": 51812,
"change_cents": 18000, "change_pct": 34.7, "yoy_cents": 45500,
"yoy_change_cents": 24312, "yoy_pct": 53.4, "seasonal": None,
"recent_direction": "up", "baseline_years": 10, "points": 39}}
out = fmt.fmt_price_trend(payload)
assert "urea — Cornbelt — urea" in out # item, region, label all present
assert "$698.12/ton" in out
assert "USDA AgTransport" in out
assert "Mar 2026" in out # monthly period rendered as month
def test_fmt_inputs_lime_table():
payload = {
"product": "lime", "count": 2,