May 2026
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.
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, Postman, and RapidAPI 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. ADR data licensed from Labeline.com. For corrections or data issues contact contact@freightutils.com.