July 2026
The homepage is now a dense reference surface: a six-cell dataset strip (counts, issuing authority, last-audited date), a PackβWeighβClassifyβDeclareβLand consignment lifecycle, a 24-tile tool grid where every example is executed at build time by the live tool it belongs to (AWB prefix 176 β Emirates; UN 1203 β Class 3 PG II; an ISO 6346 check digit computed by the validator itself), a provenance table mirroring /methodology (including honest "provenance pending verification" rows), rate-limit and distribution sections, and an agent terminal showing a real adr_lookup response with its _source envelope. A build-time pipeline generates every number and example from the datasets and calculation functions β hand-typed counts fail the build gate. Hover-only motion, keyboard-operable menus and tabs, full dark-mode parity, prefers-reduced-motion respected site-wide. The API docs open with the same terminal hero (MCP / curl / GPT tabs plus an npx install strip), and /pricing gains a Teams line (pooled usage across seats β email contact@freightutils.com to talk; no self-serve checkout yet).
The airline dataset has held 6,357 entries since its last additions, but several surfaces still displayed a stale hand-typed 6,352. All current-state surfaces (site stats, tool registry, MCP tool description, the redesigned homepage strip β which now derives the count from the dataset itself at build time β and the brand banners) now state 6,357. Dataset contents are unchanged; the npm package description refresh rides the next scheduled package update.
The ICS2 Stop-Words Checker (ics2_check, /api/ics2-check, /ics2-check) now flags 12 additional goods-description terms that the EU added to the ICS2 Common Repository list on 2 February 2026: Aid products, Comercial, Consumption, Ensemble, Fake, Headwear, Item, Miscelaneous, Miscellaneous, N/A, Oddments and Promotional. The list is mirrored verbatim from the EU CIRCABC source (TAXUD.A.3.003/TA, current in-force document), including the term statuses the Commission itself sets β FreightUtils does not editorialise it. "N/A" is now also treated as a goods-description stop-word per the EU categorisation (it was previously excluded here as a party-field-only placeholder; party-field checking remains out of scope). The shipped list now matches the EU current-in-force set exactly (245 terms; 239 active, plus 6 the Commission has temporarily disabled that this tool therefore does not flag). Reference check only β a flagged term will likely cause an ENS rejection, and a clean result still does not guarantee acceptance. No change to the tool count (still 23 REST / 24 MCP), endpoints, or schema; no npm republish (ics2_check is REST-backed).
Refreshed the agent-facing surfaces so AI agents and directories read the current, accurate picture. Rebuilt llms.txt as an agent-first index β an identity + agent-safety contract, a connect block (MCP remote endpoint + npm + REST + OpenAPI + Custom GPT + key signup), and every tool phrased as the capability an agent searches for; llms-full.txt regenerated to match. The Identifier Validator now leads with its parse-a-string use case everywhere (the /validate page, /api-docs, and the OpenAPI + MCP tool descriptions): hand it any text β a booking line, an email β and it finds and check-digit-validates every ISO 6346 container, IATA AWB and IMO number in it, with single-identifier validation as the secondary path. Both GitHub READMEs and the MCP registry metadata (server.json, package.json) were refreshed to the neutral-reference + validation-layer, source-citing framing, the OpenAI GPT Store is now listed as a distribution surface, and stale tool counts across the repo docs were corrected to the live figures. Copy and documentation only β no tool, endpoint, schema, data or behaviour change; tool counts unchanged (23 REST / 24 MCP). All OpenAPI operation descriptions remain within the 300-character agent-tooling limit.
Shortened the operation descriptions in the OpenAPI spec (/openapi.json, shown on the interactive /playground explorer) so the spec imports cleanly into agent tooling that caps each operation description at 300 characters β e.g. ChatGPT Custom GPT Actions, which otherwise silently fails to load the action. Five over-length descriptions were trimmed (emissions, ics2-check, consignment, validate, airports), keeping the same meaning. Spec text only β no endpoint, parameter, response, or calculation change; tool counts unchanged (23 REST / 24 MCP).
June 2026
The emissions calculator (emissions_calculator, /api/emissions, /emissions) now returns extra self-describing fields β additive only, with no change to any factor value or the calculation. Each result carries a human-readable summary to surface; a mass_basis reminder to use actual gross mass, not chargeable/volumetric weight (a common air-freight mistake); an empty_running field (whether the fleet-average factor already includes average empty running β so you do not double-count an empty-return leg); a representativeness label (low for sea and air, medium for road/rail/inland) with a high-variance warning in the summary for sea/air (a generic default for screening only β real emissions vary materially by vessel/aircraft type, load factor and routing); and a last_verified date on the factor (when it was last checked against its source). The method wording is now "computed using the ISO 14083 / GLEC distance-based method". On the /emissions page, sea/air estimates are rounded harder to signal their imprecision while the API keeps full precision. The freightutils-mcp MCP output schema gains these fields (republished as 2.10.1). No factor values changed; tool counts unchanged.
Fixed airport search (airport_lookup) returning incorrect results for IATA/ICAO code queries; exact code matches now rank first. Searching a code such as "GLA" or "EDI" via the search box could previously surface a name-substring match (e.g. Brazil's "Glauber Rocha" airport for GLA) or small airfields/heliports instead of Glasgow / Edinburgh, because the search truncated its scan before reaching the exact-code record. No data change; the nearest-airport tool and the direct iata/icao lookups were unaffected.
You can now regenerate your FreightUtils API key from your account page β useful if your key may have been exposed. Regenerating issues a new key, immediately deactivates the old one, and shows the new key once (copy it before you leave the page). The account page already shows your plan, masked key (last 4 characters only), usage this month, and a Stripe billing-portal link for managing your subscription. This is a logged-in, account-only page; the older /dashboard link now takes you to the same account page.
Two new reference tools backed by the public-domain OurAirports dataset (85,555 airports). airport_lookup (GET /api/airports, the /airports page, MCP airport_lookup) resolves an airport by IATA code, ICAO code, or name/city search and returns the full record β both codes, name, type (large/medium/small/heliport/closed/seaplane), municipality, region, country, coordinates and elevation β with ranked candidates for ambiguous names. nearest_airport (GET /api/nearest-airport, MCP nearest_airport, and the "Nearest to me" button on the /airports page) takes a caller-provided latitude/longitude and returns the closest airports by great-circle (haversine) distance, with distance_km on each result; optional radius_km, max_results and type filter. Coordinates are input only β never stored or logged (the audit log records neither query nor body, and the Sentry redactor now also drops lat/lon). The data is public domain: all 1,178 large airports were cross-checked against the independent OpenFlights dataset (95.2% agree within 5 km + IATA) and a representative sample β including every outlier β was independently re-verified against Wikidata (all within 0.8 km), with provenance and limitations documented. Built to the tool-quality bar: the website MCP and the npm stdio server register both tools via registerTool with full output schemas + structuredContent and a _source citing OurAirports. Reference data only β coordinates are the airport reference point, not for navigation; verify current codes with IATA/ICAO. Counts are now 23 REST tools / 24 MCP tools; the freightutils-mcp npm package adds both tools and is republished as 2.10.0.
New tool: the ICS2 Stop-Words Checker flags terms from the official EU ICS2 stop-words list (vague or generic goods descriptions that the European Commission deems unacceptable in an entry summary declaration). Paste a goods description and it returns the flagged terms β each with a note on whether the term is the standalone description (an automatic ENS rejection) or embedded in a longer description (make it more specific) β plus a clean boolean, a caveat and a _source. There is no binary accepted/rejected verdict. Available as GET /api/ics2-check, the /ics2-check page (with an interactive checker), and the MCP tool ics2_check. The list is the EU Commission DG TAXUD list (base guidance v4.10 plus the 4 May 2026 update; legal basis Commission Delegated Regulation (EU) 2015/2446, data element 18 05 000 000 β cited, not reproduced) and is non-exhaustive: a clean result does NOT guarantee acceptance, and a flagged term will likely cause a rejection. It is a reference check only β not an ENS filing, not a customs-compliance determination, and not legal advice β aimed at customs and documentation teams, brokers, forwarders and agents building filing pipelines. Submitted descriptions are not persisted or logged. Counts are now 21 REST tools / 22 MCP tools; the freightutils-mcp npm package adds the tool and is republished as 2.9.0.
The Emissions Calculator (/emissions) and Identifier Validator (/validate) pages now include a live, interactive calculator β enter your inputs and get an instant result from the same /api/emissions and /api/validate endpoints, in addition to the existing API documentation and worked examples. Behind the scenes, the site footer's Calculators and Reference columns now render from the single tool registry so they can no longer drift from the navigation, homepage and sitemap, and a new surface-consistency guard asserts that every tool appears on every public surface (footer, nav, sitemap, API docs, llms.txt and the homepage grid), wired into both the lint gate and a daily health check.
New tool: the Identifier Validator checks freight-identifier check digits β shipping container (ISO 6346), air waybill (IATA modulus-7) and IMO ship number β or parses an arbitrary string to find and validate every identifier in it. Available as GET /api/validate, the /validate page, and the MCP tool validate. Returns per identifier: type, normalised form, valid (check-digit pass/fail), expected vs actual check digit, details (container owner prefix + equipment category; AWB airline prefix + airline, reused from the existing airline dataset; IMO number) and a _source citing the scheme. Structural validation only β a valid check digit means the identifier is well-formed, not that the container/shipment/vessel exists or is active; it is not a registry or tracking lookup. The algorithms are implemented from the public schemes (ISO 6346:2022; IATA Cargo Services Conference Resolution 600a; IMO Resolution A.1215(34)); no standard text is reproduced, and they are verified against documented known-good and known-bad examples. Built to the tool-quality bar: the website MCP and npm stdio server register it via registerTool with a full output schema + structuredContent. Counts are now 20 REST tools / 21 MCP tools; the freightutils-mcp npm package adds the tool and is republished as 2.8.0.
New tool: the Emissions Calculator estimates freight transport CO2e per ISO 14083:2023 / GLEC Framework v3.2 β emissions = mass Γ distance Γ an emission-intensity factor (kgCO2e/tonne-km). Available as GET /api/emissions, the /emissions page, and the MCP tool emissions_calculator. It returns well-to-wheel AND tank-to-wheel emissions where the factor has both, the exact factor used (value, authority, edition), the tonne-km activity, and a _source citing both the ISO method and the specific open factor. Factors come from three verified open authorities β UK DEFRA/DESNZ (Open Government Licence v3.0), US EPA GHG Emission Factors Hub (US-Gov public domain) and ADEME Base Carbone (Licence Ouverte) β across road, rail, sea, air and inland waterway; GLEC is a methodology citation only and no GLEC values are redistributed. It is the first tool built to the new quality bar: the website MCP and the npm stdio server register it via registerTool with a full output schema + structuredContent (the other tools follow in a later sprint). You provide the distance β the tool does not route or geocode. Best-effort estimate using open factors, not a verified or audited carbon report. Counts are now 19 REST tools / 20 MCP tools; the freightutils-mcp npm package adds the tool and is republished as 2.7.0.
Each FreightUtils REST endpoint and MCP tool response now includes an additive "_source" block citing where the data or method comes from, assembled from the single provenance source of truth. Reference datasets carry authority, dataset/edition, source URL and licence (ADR 2025/UNECE, HS 2022/WCO via UN Comtrade PDDL, UN/LOCODE 2024-2, UK Trade Tariff/OGL v3); the calculators carry a methodology citation pointing at /methodology; and the datasets still completing a provenance audit β airline, container, ULD, Incoterms and vehicle β carry authority plus "provenance_status": "pending-verification" only, with no licence asserted. The website MCP responses add the same _source in structuredContent plus a one-line human-readable citation in the text. Purely additive: no existing field was renamed, removed or retyped, so any agent that ignores unknown fields is unaffected (still 18 REST tools / 19 MCP). Documented in the OpenAPI spec via a new SourceEnvelope schema. The freightutils-mcp npm server inherits _source automatically through the REST proxy β no package republish needed.
The published OpenAPI version label now reads 3.1.0 wherever it appears (the spec file already declared 3.1.0, but the API docs, the spec download buttons and the pricing feature list still said 3.0). Behind the scenes, tool/endpoint counts, dataset editions, per-dataset data-source citations and the "last updated" marker now all derive from a single source of truth, so they stay consistent across the API responses, the dataset pages and the docs and cannot drift apart. No tool, endpoint, schema or behaviour change β still 18 REST tools / 19 MCP.
FreightUtils now leads with one consistent positioning across every source agents and directories read β "the neutral freight reference layer for AI agents": authoritative dangerous-goods, customs, location and freight-calculation data an agent can call and cite, from primary sources (ADR 2025/UNECE, HS 2022/WCO, IATA-regulated airline prefixes), with no freight to sell and no carrier to push. Applied to the homepage title/description and Open Graph, the /api-docs intro, the OpenAPI spec description, llms.txt and llms-full.txt, the GitHub repo About, and the freightutils-mcp package (README, package.json, server.json) β republished to npm as 2.6.0 so the MCP Registry, Smithery and Glama re-sync from the corrected source. Tool counts are unchanged and still derive from the single source of truth (18 REST tools / 19 MCP). Metadata and copy only β no tool, endpoint, schema or behaviour change. @modelcontextprotocol/sdk is verified at or above the 1.26.0 release that patches CVE-2026-25536 on both surfaces.
Several MCP tools advertised a plain text input where the description promised a specific format, so malformed values passed validation and only failed (or returned nothing) further down. Each is now validated to exactly the forms it actually accepts, with a clear error, and without narrowing any feature: unit_converter now takes a known unit code (the 17 weight/volume/length units, plus chargeable_kg and freight_tonnes as conversion targets); hs_code_lookup validates a 2β6 digit code and a Roman-numeral section; incoterms_lookup takes a 3-letter Incoterm code; uk_duty_calculator validates the commodity code (6β10 digits), 2-letter origin country and the Incoterms enum; airline_lookup keeps its 2-char IATA / 3-char ICAO / 3-digit AWB-prefix / country / name union but rejects mis-shaped codes; unlocode_lookup validates the 5-char code, 2-letter country and a function_type enum (port/airport/rail/road/icd/border); and the container/ULD/vehicle selectors reject empty input while still accepting any code or slug. Applied identically to the website /api/mcp surface and the freightutils-mcp npm package (published 2.5.0) so tools/list and validation match across both. No tool, endpoint or count change (still 18 REST tools / 19 MCP). Note for npm users: unlocode_lookupβs function filter is now named function_type.
The consignment calculator can now be shared with a single link. "Copy share link" encodes the entire consignment β mode plus every line (quantity, dimensions, units, goods code and stackable flag) β into the URL itself; opening that link in any browser reopens the consignment as a clearly-labelled, read-only "Shared consignment" view that recomputes client-side, dangerous-goods flags included, with the same totals and billed-on basis the sender saw. Nothing is uploaded, saved or tracked β the consignment lives entirely in the link β and the recipient can hit "Edit a copy" to branch their own editable version. Oversized consignments that would make a broken link are declined gracefully with a prompt to send a CSV/JSON file instead. The four exports (Excel, email, CSV, JSON) now all carry each line's goods code so a pasted-back consignment is complete; the JSON keeps the canonical POST /api/consignment result shape and adds an input echo alongside it. No new tool, endpoint, URL or API change.
The website MCP endpoint (/api/mcp) now declares the prompts and resources capabilities and returns a valid empty result for keyless resources/list, prompts/list and resources/templates/list, instead of a -32601 "Method not found". This matches the freightutils-mcp npm package so both MCP surfaces behave identically when a directory crawler or strict client introspects them without an API key. The tool surface is unchanged β still 19 tools with the same enriched schemas β and tool calls still require an API key only at call time. Website surface only; no new endpoint, tool or count, and no package republish.
On the consignment calculator the results card is sticky so it stays in view while you work, but its sticky scope reached past the tool β so on scroll it stayed pinned and the "How the consignment calculator works" copy slid up behind it, reading as a card floating on top of the page. The sticky is now scoped to the tool section: the results card stays put while the tool is on screen, then scrolls away normally as the explanatory copy comes up. No layout shift, and the mobile layout (where the card is already non-sticky) is unchanged.
The multi-item consignment calculator is now a spreadsheet-style tool. Switch between sea, air and road with a single control β the chargeable-weight basis follows the mode β and add as many lines as you need, each with its own quantity, dimensions (mm/cm/m/in), weight (kg/g/t/lb) and an optional goods code. Results sit directly under the table and update instantly as you type, entirely in your browser: total volume, gross and chargeable weight, what the consignment is billed on, the mode's costing basis, and objective advisory flags including dangerous-goods presence. Type a UN number into the goods-code field and its ADR 2025 hazard class and proper shipping name appear (UN 1203 β Class 3, petrol) β resolved client-side against a slim public index, so the full dangerous-goods dataset is never downloaded to the browser. Four one-click exports round it out: copy for Excel (tab-separated rows + totals that paste straight into cells), copy a plain-text summary for email, download a CSV, or copy the canonical JSON result β the exact same object the POST /api/consignment endpoint returns. The in-browser figures match the API to the last decimal. Separately, the tool and REST-endpoint counts shown across the site now read 18 (matching the live tool registry); the MCP server still exposes 19 tools.
Internal infrastructure only β no change to the consignment calculator's inputs, outputs, schema, flags, or tool counts. The consignment dangerous-goods-presence flag's ADR lookup is now an injected dependency that defaults to the full server-side ADR 2025 dataset, so REST /api/consignment, the MCP consignment_calculator, and the npm package behave exactly as before. A new committed, re-runnable generator (scripts/build-adr-un-index.mjs) projects the canonical ADR 2025 dataset 1:1 to a slim public index at /data/adr-un-index.v1.json (2,939 entries of un_number, un_class, proper_shipping_name) so the upcoming web consignment tool can resolve UN-number presence without bundling the full ADR dataset. The index is drift-guarded in CI against its single source of truth. Reference facts only (presence + class), never a compliance statement.
The consignment calculator (POST /api/consignment and the consignment_calculator MCP tool) now shares one canonical, versioned contract. Pick a transport mode (sea, air or road) and send canonical "lines" β each with quantity, dims {l, w, h, unit} (mm/cm/m/in) and weight {value, unit} (kg/g/t/lb) β or keep sending the legacy flat "items" array, which still works unchanged. The engine returns per-line and grand totals (CBM, loading metres, volumetric and mode-specific chargeable weight; sea uses weight-or-measure at 1,000 kg/mΒ³, air a settable IATA volumetric divisor defaulting to 6000, road loading metres against a 2.4 m trailer width) and stamps a schema_version. It also returns objective, advisory-only flags: an implausible-density check, a mode/option mismatch check, a dangerous-goods-presence note when a line carries a UN number (matched against the ADR 2025 reference β presence only, never a compliance verdict), and an ISO 6346 container / IATA air-waybill check-digit sanity check. Every response carries a best-effort disclaimer β deterministic calculations and reference data only, not regulatory, customs or dangerous-goods compliance advice; you remain responsible for classification, documentation and carrier acceptance. The canonical JSON Schema is published at /schema/consignment.v1.json. Backward compatible: previously valid calls keep working.
The remote MCP tools adr_lookup and adr_lq_eq_check now validate their inputs the same way the freightutils-mcp npm package already does: un_number must be 4 digits (optionally "UN"-prefixed), name search needs at least 2 characters, and the LQ/EQ checker takes 1β20 items. Malformed inputs now return a clear validation error instead of an empty or confusing result. Both tools also gained fuller descriptions of what they return, when to use them, and an explicit note that they return ADR reference data rather than certifying that a shipment is compliant. This brings the website MCP surface into line with the published npm package β no behaviour change for valid calls.
The three API access tiers are now spelled out identically everywhere they appear β 25 requests/day for anonymous access (per IP, no signup), 100 requests/day with a free API key, and 50,000 requests/month on Pro (Β£19/mo). Previously the free-key tier was missing from the /about "How this runs" section and from the pricing summary that feeds the LLM index, so the real free-key limit read differently depending on which page you landed on. Every number is verified against the live rate-limit middleware, the anonymous limit now reads from the same shared constant the enforcement path uses (so the displayed and enforced values cannot drift), and the OpenAPI spec description states all three tiers for any tool that imports it. Commercial-access enquiries now point straight to Pro pricing. A new docs/RATE-LIMIT-SEMANTICS.md records the exact enforced behaviour: per-tier limits, per-IP vs per-key bucketing, the daily/monthly renewing (not lifetime) windows, and the precise 429 response shape.
Magic-link sign-in now works reliably from corporate email accounts. Many company mail systems (Microsoft Safe Links, Mimecast, Proofpoint and similar) automatically open every link in an email to scan it for threats β and because FreightUtils sign-in links were single-use, the scanner's automated visit was using the link up before the recipient ever clicked it, producing a confusing "link expired or already used" error even well inside the 15-minute validity window. Clicking a sign-in link now lands on a lightweight confirmation page with a single "Continue to sign in" button; the link is only marked as used when that button is pressed, which automated scanners never do. The confirmation step is a plain HTML form, so it works in locked-down corporate browsers with JavaScript disabled. Double-clicking the button or using the back button after signing in no longer shows an error β if you are already signed in, you are taken straight to your account. All sign-in destinations behave exactly as before, including the verify-then-checkout flow for Pro upgrades. If your sign-in link does expire (they still last 15 minutes), the error page and the confirmation page both offer a one-click path to request a fresh one.
Root-cause fix for the customer-tier-sync family (FAULT 16/17). updateUserPlan in lib/auth/kv.ts no longer silently no-ops when there is no usable user record: a paying customer who reached Stripe checkout without a prior free account β or whose record was left as a non-JSON blob / field-dropped shape by a past manual dashboard edit (kv-encoding audit F1) β was resolving as Free with no API key, the /account page showing "key not available β contact support" and the "missing information" banner, and the self-heal backstop could not reach the record because it gates on a stripeCustomerId the blob lacked. On any active-subscription write (plan: pro) updateUserPlan now materialises a complete record β minting + linking an fu_live_ key (reusing a recoverable legacy email:<email> key first, so nothing is orphaned) β and repairs a record that lost its apiKey regardless of plan. Persistence is email-independent by construction: the persist path performs only KV writes (user: + key: mirror + email: index) and NEVER calls Resend, so a silent email failure can no longer leave a half-written record; the webhook fires notification email best-effort AFTER the write, with the existing Sentry skip-capture. Sync-only (plan: null) and downgrade (plan: free) events against a missing record stay a no-op β we never fabricate accounts. New admin repair tool scripts/admin/repair-pro-record.mjs rebuilds an absent/malformed/key-less record end-to-end (Stripe-gated, persist-first, key emailed via the standard Resend template only when newly minted). New regression suite scripts/test-provisioning-record-integrity.mjs pins: an account with an active Pro subscription resolves to tier=pro with a linked key independent of email-send outcome, plus malformed-blob and key-less repair cases. Audit: docs/audit/customer-pro-provisioning-2026-06-08.md.
GET /api/mcp/sse threw "redisUrl is required" as an unhandled rejection in prod (Sentry FREIGHTUTILS-5). mcp-handler routes three transports β mcp (streamable-HTTP), sse, and message β but sse and message need a Redis URL (REDIS_URL / KV_URL) for pub/sub that this deployment never configures, so those two endpoints threw the moment they were hit and have never worked; no real client uses them (Claude Desktop, Cursor, the MCP Registry, Smithery all point at /api/mcp/mcp). Fix: a new allowlist (lib/mcp/transport-allowlist.ts) restricts the dynamic [transport] segment to exactly mcp, validated against params.transport at the start of every exported method (GET / POST / DELETE) before any delegation β so sse, message, scanner probes, and random hex all return 404 { error: "unknown_transport", hint } and never reach the Redis-requiring code path. Those rejections are deliberately not sent to Sentry (noise, not faults). As defence in depth the delegation is also wrapped in try/catch, so any error that still escapes on the mcp transport becomes a handled 500 { error: "internal_error" } captured once with tag mcp_route_error=true. The /api/mcp alias forwards a synthetic { transport: "mcp" } context the guard reads (fix-once-mirror-everywhere). Unit + structural tests (scripts/test-mcp-transport.mjs) wired into npm run lint; smoke extended with GET /api/mcp/sse β 404, GET /api/mcp/message β 404, junk-segment 404, and a Bearer initialize regression. Server-side only β no public page, sitemap, OpenAPI, nav, or tool-count change; the freightutils-mcp npm package is untouched.
May 2026
Continuous proof that a clean MCP client can connect, authenticate, and invoke tools β so the first time it cannot, for any user, an alert fires. New GET /api/mcp/health (public, 60s edge cache, rate-limit exempt) returns status / mcp_version / tools_registered (19) / transports / data_freshness / timestamp; callable by a customer agent for self-diagnostic without burning anonymous budget. New scripts/smoke/mcp-customer-experience.mjs simulates a brand-new user end-to-end against both transports β remote streamable-HTTP runs handshake + tools/list + four tool_calls with golden-value asserts (cbm_calculator 0.96 mΒ³, adr_lookup UN 1203 class 3, airline_lookup AWB 176 β Emirates, shipment_summary non-empty); stdio spawns npx -y freightutils-mcp@latest and verifies handshake + 19 tools registered. Wired into .github/workflows/mcp-smoke.yml β daily 06:15 UTC cron, push-to-main on MCP paths, workflow_dispatch trigger, captures a Sentry event via sentry-cli on failure. Sentry MCP-specific failure tagging now lives in lib/observability/audit.ts: every MCP request sets scope tags surface=mcp / transport=remote / stage / tool_name; explicit captureException fires on handler throws, captureMessage on 5xx and 401. PII-safe β only the existing apiKeyTail (last 8 chars) carries customer attribution. /api-docs#mcp-setup gains a First-time setup verification block with Claude Desktop / Cursor / Cline configs, the "fully quit and relaunch" instruction, the health-endpoint curl, and a 5-row troubleshooting table. Two real bugs surfaced during development (separate sprints to fix): the standalone freightutils-mcp npm package does not pass an API key through its proxied HTTP calls (so stdio users hit the 25/day anon cap regardless of Pro status); and the MCP cbm_calculator response is still in camelCase while the REST /api/cbm equivalent is snake_case. Smoke handles both bugs gracefully β stdio tool_call leg skipped via MCP_SKIP_STDIO_CALLS=1 until the package fix ships; cbm assert accepts either casing.
New /methodology page (629 words) lists every shipped dataset with its named authoritative source, update cadence, and last audit date β HS (WCO 2022), ADR 2025 (UNECE), Airlines (IATA/ICAO), UN/LOCODE (UNECE), Containers (ISO 668 + carrier specs), Vehicles (EU 96/53/EC + UK DVSA + manufacturer specs), INCOTERMS (ICC 2020), ULDs (IATA ULD Technical Manual), UK Duty (HMRC Trade Tariff). Documents the two-agent verification chain (Code transcribes; Chrome re-reads the live source URL; smoke tests + regression guards catch systematic errors) and the per-record provenance.decision_rationale audit trail. Names the limitations plainly (not a substitute for a DGSA, not customs-broker advice, real-world tare varies between build lots). Error-report path: email or GitHub issue, investigated within 48h, fixed entries logged here by date. /about page rewritten end-to-end (462 words) into first-person practitioner voice β names Soap, a UK ADR-certified freight transport planner working live air cargo operations at Heathrow, after years in freight operations planning; What-this-isn't section plainly disclaims VC-funded / chatbot wrapper / canonical-source framings. A new <CredibilityLine /> component is wired above the input on all 7 ADR tool pages (/adr, /adr-calculator, /adr/lq-eq-checker, /adr/limited-quantities, /adr/tunnel-codes, /adr/training-guide, /adr/changes-2025) and all 8 non-ADR tool pages (/hs, /airlines, /containers, /uld, /unlocode, /incoterms, /vehicles, /duty). Footer Platform column gains a Methodology link. /about is also in the main nav. Voice + endorsement scan clean (no founder-marketing tells; no false UN/IATA/ICC endorsement claims).
Google Search Console (2026-05-25 alert) flagged the new "Excluded by noindex tag" indexing reason on pages submitted via sitemap. Audit confirmed four overlap URLs β /privacy, /terms, /refund-policy, /dpa β all carry robots: noindex, follow in their metadata export (added 2026-04-30 in the legal/compliance audit commit e169a5b) but were still being emitted from app/sitemap.ts. Removed the four entries from staticRoutes and added a NOINDEX_PATHS ReadonlySet at the top of app/sitemap.ts that drives a final-stage filter on the assembled sitemap as a safety net β covers /privacy /terms /refund-policy /dpa /account /signin /login /dashboard so a future drive-by re-add of any of these still gets caught before publication. Sitemap URL count drops 9,830 β 9,826. No unintentional noindex found β all noindex pages are legitimately user-state, legal, or the /hs/code/00000000 honeypot (already excluded at its generation site). The "Blocked due to other 4xx issue" GSC reason was also audited: no URLs in the current sitemap return 4xx for valid inputs and next.config.ts redirects cover the known legacy URLs Google may still have indexed (/privacy-policy β /privacy, /tos β /terms, /data-processing-agreement β /dpa, etc.). Post-deploy IndexNow ping for the four removed URLs accelerates the deindex.
Smithery scrape verification on 2026-05-24 caught the website MCP transport advertising serverInfo.version "1.0.8" via initialize even though SITE_STATS.mcpVersion and npm both read 2.1.1. The hardcoded literal in app/api/mcp/[transport]/route.ts is replaced with SITE_STATS.mcpVersion so the wire identity tracks the same constant the rest of the site uses (/api/health, changelog, audit logs). Two drift surfaces collapsed into one; the deeper "compute from freightutils-mcp/package.json or npm registry at build time" automation stays queued in Notion. Companion docs: root README.md replaced (was stock Next.js boilerplate) with a proper surfaces table + dev quickstart + related-repos table; SoapyRED/freightutils-mcp README updated β uld_lookup count line 15 β 16, changelog backfilled with 1.1.0 / 2.0.0 / 2.1.0 / 2.1.1 entries (previously stopped at 1.0.8). Next Smithery + Glama re-scrape will pick up both surfaces.
Three small drift fixes. (1) Sitewide RSS auto-discovery β added <link rel="alternate" type="application/rss+xml" title="FreightUtils Changelog" href="/changelog.xml" /> to the root layout <head> in app/layout.tsx so RSS readers like Feedly and NetNewsWire pick up the feed from any page on the site (previously only /changelog itself advertised the feed via per-page metadata). Footer Platform column gains an explicit "RSS Feed" link to /changelog.xml for human discovery alongside the existing Changelog entry. The feed itself β 50-entry RSS 2.0 generated from lib/changelog-data.ts via app/changelog.xml/route.ts β was already live; this PR closes the discoverability gap. (2) /uld page meta description and OG image generator URL still hardcoded "15+ unit load device types" while the underlying lib/data/ulds.json is 16 entries since PR #25 (2026-05-13 rebuild) and the on-page chip already reads ${ULD_COUNT} dynamically. Bumped both hardcoded strings to "16" in app/uld/page.tsx so SERP snippets and social cards match the on-page chip and /api/uld meta.total. Reconciliation report β all other surfaces already consistent: SITE_STATS has no ULD count field; /about, /, /api-docs do not mention a count; root README.md is the stock Next.js template; no lib/api-tools-registry file exists. (3) SITE_STATS.mcpVersion in lib/constants/siteStats.ts bumped 1.0.5 β 2.1.1 to match the live freightutils-mcp npm publish; used in the /api/health response only β string stringify, no shape change. Added a TODO comment beside the constant to compute from freightutils-mcp/package.json or fetch from the npm registry at build time (queued in Notion idea vault as the staleness-automation thread; not in this PR scope).
/contact reworked to fix the "same email N times" repetition that read as amateurish. Five topic cards (general, Pro support, data corrections, security, abuse) keep their titles and descriptions, drop the per-card visible email line, and gain a single orange "Email us β" CTA with a topic-specific mailto subject. The raw contact@freightutils.com address now appears exactly once, in a dashed block below the card grid. Cards also gain category-coloured left borders (general = accent, Pro = customs blue, data = ref purple, security + abuse = DG red) so they match the rest of the site's card pattern. The Pro card optionally renders a secondary "Book a 15-min call β" button when NEXT_PUBLIC_CALENDLY_URL is set in the Vercel environment β unset by default, so flipping it on is a config-only change with no redeploy logic needed. The resolver lives at lib/contact/calendly.ts and the conditional contract is pinned by scripts/test-contact-calendly.mjs (11 assertions covering undefined / null / empty / whitespace / trimming / truthy paths). Separately: welcome email 2 ("3 things to try") now leads with the API Playground β paste your key, hit Send, see live responses with no terminal needed. The MCP-server item was dropped (covered well enough by /api-docs#mcp and the homepage MCP card); shipment-summary (now #2) and ADR lookup (now #3) stay. Subject line unchanged so the existing drip-scheduler state in KV keeps working.
/playground is now live as a single interactive surface for every endpoint in the OpenAPI spec. Pulls the endpoint list from /openapi.json at runtime; default inputs live in lib/playground/endpoint-examples.ts and are parity-enforced by scripts/test-playground-defaults.mjs (adding a new endpoint without an example breaks CI). Cards are grouped by the four design-system categories with the v1 solid-left-border pattern; response panel uses the dashed informational variant. A single API-key field at the top is gated by an explicit "Remember this key in my browser" checkbox β untick to clear, tick to persist to localStorage under fu-playground-key. The key is sent only as Authorization: Bearer on the actual API call β never logged, never in analytics, never in error reports. Displayed key in curl/JS/Python tabs is redacted to fu_live_***LAST8 unless Show full key is toggled (same last-8 convention as the apiKeyTail Sentry tag). Response panel surfaces status, latency, pretty-printed JSON, and inline upgrade prompts on 401/403/429. Deep-linkable cards (/playground#get-api-cbm) auto-expand on landing. /api-docs gets a prominent Open Playground CTA at the top plus per-endpoint "Try it β" links on all 18 endpoint headers. Wired into sitemap, nav Platform dropdown, footer Developers column, and the homepage For Developers CTA cluster. Tests: scripts/test-playground-defaults.mjs (defaults parity) and scripts/test-playground-key-persistence.mjs (localStorage tick/untick/reload).
Companion to the recursion fix (#51). New reconciliation backstop at lib/auth/reconcile.ts wired into /api/auth/me and /api/auth/whoami: when KV reads plan: free and the user has a non-null stripeCustomerId, the endpoint asks Stripe whether the customer actually has an active subscription and self-heals if so. Cooldown-gated 1h per email so identity-page refreshes do not hammer Stripe. Emits a tier_mismatch_reconciled Sentry warning on every heal β that is the alert signal that a primary webhook path failed silently again. /api/auth/whoami only invokes the backstop on the plan === free slow path so Pro keys keep the one-KV-read fast path. New manual override at scripts/admin/upgrade-user.mjs β Stripe-gated one-shot for KV writes, refuses to upgrade an unpaid account, reads back to verify both user:<email> and key:<apiKey> records. Triage runbook at docs/runbooks/customer-tier-sync.md. Phase-1 diagnosis archived at docs/audit/customer-pro-tier-sync-2026-05-21.md. FAULT 16 encoded in docs/FAULT-HISTORY-AND-PREVENTION.md.
Two observability gaps closed in response to the Saytz incident. (1) Sentry only had has_api_key:true as a boolean β no way to filter errors to a specific paying customer. The audit pipeline at lib/observability/audit.ts now sets an apiKeyTail Sentry tag with the last 8 chars of the request key (Authorization Bearer or x-api-key). Last 8 only, no fu_live_ prefix β so the message redactor at lib/sentry-redact.ts (which does not walk event.tags) leaves the value alone. The tag is OMITTED entirely for anonymous requests so Sentry does not index empty values. (2) No abandonment detection β Saytz had 4 calls on day 1 and zero since, and we only learned via the customer email. New Vercel cron at /api/cron/customer-health (daily 09:00 UTC) scans KV user:*, sums the last 7 daily-bucket counters for each Pro customer with daysSinceSignup >= 3, and emails a digest to contact@freightutils.com when any total is below 10. Manual mute via a silenced_until ISO field on the KV record. Logic is dependency-injected so the unit test exercises the full branching without hitting prod KV or sending real emails (31 assertions across 7 scenarios). Sentry alert rules to be applied via the dashboard β spec in the PR body. Runbook: docs/runbooks/customer-health-cron.md.
safeUpdateUserPlan in app/api/stripe/webhook/route.ts called itself recursively instead of the imported updateUserPlan. Every checkout.session.completed / customer.subscription.deleted event silently stack-overflowed; the surrounding try/catch ate the RangeError and the webhook still ACK'd 200 so Stripe never retried β 100% silent failure for ~24h on the Sytze upgrade. Replaced with a wrapper in lib/auth/safe-update-user-plan.ts that delegates to updateUserPlan, captures any exception to Sentry with source=stripe-webhook + stripe_event_type + email + plan tags, and re-throws so the route returns 500 and Stripe applies its standard retry backoff. Silent-eat is exactly what made the failure invisible. Added stripeSubscriptionId as an optional field on the User type in lib/auth/kv.ts and extended updateUserPlan to persist it (plus a plan: null sync-only path for transient Stripe statuses like past_due where downgrading is incorrect). Webhook now wires customer.subscription.created and customer.subscription.updated to sync subscription.id on every transition; checkout.session.completed extracts session.subscription so the field is populated at upgrade time. Unit tests pin the no-recursion regression and the User-type round-trip.
A paying customer whose KV record had been hand-edited in the Upstash dashboard (manual recovery from a separate webhook issue) triggered 500s on /account and /api/auth/me. The dashboard edit replaced the JSON value instead of merging, dropping the apiKey, email, and createdAt fields. Both call sites did user.apiKey.slice(-4) and threw TypeError: Cannot read properties of undefined. The production release SHA was unchanged through the incident β the failure was purely data-shape, not a deploy regression. Anonymous traffic and every other authenticated user were unaffected. Fix: defensive guards on both surfaces (apiKey and email accessed via nullish-fallback), the masked-key chip on /account now renders "not available β contact support" instead of crashing, both surfaces emit Sentry.captureMessage("malformed_user_record", {level:"warning"}) when a guard fires, and a new "needs attention" banner on /account tells the customer their subscription is intact while support restores the record. FAULT 17 encoded. Diagnosis at docs/incidents/2026-05-21-prod-homepage-500.md.
Free-tier signups (the top of the API funnel) have been silent on prod β Stripe-side notifications cover paid signups, but free-tier API key issuance fired no internal alert. New helper lib/email/admin-signup.ts sends a transactional email via the existing Resend integration with subject "[FreightUtils] New free signup: {email}" or "[FreightUtils] New Pro signup: {email}" to contact@freightutils.com (override via ADMIN_NOTIFICATION_EMAIL env, e.g. mcristoiu@gmail.com for direct-to-personal). Body carries: email, tier, UTC + UK timestamps, country (x-vercel-ip-country for free flows; Stripe address.country for Pro), referer (free flows only β Stripe webhook is server-to-server), source (legacy-form / magic-link-verify / stripe-checkout), and the optional use_case for legacy-form signups. No PII beyond email; no payment details, no Stripe IDs, no API key values. Three call sites wired: app/api/keys/register/route.ts (after KV insert), app/api/auth/verify/route.ts (with a pre-createUser getUser check so notifications only fire for genuine first-time signups, not returning sign-ins), and app/api/stripe/webhook/route.ts on checkout.session.completed. Non-blocking β every site uses `void notify(...).catch(...)` so a Resend or KV outage cannot delay or fail the signup ack. Kill-switch ADMIN_NOTIFICATIONS_ENABLED=false silences without a deploy. Rate-limit cap 30/h via KV bucket admin-signup-notifs:${YYYY-MM-DDTHH}; above cap console.warn and skip. Contract-verified by scripts/admin-signup-notify-test.mjs which checks the helper exports, the kill-switch boolean logic (unset β on; "true" β on; "false" β off; empty β on), every call site's void+.catch wrapping, and the rate-limit constants. Audit: docs/audit/admin-signup-notifications-2026-05-20.md.
siteStats.ts toolCount + mcpToolCount both moved 18 β 19, propagating to homepage / /about / /api-docs. FREIGHTUTILS_STRATEGY.md mcp.directory bullet updated to live v2.1.1 / 19 tools. CLAUDE.md "Distribution cascade" + STATE.md surfaces table amended to encode Chrome-verified Glama cascade: Glama auto-publishes its build artefact on successful scrape-derived build β the "Build succeeded for FreightUtils MCP Server" maintainer email is informational, no "Create a release" click required. Maintenance grade C β B confirmed post-publish. Added a "verify Glama behaviour, do not predict it" one-liner β this was the third cascade drift in three days. Historical "11" / "18" mentions in past changelog entries + audit doc forensic record left intact. External follow-ups (freightutils-mcp README + server.json + package.json descriptions, Glama dashboard description) listed with exact target text in the PR body. Audits: docs/audit/tool-count-standardise-2026-05-17.md (Phase 1 diagnosis); docs/audit/distribution-cascade-2026-05-16.md (2026-05-17 afternoon amendment).
Sprint 1 made the MCP Registry cascade work, but Chrome verification on Glama showed "Latest release: v1.0.0", Tool Definition cache stuck at 11 tools, Maintenance grade C. Diagnosed that Glama scrapes the GitHub Releases API specifically β freightutils-mcp had zero Releases ever cut. Extended the publish workflow with a final step that creates a Release on every tag push (notes pulled from freightutils-mcp CHANGELOG.md between the version heading and the next one; marked --latest=true when the version matches MCP Registry isLatest). Plus an idempotent publish guard so re-runs on already-published versions are no-ops. Backfilled four historical Releases via workflow_dispatch: v2.1.1 (Latest), v2.1.0, v2.0.0, v1.1.0. Glama re-scrape lag is days; Chrome re-verify in 3β5 days. Walkthrough: future v2.1.2 will npm publish β git push --follow-tags β publish to MCP Registry β cut GitHub Release in one workflow run. Companion PR SoapyRED/freightutils-mcp#3 MERGED. Audit amendment: docs/audit/distribution-cascade-2026-05-16.md (2026-05-17 section).
/account, /contact, /docs/{deprecation,versioning}, /dpa, /for-it, /guides, /guides/[slug], /refund-policy, /roadmap, /signin, /status all rendered <title>X β FreightUtils | FreightUtils on prod (the per-page title hardcoded "β FreightUtils" and the root layout template appended "| FreightUtils" too). Removed the per-page brand from each; the root template adds it once. Recovers ~9 SERP characters per affected page. ADR detail titles regained the "ADR 2025" edition signal β they now read "UN 1203 Petrol β ADR 2025 Class 3 PG II" instead of "UN 1203 Petrol β ADR Class 3 PG II", across ~2,347 ADR detail pages. Prevention: scripts/lint-seo-titles.mjs now walks every app/**/page.tsx and asserts no rendered title contains "FreightUtils" more than once (Rule A) + every ADR fixture title includes "ADR 2025" (Rule B); CI-gated via a new prebuild hook so Vercel runs the lint before next build. Search-performance baseline + re-crawl windows in STATE.md "Search performance baseline" section. Diagnosis + repair: docs/audit/title-template-2026-05-16.md. Encoded as FAULT 15.
The Publish to MCP Registry workflow in SoapyRED/freightutils-mcp now fires on tag push (v*) in addition to GitHub Release + manual workflow_dispatch. The version-resolve step reads from input β tag β package.json so catch-up runs work without an explicit input. MCP Registry caught up in-sprint to freightutils-mcp@2.1.1 (isLatest=true confirmed via the public Registry API). Smithery and Glama are scrape-based β they re-fetch independently with days-of-lag, no event link from the Registry; the prior internal docs claim of a Registry β Smithery/Glama cascade was wrong and is corrected. The freightutils-distribution skill plus a new CLAUDE.md "Distribution cascade" section now anchor the actual sync model. Full diagnosis + version-bump runbook at docs/audit/distribution-cascade-2026-05-16.md. awesome-mcp-servers PR #5358 updated to the 19-tool surface; Punkpeye-merge pending.
All 17 records in lib/data/vehicles-ref.json carry per-record provenance (β₯2 sources each: EU Directive 96/53/EC, UK gov.uk vehicle weights & licence categories, 49 CFR Part 393 for US trailers, plus manufacturer specs from Schmitz Cargobull / Krone / Faymonville / DAF / Mercedes-Benz / VW / Ford / Wabash National) plus audited_at / verified / decision_rationale β surfaced on /api/vehicles. The customs duty calculator gained a structured DUTY_METHODOLOGY constant documenting the CIF β duty β VAT formula and Trade Tariff measure-type resolution (103 third-country, 142 preferential, 305 VAT, 695 anti-dumping, 277 restrictions), with 8 HMRC source URLs β exposed via GET /api/duty?methodology=true. A 10-code regression fixture (lib/data/duty-sample-fixture.json: 0901, 2204, 6110, 8471, 8703, 3004, 7113, 4011, 9504, 0207) anchors the live Trade Tariff API integration for future smoke-test extension. Sandbox HTTPS allowlist did not include the cited domains this audit pass β every record + the methodology flagged verified: false until live re-fetch. Gap report at docs/audit/vehicles-customs-completeness-2026-05-16.md; verification runbook at docs/audit/vehicles-customs-verification-2026-05-16.md.
New page at /hs/code/00000000 returns a 200 with a clearly-marked placeholder entry ("RESERVED FOR TESTING β NOT A REAL COMMODITY CODE", duty 0.00%); noindex,nofollow keeps honest crawlers out of the index. A hidden bait link (position:absolute; left:-9999px, tabindex=-1, aria-hidden, rel="nofollow") is seeded on /hs so DOM-parsing scrapers follow it. Hits are logged in middleware with the marker `[ScrapeGuard] HONEYPOT path=/hs/code/00000000 ip=β¦ ua=β¦`, fired before the HS rate-limit check so a trap-trigger is captured even when the IP is over its 10/5min budget. No auto-block this sprint β evidence collection first. Defensive filter on app/sitemap.ts keeps the path out of sitemap.xml.
50 sample HS codes (20 top-traffic, 15 DG-adjacent in chapters 27/28/29/36/85, 10 high-value in 84/87/71/62/22, 5 coverage) now carry per-record sources / audited_at / verified / decision_rationale via a new sample-provenance file merged at getCodeDetails β surfaced on /api/hs?code=β¦. The other 6,890 records of the 6,940-code HS 2022 dataset are untouched. Full-dataset gap report: 100% per-field populated, 97/99 chapters present (77 + 98 absent by HS-2022 design), 0 orphan parent references. Sandbox HTTPS allowlist did not reach trade-tariff.service.gov.uk this pass; every sample record flagged verified: false until live re-fetch.
All 10 sea-container records carry ISO 6346 codes (22G1, 42G1, 45G1, L5G1, 22R1, 45R1, 22U1, 42U1, 22P1, 42P1) plus per-record sources / audited_at / verified / decision_rationale β surfaced on /api/containers. 30 LHR-weighted UN/LOCODE anchors (GBLHR, USJFK, SGSIN, HKHKG, AEDXB and others) now carry per-record provenance via a new merged anchor-provenance file β surfaced on /api/unlocode?code=β¦. Sandbox HTTPS allowlist did not reach primary sources this pass, so no field values mutated; every record flagged verified: false until live re-fetch.
Fixed double-branded titles (root template now appends "| FreightUtils" instead of "| FreightUtils.com"; /changelog uses title.absolute). Dark-mode toggle syncs across tabs via the storage event. /api-docs rate-limit copy normalised to "25 requests per day" across all three mentions. NewsletterCapture added to /hs/code/[code] and /hs/heading/[heading] templates so HS detail pages match the ADR-detail email-capture pattern.
Middleware now emits the User-Agent and full client IP on every 429 β never on the success/cache-hit path. Supports evidence-based firewall rule additions. UA is sanitised (control chars stripped to prevent log injection, internal quotes replaced with apostrophes, truncated to 200 chars, falls back to `ua=empty` for null/whitespace). Log line format converted to space-separated `key=value` pairs for grep/awk parsing. IP resolution unchanged.
Scraper-target page routes now served from Vercel edge cache with explicit Cache-Control (s-maxage=86400, SWR=7d). Warm-cache hits skip page render β cold-serve response unchanged. ISR was already enabled on both routes; this surfaces the cache strategy in next.config.ts so it is tunable and visible via curl -I. The middleware-layer ScrapeGuard rate limiter continues to 429 the active 216.* scraper as designed.
Redis errors in the ScrapeGuard middleware now log at most once per minute per error class instead of per request, preventing the Sentry-event flood we saw during Upstash quota exhaustion on 2026-05-14 (~1K events in 30 min). Fail-open behaviour preserved β Redis failures still allow the request through.
25 anchor UN numbers (55 variant rows) now carry per-record `provenance.sources` URLs, `decision_rationale`, and `audited_at` timestamps. `/api/adr` response surfaces all three fields. Full-dataset gap audit at docs/audit/adr-completeness-2026-05-13.md.
Fixes a long-standing data fault where IATA prefix CV was misattributed; prefix index repaired so cargo-only filters now resolve cleanly. 35-airline anchor set gained per-record provenance. Cargolux, Cathay Cargo, Emirates SkyCargo, and others all verified.
7 verified, 9 variant-decision-needed. Magnitude corrections: AKH height 163β114 cm (LD3-45 is narrowbody-only); PLA dims/MGW rebased from PAJ-class to actual P1P (318Γ153Γ163, MGW 3175); ALP re-labelled LD7βLD11; RAP rewritten from passive blanket to active LD-9 thermal container (Cargolux/DSV/ACL/Wikipedia all confirm IATA registration); PMC-Q7 tall variant (300 cm) split into a separate entry. New provenance schema with `audited_at`, `chrome_verified_at`, `soap_signoff_at` timestamps. Predecessor PR #24 (PAG/PGA swap fix) consolidated here.
New /signin page and `/api/auth/public-signin` endpoint. Reveals nothing to anonymous callers about which email addresses are registered; `return_to` query parameter whitelisted to FreightUtils paths only. Nav now flips between "Sign in" and "Account" based on session-cookie identity.
Logged-in customers can view subscription status, billing history, and trigger Stripe customer-portal flows. Anonymous visitors to /account redirect to /signin. Session-cookie identity (`/api/identity/whoami`) + customer-portal endpoints land in the same PR.
Full subscription cancellation flow: customer-initiated cancel through Stripe portal, immediate admin email, customer confirmation email with UK Consumer Contracts Regulations 2013 Reg 37 waiver wording (no 14-day right to cancel after services are partly performed at customer request).
April 2026
New /contact page (mailto + form), /dpa page (Data Processing Agreement listing sub-processors: Vercel, Cloudflare, Stripe, Loops, Sentry). robots.txt and redirects audited and tightened in the same commit.
Fixes B029: an empty `X-API-Key` header was previously bucketed as anonymous instead of unauthenticated. n8n/Zapier integrations that test with an empty key during credential setup now correctly see 401 and surface the error to the user.
/api/mcp tool input schemas migrated from camelCase to snake_case to mirror the freightutils-mcp npm package v2.0.0. JSON contract stays consistent across the website MCP surface and the standalone npm/Zapier/n8n integrations.
Added `lib/seo/page-metadata.ts` builders for the four templated page families. Titles now β€60 chars with the primary search keyword in the first 50; descriptions β€155 chars with "free" + ("no login" / "updated YYYY"). Permanent `lint:seo-titles` build-time check enforces the pattern. Fixes the US CTR gap (0.07% vs UK 0.94%) flagged by GSC.
Fixes prefix misattributions on ZP and GS, backfills missing AWB prefix data for AV (Avianca) and UK (Air Bridge Cargo Airlines). 5 newly-active cargo airlines flagged in the dataset.
Every `app/api/**/route.ts` now wrapped with `withAuditRest` (or `withAuditMcp` for the MCP transport route). Emits a single structured `[fu-audit]` line per request with path, method, status, tier, latency. Privacy contract: NO request bodies, NO API key values, NO IPs, NO emails ever logged. Build-time `lint:audit` check fails if any new route forgets to wrap.
New endpoint that validates an API key against the registry and returns tier (free/pro) plus the key prefix (never the full key). Replaces /api/health as the credential-test target for n8n and Zapier integrations β fixes B028, where /health returned 200 to any caller and silently green-ticked invalid keys.
Fixes B003: /api/health reported 18 tools while /api/tools listed 17. Both endpoints now derive from a single registry; the missing entry (ADR LQ/EQ Check, shipped 2026-04-09) was added back. Adding a new tool propagates to both endpoints automatically.
Six endpoints migrated from camelCase to snake_case response fields (/api/unlocode, /api/uld, /api/containers, /api/vehicles, /api/consignment, /api/duty). Site-wide consistency with the other thirteen endpoints. Clean break β no dual-output. Full field rename table in CHANGELOG.md.
Five new platform pages plus a `/changelog.xml` RSS feed. Roadmap is GitHub-seeded so public-facing items stay in sync with the internal sprint plan. Versioning and deprecation policy documents codify API stability commitments.
Check Limited Quantity and Excepted Quantity eligibility for mixed dangerous goods consignments. Multi-item input with per-item breakdown, green/red/amber verdicts. POST /api/adr/lq-check.
15 unit load device types β AKE (LD3), PMC, PLA, and more. Dimensions, weights, volume, aircraft compatibility, and deck positions. Free REST API at /api/uld.
17 road freight vehicle and trailer types β curtainsiders, rigids, vans, US 53ft. Dimensions, payload limits, pallet capacity. Free REST API at /api/vehicles.
POST /api/shipment/summary chains CBM, LDM, chargeable weight, ADR compliance, and UK duty estimation into one response. Accepts road/air/sea/multimodal modes.
Consignment calculator and chargeable weight calculator now support sea freight mode with W/M (Weight or Measure) at 1 CBM = 1 revenue tonne.
Canonical FreightUtils Shipment schema defined β the foundational data model for all composite endpoints.
Estimate UK import duty and VAT for any commodity code using live GOV.UK Trade Tariff data. Supports Incoterm-adjusted CIF, preferential rate flagging, and cross-tool HS code workflow.
116,000+ transport locations from UNECE UN/LOCODE 2024-2. Searchable by name, code, country, and function type (port, airport, rail, road, ICD, border).
Calculate total CBM, weight, LDM, and chargeable weight across mixed items. Available as web tool, REST API, and MCP tool.
FreightUtils MCP Server v1.0.1 now on registry.modelcontextprotocol.io. Also listed on Smithery.ai, mcp.so, and 5+ directories.
Connect AI agents to all FreightUtils tools at freightutils.com/api/mcp/mcp. Streamable HTTP transport via mcp-handler.
Courtesy rate limit changed from 1,000/hour to 100 requests per day per IP across all API endpoints. Real enforcement via Vercel KV middleware.
Calculate ADR exemption thresholds for mixed hazardous loads. Supports multi-substance mixed-load calculation.
2,939 dangerous goods entries from UNECE ADR 2025 edition (licensed from Labeline.com).
6,940 codes from WCO HS 2022 nomenclature with full section/chapter/heading hierarchy.
6,352 entries with IATA/ICAO codes and AWB prefixes. Cargo-only default view with 390 cargo airlines.
March 2026
11 free freight tools with open REST APIs. LDM, CBM, chargeable weight, pallet fitting, container capacity, unit converter, ADR, airlines, INCOTERMS, HS codes.
Full OpenAPI spec covering all endpoints. Compatible with Swagger and Postman import.
All 11 Incoterms with seller/buyer responsibilities, risk transfer, cost transfer, and insurance details.
Dates are approximate. Data sourced from UNECE, WCO, ICC, IATA. For corrections or data issues contact contact@freightutils.com.