Maritime API — 9 paid vessel + Hormuz endpoints, one free facets surface

Published

TL;DR

What's live

Path Default price Bucket
/api/v1/vessels/facets free Discovery: enum dump of legal flag / type / subtype / commercial_market_name / country_code values. Daily-cached.
/api/v1/vessels/{imo} $0.001 Identity row for one vessel by IMO. Returns canonical metadata: name, AIS name, MMSI, flag, type, subtype, length, build year, transponder class, commercial market, sanctioned flag.
/api/v1/vessels/{imo}/position $0.001 Most recent AIS position for one vessel — (captured_utc, lat, lon, sog, cog, heading, destination, age_seconds).
/api/v1/vessels/{imo}/sanctions $0.001 Sanctions-list memberships for one vessel (OFAC SDN, UK OFSI, EU CFSP, Swiss SECO, …). One row per sanctioning body + listing date.
/api/v1/vessels/{imo}/track $0.010 AIS polyline for one vessel over the last hour (downsampled, oldest-first).
/api/v1/vessels/search $0.002 Faceted vessel-identity search across the ~600k-vessel mirror. Free-text q matches name prefix, IMO, or MMSI; facet filters on flag/type/subtype/commercial market; boolean sanctioned / watchlisted; numeric min_length / min_year_built. 20 rows/page.
/api/v1/vessels/in-area $0.010 Vessels currently inside a bbox (e.g. Strait of Hormuz), snapshot cursor for stable pagination. 10 rows/page.
/api/v1/vessels/sanctioned $0.025 Bulk sanctioned-fleet feed with last-known position for each row. 10 rows/page. ~1,400 vessels total → ~140 pages.
/api/v1/hormuz/crossings $0.010 Per-vessel Strait of Hormuz crossings over a rolling 14-day window. Date facets, sanctioned-only filter, direction (0/1/unknown). 10 rows/page.
/api/v1/hormuz/summary $0.005 Daily aggregate analytics: per-day transit counts (split by direction, sanctioned / watchlisted / distinct-operators), top-10 operators, top-10 flags. Single paid call returns three rollups in one response.

Per-route prices are env-tunable (X402_PRICE_MARINE_* knobs on the gate). The defaults above are what's currently in production.

Why these specific endpoints

We started from "what does an agent actually need to answer a maritime question?" and worked backwards:

  1. What's that ship?/api/v1/vessels/{imo} for the identity row + /position for where it is right now.
  2. Find me ships matching X./search with facets.
  3. What's currently in this area?/in-area with a bbox snapshot cursor.
  4. Show me the sanctioned fleet./sanctioned for the bulk dump, or /{imo}/sanctions for one vessel.
  5. What's been moving through Hormuz?/hormuz/crossings for the raw feed, /hormuz/summary for daily rollups.
  6. What facet values are legal for filtering?/vessels/facets (free, pre-flight).

That's it — no kitchen-sink "vessels API" with 50 verbs. Every paid route does one job and returns the smallest payload that answers it.

Pagination contract (worth understanding before you pay)

Every list endpoint uses an opaque cursor that encodes the original filter set. The pattern is:

  1. First call: pass your filters (?country_code=PA&type=Tanker&...), no cursor.
  2. Response includes next_cursor and has_more: true.
  3. Subsequent calls: pass ?cursor=<the opaque string> alone — DO NOT also repeat the original filters. The cursor carries them.
  4. When has_more: false (or next_cursor: null), you're done.

This matters because: - Cursors are stable across snapshots for /in-area (the cursor pins the snapshot timestamp, so pagination doesn't drift even if AIS data updates between calls). - Pages are hard-capped at 10-20 rows so per-page cost is predictable. A 20-row /search page is always $0.002 — never more, never less. - Repeating filters with a cursor is a soft conflict; the server prefers the cursor's encoded filters and ignores the duplicates. Saves the cost-walk debugging trip.

Free-tier walkthrough: /api/v1/vessels/facets

Before you spend a cent, hit /api/v1/vessels/facets. It's a single JSON response containing seven distinct enum dumps:

{
  "flags":      ["PA", "LR", "MH", "..."],
  "types":      ["Tanker", "Cargo", "Tug", "..."],
  "subtypes":   ["Crude Oil Tanker", "Chemical Tanker", "..."],
  "markets":    ["Tanker", "Wet Bulk", "..."],
  "countries":  ["Panama", "Liberia", "Marshall Islands", "..."],
  "sanction_sources": ["OFAC SDN", "UK OFSI", "EU CFSP", "..."],
  "operators":  [{"name": "...", "vessel_count": 472}, "..."]
}

Cache for ~24 hours and use the values verbatim as query-string inputs to /search. Without this dump, an agent has to guess that ?type=Tanker is valid but ?type=tanker isn't — and every guess is a paid call.

The endpoint is free for everyone, no x402, no UA gate. We take the position that taxonomy discovery is part of the public contract, not part of the revenue surface. Same policy as /api/v1/regions, /countries, and /event-types on the events side.

Sanctioned-fleet specifics

The /api/v1/vessels/sanctioned row count tracks the union of OFAC SDN, OFAC NS-MBS, UK OFSI, EU CFSP, Swiss SECO, and a handful of public sanctions watchlists. As of this post that's roughly 1,400 vessels. We don't deduplicate listings across bodies — a vessel listed by both OFAC and OFSI returns separate rows from /{imo}/sanctions, with each (body, listing_date) pair preserved.

Each row in the bulk feed carries the vessel's last known AIS position (or null if the vessel hasn't broadcast since being added). That makes the endpoint usable as a "where are the sanctioned ships right now?" query without an N+1 follow-up into /{imo}/position.

Hormuz endpoints

/api/v1/hormuz/crossings and /hormuz/summary are derived from a separate ShipTracker pipeline that watches a fixed bbox over the Strait of Hormuz and records each unique vessel transit (direction inferred from the trajectory's east-west component).

Both endpoints honour a ?sanctioned=true filter that intersects against the sanctioned-vessel set above.

Why per-call x402 vs a flat API key

The events corpus answered this two posts ago. Same answer here: AI agents are stateless, want to evaluate a single question without first negotiating a contract, and prefer the cost of an answer to scale with the size of the answer. A $0.001 per-call price on /vessels/{imo} means an agent asking "what's the IMO 9217981 vessel?" pays $0.001 and gets the JSON. No signup, no key rotation, no monthly minimum.

For high-volume buyers running thousands of /sanctioned page walks a day, the per-call model is also defensible: a full sweep of the sanctioned fleet (~140 pages × $0.025) costs ~$3.50, runs in under a minute, and is fully reproducible across runs because cursors are deterministic for a given filter.

Live mainnet receipts

Every advertised (facilitator, chain) cell on the marine routes was validated with a real paid request before this post went live, across the same four facilitators that already cover the events corpus (xpay on Base, CDP on Base + Polygon, PayAI on Base + Polygon + Avalanche, Dexter on Base via Permit2). The seeded settlements are visible in the per-facilitator counters on /admin/ai-traffic and on-chain via block explorers (basescan.org, polygonscan.com, snowtrace.io).