api.geoprimitives.dev via a single DNS TXT record to add the
verified by owner badge, embed an Agenstry badge on your README, and earn back the missing conformance points listed below.
Dispute or improve this rating
F
Conformance score: 34/100
F-grade: card is reachable but fails most operational signals.
click to expand breakdown ▾
click to collapse breakdown ▴
agent-card.json changed within the last 7 days. We track these so downstream callers can react.
Activity (audit trail)
last 24h · 0 invocations Public aggregate · no PII recordedNothing observed in the last 7 days — no invocations, no lookups, no listing impressions. Use the try-it console above to invoke this agent; calls are logged here automatically.
Endpoints
| Endpoint | Price | Currency |
|---|---|---|
https://api.geoprimitives.dev/v1/elevation
|
0.005 | USDC |
https://api.geoprimitives.dev/v1/elevation/profile
|
0.01 | USDC |
https://api.geoprimitives.dev/v1/geojson/repair
|
0.01 | USDC |
https://api.geoprimitives.dev/v1/geojson/validate
|
0.005 | USDC |
https://api.geoprimitives.dev/v1/geometry/area
|
0.01 | USDC |
https://api.geoprimitives.dev/v1/geometry/bbox
|
0.01 | USDC |
https://api.geoprimitives.dev/v1/geometry/buffer
|
0.01 | USDC |
https://api.geoprimitives.dev/v1/geometry/centroid
|
0.01 | USDC |
https://api.geoprimitives.dev/v1/geometry/difference
|
0.01 | USDC |
https://api.geoprimitives.dev/v1/geometry/intersect
|
0.01 | USDC |
https://api.geoprimitives.dev/v1/geometry/simplify
|
0.01 | USDC |
https://api.geoprimitives.dev/v1/geometry/union
|
0.01 | USDC |
https://api.geoprimitives.dev/v1/h3/index
|
0.005 | USDC |
https://api.geoprimitives.dev/v1/h3/polyfill
|
0.01 | USDC |
https://api.geoprimitives.dev/v1/hello
|
0.001 | USDC |
https://api.geoprimitives.dev/v1/marine/depth
|
0.005 | USDC |
https://api.geoprimitives.dev/v1/marine/hazards
|
0.005 | USDC |
https://api.geoprimitives.dev/v1/pip
|
0.01 | USDC |
https://api.geoprimitives.dev/v1/timezone
|
0.005 | USDC |
https://api.geoprimitives.dev/v1/transform
|
0.005 | USDC |
https://api.geoprimitives.dev/v1/us/geographies
|
0.01 | USDC |
0x76a51b5dd3729950b3b60c17f19252d221968fce · basescan ↗
| Agent card | https://api.geoprimitives.dev |
| Provider | https://api.geoprimitives.dev |
| Docs | https://api.geoprimitives.dev |
Health · last 0 probes
Similar agents embedding-nearest
Embed your Agenstry badge
Paste any of these into your README, agent card, or marketing page. Each badge auto-updates and links back to this page.
Markdown / HTML snippets
[](https://agenstry.com/agents/api.geoprimitives.dev) [](https://agenstry.com/agents/api.geoprimitives.dev) [](https://agenstry.com/agents/api.geoprimitives.dev) [](https://agenstry.com/agents/api.geoprimitives.dev)
Audit-grade evidence bundle
JSON snapshot for vendor-review files. Add ?sign=true for a JWS-signed envelope verifiable against
our JWKS. See the methodology.
Raw agent card JSON
{
"_source": "agentic.market",
"service": {
"id": "api-geoprimitives-dev",
"name": "api.geoprimitives.dev",
"description": "",
"domain": "api.geoprimitives.dev",
"provider": "api.geoprimitives.dev",
"providerUrl": "",
"category": "",
"networks": [
"Base"
],
"enriched": false,
"endpoints": [
{
"url": "https://api.geoprimitives.dev/v1/elevation",
"description": "Give it a latitude and longitude and it answers with the ground elevation there in metres above NAVD88, read from the USGS 3D Elevation Program seamless DEM. Parameters go in the query string: this is a GET, there is no body. COVERAGE, stated up front because it is the first thing to check: the state of Alaska, whole, and nothing else yet. The coverage gate reads the coordinate, so it runs in the handler and after payment, not in front of the 402: a coordinate outside Alaska is quoted like any other and then refused with outside_coverage. A refusal does not settle, so nothing is charged, but it costs a signature and a second round trip to find out. Check the coverage against this description or the free catalog first. The conterminous United States and Hawaii are the next extension and arrive through the same pipeline with no change to this interface; every response repeats the coverage under `coverage` so an agent never infers it from an error. Canada is not served even where USGS publishes pixels over it: those come from Natural Resources Canada under a licence with an attribution clause, and this catalog carries public-domain data only, so they were not stored. SOURCE AND VINTAGE: USGS 3DEP seamless 2 arc-second product, the resolution USGS publishes for Alaska, a work of the United States government in the public domain (17 U.S.C. 105). A 2 arc-second pixel is about 62 m north to south and 20 to 30 m east to west at Alaskan latitudes; the response states the pixel size at your latitude. USGS republishes blocks as new lidar and IFSAR arrive, and the response carries the publication date of the block that answered you. Heights are orthometric, NAVD88, metres; horizontal positions are NAD83, which this endpoint reads as WGS 84 since the two differ by under two metres, a fraction of a pixel. INTERPOLATION: bilinear over the four pixel centres around the point by default, which is what GDAL and PROJ do for a continuous surface. Pass interpolation=nearest for the raster's own value at the pixel the point falls in, which is bit-identical to gdallocationinfo on the USGS file and the right choice when you want the source unblended. Checked against GDAL rather than against itself: of 200 coordinates drawn over the built blocks, the 150 inside the coverage agreed with `gdallocationinfo -r bilinear` on the USGS GeoTIFFs to 6.2e-4 m at worst, and interpolation=nearest agreed with plain `gdallocationinfo` to the millimetre this endpoint rounds to. The response always names the method used. VOIDS AND WATER: gaps in the USGS footprint and the Canadian side of the border are void, and so is open water beyond the three-mile county limit. A void pixel inside a bilinear window is dropped and the remaining weights renormalised, as gdalwarp treats nodata, and the response says how many of the four were used. A window that is entirely void is refused with no_data and not charged, never answered with zero. Inshore water is a different case and you should know it: USGS writes 0 over the sea in some blocks and nodata in others, so a coordinate in a harbour or a bay inside the county limit may answer 0 m, which is the source's value and not a measurement of anything. ACCURACY of the data itself is USGS's, not this endpoint's: the Alaska seamless layer is built from IFSAR at 5 m resampled to 2 arc-seconds, and USGS states its vertical accuracy per source project rather than for the seamless layer as a whole. Expect metre-level agreement with a survey on open terrain and worse under forest and on steep slopes. This endpoint adds nothing to that error beyond interpolation. Longitude and latitude are checked against EPSG:4326 bounds first. Reproject with POST /v1/transform if you are not already in WGS 84 or NAD83. For a line rather than a point use POST /v1/elevation/profile, which samples along a polyline in one call.",
"pricing": {
"amount": "0.005",
"currency": "USDC",
"network": "eip155:8453",
"scheme": "exact",
"maxAmount": "",
"minAmount": ""
},
"method": "GET",
"providerName": "",
"parameters": [
{
"group": "query",
"name": "lat",
"type": "number",
"description": "",
"example": 61.2181,
"enumValues": [],
"default": null,
"required": false
},
{
"group": "query",
"name": "lon",
"type": "number",
"description": "",
"example": -149.8997,
"enumValues": [],
"default": null,
"required": false
}
],
"serviceName": "geoprimitives",
"tags": [
"elevation",
"dem",
"3dep",
"alaska",
"navd88"
],
"quality": {
"l30DaysTotalCalls": "3",
"l30DaysUniquePayers": "1"
}
},
{
"url": "https://api.geoprimitives.dev/v1/elevation/profile",
"description": "Send a polyline as an array of [longitude, latitude] positions and it answers with elevations sampled at equal geodesic spacing along it, from the USGS 3D Elevation Program seamless DEM, plus the cumulative gain and loss and the minimum and maximum. One call for a whole route rather than one call per point: 2000 samples cost what two cost. COVERAGE is the state of Alaska, whole, and nothing else yet; see GET /v1/elevation for the full statement, which this route shares and repeats under `coverage` on every response. Every sample is checked against the coverage before a tile is read, and a profile with any sample outside Alaska is refused with outside_coverage naming the sample. That check reads coordinates, so it runs after payment like the rest of the handler; the refusal does not settle and nothing is charged, but it costs a signature and a second round trip. Canada is not served even where USGS publishes pixels over it, for the licensing reason stated there. SAMPLING: samples are placed at equal geodesic distance (GRS 80) along the polyline, each on the great circle of its segment, the first at the first position and the last at the last. Omit `samples` to sample every 60 m, the native pixel height, up to 2000; pass it to choose the count. Each sample is bilinear over the four pixel centres around it by default, or `nearest` for the raster's own values; the response names the method. The source is the 2 arc-second product USGS publishes for Alaska, about 62 m north to south, so asking for spacing much under 60 m buys interpolation rather than information. VOIDS inside the line: gaps in the USGS footprint, the Canadian side of the border and water beyond the county limit are void. A sample whose window is entirely void is null in the list and counted in `stats.voidSamples`; gain and loss are summed over consecutive non-null samples so a crossing of void water is not a descent to zero and back. Inshore water is 0 in some USGS blocks rather than void, so a line across a bay may read 0 m there, which is the source's value; see GET /v1/elevation. A profile that is void throughout is refused with no_data and not charged. SOURCE, VINTAGE, DATUMS AND ACCURACY are those of GET /v1/elevation: USGS 3DEP, public domain, NAVD88 metres, NAD83 horizontal read as WGS 84, IFSAR-derived in Alaska with metre-level vertical accuracy on open ground. Gain and loss over a 2 arc-second surface are an estimate of the terrain's, not of a track's: fine structure narrower than a pixel is not in the data. Limits: 500 positions in the polyline, 2000 samples, 1000 km of length, 512 elevation tiles read per call (about 2000 km of profile at the coarsest sampling; a 100 km profile at native spacing reads about 40), and a 1 MiB body. All are checked before a tile is read.",
"pricing": {
"amount": "0.01",
"currency": "USDC",
"network": "eip155:8453",
"scheme": "exact",
"maxAmount": "",
"minAmount": ""
},
"method": "POST",
"providerName": "",
"parameters": [
{
"group": "body",
"name": "coordinates",
"type": "array",
"description": "",
"example": [],
"enumValues": [],
"default": null,
"required": false
},
{
"group": "body",
"name": "samples",
"type": "number",
"description": "",
"example": 5,
"enumValues": [],
"default": null,
"required": false
}
],
"serviceName": "geoprimitives",
"tags": [
"elevation",
"profile",
"dem",
"3dep",
"alaska"
],
"quality": {
"l30DaysTotalCalls": "1",
"l30DaysUniquePayers": "1"
}
},
{
"url": "https://api.geoprimitives.dev/v1/geojson/repair",
"description": "Repair GeoJSON and get the fixed document back with a log of every alteration: rings rewound to RFC 7946 winding order, consecutive duplicate vertices dropped, unclosed rings closed, and polygon nesting fixed (mis-ordered exterior rings, holes that fall outside their exterior, MultiPolygon members that are really holes). Reach for it when POST /v1/geojson/validate reported errors, or when /v1/geometry, /v1/h3/polyfill or /v1/pip refused your document -- unclosed rings are the usual reason, and this is the cheapest way to clear them. Conservative by design: it only rewrites the classes above, and anything else it found is returned in unrepaired rather than guessed at, so it will not invent geometry or move a coordinate. Idempotent -- a conformant document comes back untouched with changed:false, so a second call costs money and changes nothing. Your input object is never mutated. Coordinate values are preserved exactly; nothing is rounded or reprojected. Limit: 1 MiB request body.",
"pricing": {
"amount": "0.01",
"currency": "USDC",
"network": "eip155:8453",
"scheme": "exact",
"maxAmount": "",
"minAmount": ""
},
"method": "POST",
"providerName": "",
"parameters": [
{
"group": "body",
"name": "coordinates",
"type": "array",
"description": "",
"example": [],
"enumValues": [],
"default": null,
"required": false
},
{
"group": "body",
"name": "type",
"type": "string",
"description": "",
"example": "Polygon",
"enumValues": [],
"default": null,
"required": false
}
],
"serviceName": "geoprimitives",
"tags": [
"geojson",
"repair",
"rfc7946"
],
"quality": {
"l30DaysTotalCalls": "1",
"l30DaysUniquePayers": "1"
}
},
{
"url": "https://api.geoprimitives.dev/v1/geojson/validate",
"description": "Validate GeoJSON against RFC 7946 and get back a structured list of problems, not a bare boolean. Checks structure, coordinate range, ring closure, ring winding order, consecutive duplicate vertices, self-intersections, and hole nesting. Every issue carries the feature, geometry, ring and position index it sits on, so a 500-feature collection is actionable rather than merely condemned. Reach for it before paying for an operation on data you did not author, and to find out why /v1/geometry, /v1/h3/polyfill or /v1/pip refused a document: those endpoints reject unclosed rings and out-of-range coordinates rather than guessing. Errors make a document invalid; warnings -- duplicate vertices, the crs member RFC 7946 removed -- do not, and are reported separately. One price per call whatever the document: a single Point costs exactly what a 1 MiB FeatureCollection costs, so send the whole collection rather than a document at a time. That is measured rather than assumed -- a Point is about 0.0015 ms of compute on this runtime and a 1 MiB collection about 10 ms, a spread far below what it costs to settle one payment -- which is why there is no size tier here to choose between and no bulk sibling to look for. Nothing is modified here: POST /v1/geojson/repair is the endpoint that rewrites. Limits: 1 MiB request body, and a self-intersection budget of 2000000 segment-pair comparisons per request, enough for any ring under about 1400 vertices. Rings past the budget are listed in checksSkipped with the reason, never silently passed as clean.",
"pricing": {
"amount": "0.005",
"currency": "USDC",
"network": "eip155:8453",
"scheme": "exact",
"maxAmount": "",
"minAmount": ""
},
"method": "POST",
"providerName": "",
"parameters": [
{
"group": "body",
"name": "coordinates",
"type": "array",
"description": "",
"example": [],
"enumValues": [],
"default": null,
"required": false
},
{
"group": "body",
"name": "type",
"type": "string",
"description": "",
"example": "Polygon",
"enumValues": [],
"default": null,
"required": false
}
],
"serviceName": "geoprimitives",
"tags": [
"geojson",
"validation",
"rfc7946"
],
"quality": {
"l30DaysTotalCalls": "1",
"l30DaysUniquePayers": "1"
}
},
{
"url": "https://api.geoprimitives.dev/v1/geometry/area",
"description": "Measure how much ground a polygon covers, in square metres and square kilometres, with holes subtracted and every part of a MultiPolygon summed. Use it after POST /v1/geometry/intersect to size an overlap, or on a boundary you already hold. Points and lines contribute zero rather than an error, so a mixed collection is safe to send. Not turf's area, which applies the spherical-excess formula to geodetic latitudes on a 6371008.8 m sphere and comes out 0.45% high at the equator and 0.85% low at 80 deg latitude. This maps latitude to authalic latitude on the WGS 84 authalic sphere first, landing within 5e-3% of GeographicLib's geodesic area on parcel-scale rings and converging to 1e-6% as ring edges are densified. Ring edges are treated as straight in longitude/latitude, per RFC 7946 and turf; a geodesic-edge area differs on long slanted edges, by 1.9% on a 1.7-degree triangle. Winding order is ignored: ring 0 is the exterior and the rest are holes. Input is EPSG:4326 only. Limits: 100000 positions across the whole document, 1 MiB request body.",
"pricing": {
"amount": "0.01",
"currency": "USDC",
"network": "eip155:8453",
"scheme": "exact",
"maxAmount": "",
"minAmount": ""
},
"method": "POST",
"providerName": "",
"parameters": [
{
"group": "body",
"name": "geojson",
"type": "object",
"description": "",
"example": null,
"enumValues": [],
"default": null,
"required": false
}
],
"serviceName": "geoprimitives",
"tags": [
"geometry",
"polygon",
"area",
"measurement",
"wgs84"
],
"quality": {
"l30DaysTotalCalls": "1",
"l30DaysUniquePayers": "1"
}
},
{
"url": "https://api.geoprimitives.dev/v1/geometry/bbox",
"description": "Get the extent of a geometry or a whole collection: a map viewport, a tile query window, a cheap prefilter before you pay for a containment test. Takes any geometry type, not just polygons. Bounding box of any geometry as [minLongitude, minLatitude, maxLongitude, maxLatitude]. Naive minimum and maximum per axis, so a geometry spanning the antimeridian reports a full-width longitude range rather than the narrow band it occupies; the response flags that case instead of guessing which side you meant. Input is EPSG:4326 only. Limits: 100000 positions across the whole document, 1 MiB request body.",
"pricing": {
"amount": "0.01",
"currency": "USDC",
"network": "eip155:8453",
"scheme": "exact",
"maxAmount": "",
"minAmount": ""
},
"method": "POST",
"providerName": "",
"parameters": [
{
"group": "body",
"name": "geojson",
"type": "object",
"description": "",
"example": null,
"enumValues": [],
"default": null,
"required": false
}
],
"serviceName": "geoprimitives",
"tags": [
"geometry",
"bbox",
"extent",
"measurement"
],
"quality": {
"l30DaysTotalCalls": "1",
"l30DaysUniquePayers": "1"
}
},
{
"url": "https://api.geoprimitives.dev/v1/geometry/buffer",
"description": "Buffer a geometry by a distance in metres and get the offset polygon back. Use it for service areas, setbacks, and proximity envelopes you will later test with POST /v1/pip. Accepts any geometry -- point, line, polygon, or a collection of them, each buffered about its own centre latitude -- and answers with a Feature, or a FeatureCollection when you sent several. A buffer that collapses (a negative distance larger than the shape, or any negative distance on a point or a line) answers geojson:null with empty:true, which is an answer and not an error. Geodesic-aware: turf buffers in an azimuthal equidistant plane scaled by a sphere, which is a systematic -0.56% error at the equator running to +0.44% at 80 deg latitude, and this endpoint cancels that by scaling the distance to the Gaussian radius of curvature at the feature's own centre latitude. Measured against GeographicLib the result is then within +/-0.34% of the requested distance at the equator, +/-0.08% at 60 deg, +/-0.04% at 70 deg and +/-0.01% at 80 deg; the residual is the ellipsoid's north-south versus east-west curvature, which no single sphere can represent, and it shrinks towards the poles. Between vertices the polygon edge cuts a further 1 - cos(pi/(4*steps)) inside the true offset curve, 0.054% at the default steps=24. Input is EPSG:4326 only. Limits: 2000 positions across the whole document, 1 MiB request body, distance within +/-1000000 m, steps 1 to 128.",
"pricing": {
"amount": "0.01",
"currency": "USDC",
"network": "eip155:8453",
"scheme": "exact",
"maxAmount": "",
"minAmount": ""
},
"method": "POST",
"providerName": "",
"parameters": [
{
"group": "body",
"name": "distance",
"type": "number",
"description": "",
"example": 500,
"enumValues": [],
"default": null,
"required": false
},
{
"group": "body",
"name": "geojson",
"type": "object",
"description": "",
"example": null,
"enumValues": [],
"default": null,
"required": false
},
{
"group": "body",
"name": "steps",
"type": "number",
"description": "",
"example": 2,
"enumValues": [],
"default": null,
"required": false
}
],
"serviceName": "geoprimitives",
"tags": [
"geometry",
"polygon",
"buffer",
"turf"
],
"quality": {
"l30DaysTotalCalls": "1",
"l30DaysUniquePayers": "1"
}
},
{
"url": "https://api.geoprimitives.dev/v1/geometry/centroid",
"description": "Find the area centroid of a polygon: a representative point for labelling, joining, or reverse geocoding a shape you only have a boundary for. It is the centre of mass of the area with holes subtracted, not the average of the vertices, so densely sampled edges do not drag it. Know what a centroid is not: on a concave or ring-shaped polygon the centre of mass can fall outside the polygon itself, and this endpoint returns it anyway rather than nudging the point inside. Test it with POST /v1/pip if you need a point that is guaranteed to be inside. Not turf's centroid, which is the arithmetic mean of the vertices and drifts towards whichever part of the boundary happens to be densely sampled. The moment integral is evaluated in a Lambert cylindrical equal-area projection of the WGS 84 authalic sphere, so the cos(latitude) area weighting is right at high latitude, then inverted back to longitude and latitude. Non-areal input, and polygons that enclose zero area, fall back to the vertex mean and say so in the method field. Input is EPSG:4326 only. Limits: 100000 positions across the whole document, 1 MiB request body.",
"pricing": {
"amount": "0.01",
"currency": "USDC",
"network": "eip155:8453",
"scheme": "exact",
"maxAmount": "",
"minAmount": ""
},
"method": "POST",
"providerName": "",
"parameters": [
{
"group": "body",
"name": "geojson",
"type": "object",
"description": "",
"example": null,
"enumValues": [],
"default": null,
"required": false
}
],
"serviceName": "geoprimitives",
"tags": [
"geometry",
"polygon",
"centroid",
"measurement",
"wgs84"
],
"quality": {
"l30DaysTotalCalls": "1",
"l30DaysUniquePayers": "1"
}
},
{
"url": "https://api.geoprimitives.dev/v1/geometry/difference",
"description": "Cut holes out of a polygon: the buildable part of a parcel once setbacks are removed, the land outside a protected area, the part of a route not already covered. Order is significant, so this is the one overlay op where swapping the operands gives a different answer. Subtracting everything answers geojson:null with empty:true, a real answer rather than an error. Send a FeatureCollection of Polygons or MultiPolygons; the first member in document order has every later member subtracted from it. The answer is a single Feature, or null with empty:true when nothing survives. Clipping is planar in longitude/latitude, so edges are straight in degrees rather than geodesics. The 2000-position cap is CPU rather than bandwidth: the sweep-line overlay is quadratic on self-intersecting input. Input is EPSG:4326 only. Limits: 2000 positions across all members, 1 MiB request body, and at least two polygonal geometries or the call is refused.",
"pricing": {
"amount": "0.01",
"currency": "USDC",
"network": "eip155:8453",
"scheme": "exact",
"maxAmount": "",
"minAmount": ""
},
"method": "POST",
"providerName": "",
"parameters": [
{
"group": "body",
"name": "geojson",
"type": "object",
"description": "",
"example": null,
"enumValues": [],
"default": null,
"required": false
}
],
"serviceName": "geoprimitives",
"tags": [
"geometry",
"polygon",
"difference",
"overlay",
"turf"
],
"quality": {
"l30DaysTotalCalls": "1",
"l30DaysUniquePayers": "1"
}
},
{
"url": "https://api.geoprimitives.dev/v1/geometry/intersect",
"description": "Clip polygons to their common area: how much of a parcel falls inside a flood zone, where two service areas overlap, what a boundary and a study area share. Pair it with POST /v1/geometry/area to turn the overlap into square metres. Disjoint input answers geojson:null with empty:true, which is a real answer and not an error, so check empty rather than treating null as a failure. Send a FeatureCollection of Polygons or MultiPolygons; the answer is the common area as a single Feature. Clipping is planar in longitude/latitude, so edges are straight in degrees rather than geodesics. The 2000-position cap is CPU rather than bandwidth: the sweep-line overlay is quadratic on self-intersecting input. Input is EPSG:4326 only. Limits: 2000 positions across all members, 1 MiB request body, and at least two polygonal geometries or the call is refused.",
"pricing": {
"amount": "0.01",
"currency": "USDC",
"network": "eip155:8453",
"scheme": "exact",
"maxAmount": "",
"minAmount": ""
},
"method": "POST",
"providerName": "",
"parameters": [
{
"group": "body",
"name": "geojson",
"type": "object",
"description": "",
"example": null,
"enumValues": [],
"default": null,
"required": false
}
],
"serviceName": "geoprimitives",
"tags": [
"geometry",
"polygon",
"intersect",
"overlay",
"turf"
],
"quality": {
"l30DaysTotalCalls": "1",
"l30DaysUniquePayers": "1"
}
},
{
"url": "https://api.geoprimitives.dev/v1/geometry/simplify",
"description": "Drop vertices from a geometry while keeping its shape. Use it to cut a boundary down before a call whose cost is vertex-driven -- POST /v1/pip caps on points times polygon vertices, and POST /v1/geometry/buffer and the overlay ops cap at 2000 positions -- and to shrink what you store or draw. The response reports outputVertices next to inputVertices, so the saving is measurable before you spend again. The algorithm is Douglas-Peucker. Send toleranceMeters for a ground tolerance applied isotropically: both axes are scaled to metres at the input's centre latitude first, so at 70 deg N the simplification is not three times as aggressive east-west as north-south, which a raw degree tolerance makes it. Send tolerance instead for the raw degree tolerance if you want turf's behaviour unchanged. Retained vertices come back exactly as sent rather than round-tripped through the scaling. Not topology-preserving: adjacent polygons simplified separately can gap or overlap along a shared edge. Input is EPSG:4326 only. Limits: 100000 positions across the whole document, 1 MiB request body.",
"pricing": {
"amount": "0.01",
"currency": "USDC",
"network": "eip155:8453",
"scheme": "exact",
"maxAmount": "",
"minAmount": ""
},
"method": "POST",
"providerName": "",
"parameters": [
{
"group": "body",
"name": "geojson",
"type": "object",
"description": "",
"example": null,
"enumValues": [],
"default": null,
"required": false
},
{
"group": "body",
"name": "toleranceMeters",
"type": "number",
"description": "",
"example": 100,
"enumValues": [],
"default": null,
"required": false
}
],
"serviceName": "geoprimitives",
"tags": [
"geometry",
"polygon",
"simplify",
"douglas-peucker",
"turf"
],
"quality": {
"l30DaysTotalCalls": "1",
"l30DaysUniquePayers": "1"
}
},
{
"url": "https://api.geoprimitives.dev/v1/geometry/union",
"description": "Dissolve overlapping or adjacent polygons into a single shape: merging parcels, coverage areas or service zones into one boundary before measuring or testing it. Send a FeatureCollection of Polygons or MultiPolygons; the answer is a single Polygon or MultiPolygon Feature, or null with empty:true when nothing is left. Clipping is planar in longitude/latitude, the way every polygon-clipping library does it: edges are straight in degrees rather than geodesics, and the two differ on edges spanning more than about a degree. The 2000-position cap is CPU rather than bandwidth: the sweep-line overlay is quadratic on self-intersecting input, where 2000 positions is already about 350 ms. Input is EPSG:4326 only. Limits: 2000 positions across all members, 1 MiB request body, and at least two polygonal geometries or the call is refused.",
"pricing": {
"amount": "0.01",
"currency": "USDC",
"network": "eip155:8453",
"scheme": "exact",
"maxAmount": "",
"minAmount": ""
},
"method": "POST",
"providerName": "",
"parameters": [
{
"group": "body",
"name": "geojson",
"type": "object",
"description": "",
"example": null,
"enumValues": [],
"default": null,
"required": false
}
],
"serviceName": "geoprimitives",
"tags": [
"geometry",
"polygon",
"union",
"overlay",
"turf"
],
"quality": {
"l30DaysTotalCalls": "1",
"l30DaysUniquePayers": "1"
}
},
{
"url": "https://api.geoprimitives.dev/v1/h3/index",
"description": "Index points to H3 cells. Send \"points\" as [longitude, latitude] positions in EPSG:4326 and get each one's containing H3 cell at one or several resolutions, optionally with the cell's centre and its closed boundary ring. Use it to bucket points into a fixed hexagonal grid so datasets that share no keys can be joined, aggregated or deduplicated on cell id. This route indexes positions and nothing else. It does not accept polygons: a body carrying \"geojson\" is refused rather than served, and because \"points\" is required here that refusal costs you nothing -- it happens before a price is quoted. To cover a Polygon or MultiPolygon with the cells that tile it, POST /v1/h3/polyfill instead. That is a separate route at a higher price because it is a materially more expensive job: measured on this runtime, indexing one point takes about 0.003 ms and returns 168 bytes, while the cheapest possible polyfill takes about 1.5 ms -- the cell count has to be estimated before any cell is generated -- and a polyfill at its cell cap is about 200 ms and 1.8 MB. This route is priced with POST /v1/transform, which does the same class of per-position work, and below POST /v1/h3/polyfill. Every call needs exactly one of \"resolution\" or \"resolutions\". Input is EPSG:4326, longitude then latitude; out-of-range values are refused rather than normalised, because H3 will happily index a latitude of 950 and answer with a real, wrong cell. Limits: 10000 points, 4 resolutions per call, and a 1 MiB request body.",
"pricing": {
"amount": "0.005",
"currency": "USDC",
"network": "eip155:8453",
"scheme": "exact",
"maxAmount": "",
"minAmount": ""
},
"method": "POST",
"providerName": "",
"parameters": [
{
"group": "body",
"name": "points",
"type": "array",
"description": "",
"example": [],
"enumValues": [],
"default": null,
"required": false
},
{
"group": "body",
"name": "resolution",
"type": "number",
"description": "",
"example": 9,
"enumValues": [],
"default": null,
"required": false
}
],
"serviceName": "geoprimitives",
"tags": [
"h3",
"index",
"grid",
"hexagon",
"points"
],
"quality": {
"l30DaysTotalCalls": "1",
"l30DaysUniquePayers": "1"
}
},
{
"url": "https://api.geoprimitives.dev/v1/h3/polyfill",
"description": "Polyfill: turn a polygon into the set of H3 cells that cover it, so a spatial question becomes a set-membership question you can answer with a join. Send \"geojson\" as a Polygon, a MultiPolygon, or a Feature or FeatureCollection wrapping them, in EPSG:4326, plus exactly one of \"resolution\" or \"resolutions\". Choose the containment rule with \"containment\": \"center\" returns cells whose centre is inside the polygon, which is H3's default and under-covers at the edges; \"overlapping\" returns every cell the polygon touches, so the set fully covers it; \"full\" returns only cells entirely inside. Holes are respected, a MultiPolygon is treated as one polygon with its parts unioned and deduplicated, and feature ids are echoed so rows join back to your records. This route covers polygons and nothing else: a body carrying \"points\" is refused rather than served, and because \"geojson\" is required here that refusal happens before a price is quoted and costs you nothing. To index individual positions to their containing cell, POST /v1/h3/index instead, which is cheaper because it is a much smaller job. What you are paying for here, measured on this runtime: cell count grows about sevenfold per resolution step, so the cost has to be estimated before any cell is generated, which puts a floor of about 1.5 ms on even a city-block polyfill against about 0.003 ms for a one-point index call, and a polyfill at the 100000-cell cap is about 200 ms and 1.8 MB. Anything estimated above that cap is refused with the finest resolution that would have fitted, before a cell is generated; set \"estimateOnly\" to size a job for the price of the call and nothing else. Input is EPSG:4326, longitude then latitude, and non-areal geometry is refused by name. Limits: 500 polygons, 10000 polygon positions, 4 resolutions per call, 100000 cells per call, and a 1 MiB request body.",
"pricing": {
"amount": "0.01",
"currency": "USDC",
"network": "eip155:8453",
"scheme": "exact",
"maxAmount": "",
"minAmount": ""
},
"method": "POST",
"providerName": "",
"parameters": [
{
"group": "body",
"name": "geojson",
"type": "object",
"description": "",
"example": null,
"enumValues": [],
"default": null,
"required": false
},
{
"group": "body",
"name": "resolution",
"type": "number",
"description": "",
"example": 8,
"enumValues": [],
"default": null,
"required": false
}
],
"serviceName": "geoprimitives",
"tags": [
"h3",
"polyfill",
"grid",
"hexagon",
"polygon"
],
"quality": {
"l30DaysTotalCalls": "1",
"l30DaysUniquePayers": "1"
}
},
{
"url": "https://api.geoprimitives.dev/v1/hello",
"description": "Echo endpoint. Returns the name you send, the network the payment settled on, and the price you paid. Use it to prove your x402 client works end to end -- discover, read the 402, sign, retry, get a body -- before you spend anything on a real endpoint. It is the cheapest route in the catalog and it computes nothing, so a failure here is a payment problem and never a geospatial one. Do not use it for anything else: it has no geospatial behaviour at all. It is priced at exactly what it costs us to settle one onchain transaction, so the whole of what you pay here goes to the facilitator and none of it to us. That is deliberate: this route is worth having only because it is genuinely paid, since a free echo would prove nothing about whether your client can pay.",
"pricing": {
"amount": "0.001",
"currency": "USDC",
"network": "eip155:8453",
"scheme": "exact",
"maxAmount": "",
"minAmount": ""
},
"method": "POST",
"providerName": "",
"parameters": [
{
"group": "body",
"name": "name",
"type": "string",
"description": "",
"example": "world",
"enumValues": [],
"default": null,
"required": false
}
],
"serviceName": "geoprimitives",
"tags": [
"test",
"x402",
"echo"
],
"quality": {
"l30DaysTotalCalls": "2",
"l30DaysUniquePayers": "1"
}
},
{
"url": "https://api.geoprimitives.dev/v1/marine/depth",
"description": "Give it a latitude and longitude and it answers with what the nautical chart says about the water there: the depth range of the charted depth area containing the position, the nearest charted sounding and how far away it is, and the chart's own zone of confidence for that area. Parameters go in the query string: this is a GET, there is no body. NOT FOR NAVIGATION, stated first because it is a condition of using the source. This is derived from NOAA Electronic Navigational Charts and redistributed; it is not an official NOAA ENC and does not meet federal chart carriage regulations under 33 CFR 164 or SOLAS chapter V, which only charts from NOAA or a certified NOAA ENC distributor do. Every response repeats this under `disclaimer`. NOAA and NOAA ENC are registered trademarks of NOAA, which does not endorse this service. COVERAGE is 65 NOAA ENC cells of usage bands 3, 4 and 5 over five working areas of southeast and southcentral Alaska: Sitka Sound; Kasaan Bay and Clarence Strait; Metlakatla, Ketchikan and Revillagigedo Channel; Juneau, Gastineau Channel and Lynn Canal; and Cordova with eastern Prince William Sound. It is not statewide, it is not the whole coast, and the small-scale cells that would make it look wider were deliberately left out. The coverage check reads the coordinate, so it runs in the handler after payment rather than in front of the 402: a position off the charts is quoted like any other and then refused with outside_coverage. A refusal does not settle and nothing is charged, but it costs a signature and a round trip, so check the coverage first. Every response repeats it under `coverage`. NOTHING IS INTERPOLATED, and that is the difference between this and GET /v1/elevation. Terrain is a continuous surface and sampling it between pixels is legitimate; a chart is a set of assertions a hydrographer made, and averaging two of them invents a depth nobody surveyed. So `depthAreaMinimum` is DRVAL1 of the charted depth area the position falls inside, the shoalest depth that area is guaranteed to hold, and `nearestSounding` is a real charted sounding with the geodesic distance to it. The two routinely disagree and are meant to: a sounding is a measurement at a point and the area is a guarantee over a region. The guarantee is the number to navigate a plan by; the sounding tells you how the area was arrived at. WHICH CHART ANSWERS: ENC cells overlap on purpose, so a position in Sitka sits on a 1:12,000 harbour cell, a 1:45,000 approach cell and a 1:180,000 coastal cell at once, and they do not agree because the smaller scales generalise. The largest-scale cell covering the position wins outright, usage band first and then compilation scale, which is the rule an ECDIS follows. The response names the cell, its band, its scale, its edition and its NOAA issue date, and lists any cell it consulted first that charted no depth area there. SURVEY QUALITY is on every answer as the chart's CATZOC zone of confidence, with the position and depth accuracy that zone implies, and the survey start and end dates when the chart gives them. Read it before you trust a depth. NOAA's own warning applies to all of it: much of the depth information on NOAA charts is from surveys conducted before 1940 and the shoreline is more than 20 years old. Zone A1 is a modern full-coverage survey; zone D or U means the chart is telling you it does not know. DATUMS: depths are metres below Mean Lower Low Water, which is the US chart datum, so a real depth at a real moment is this number plus the tide. Heights on the same charts are above Mean High Water. Positions are WGS 84. Reproject with POST /v1/transform if you are in State Plane or NAD27; the datum shift matters here, since a NAD27 Alaskan position is over a hundred metres from its WGS 84 one. VINTAGE: the charts are a snapshot taken on 2026-05-02. NOAA reissues ENCs weekly for Notice to Mariners and this store does not follow them, so a cell here can be behind NOAA's current edition. The cell edition, update number and issue date that answered are in `source` for exactly that comparison. REFUSALS that cost nothing: a position with no charted depth area is `no_depth_area`, which means land, a drying area the chart gives no depth for, or a gap between cells, and it is refused rather than answered with null. A position off the charts is `outside_coverage`. A chart object that cannot be read is a 503, not a guess. For what is in the way rather than how deep it is, use GET /v1/marine/hazards, which returns charted rocks, obstructions and wrecks within a radius.",
"pricing": {
"amount": "0.005",
"currency": "USDC",
"network": "eip155:8453",
"scheme": "exact",
"maxAmount": "",
"minAmount": ""
},
"method": "GET",
"providerName": "",
"parameters": [
{
"group": "query",
"name": "lat",
"type": "number",
"description": "",
"example": 57.0405,
"enumValues": [],
"default": null,
"required": false
},
{
"group": "query",
"name": "lon",
"type": "number",
"description": "",
"example": -135.3421,
"enumValues": [],
"default": null,
"required": false
}
],
"serviceName": "geoprimitives",
"tags": [
"marine",
"depth",
"nautical",
"noaa-enc",
"alaska"
],
"quality": {
"l30DaysTotalCalls": "3",
"l30DaysUniquePayers": "1"
}
},
{
"url": "https://api.geoprimitives.dev/v1/marine/hazards",
"description": "Give it a latitude, a longitude and a radius in metres and it answers with every charted underwater rock, obstruction and wreck inside that circle, nearest first, each with its charted depth, its water level effect, its name where the chart names it, and the geodesic distance from your position. Parameters go in the query string: this is a GET, there is no body. NOT FOR NAVIGATION, stated first because it is a condition of using the source. Derived from NOAA Electronic Navigational Charts and redistributed; not an official NOAA ENC, and it does not meet federal chart carriage regulations under 33 CFR 164 or SOLAS chapter V. Every response repeats this under `disclaimer`. NOAA and NOAA ENC are registered trademarks of NOAA, which does not endorse this service. COVERAGE is the same 65 cells as GET /v1/marine/depth: bands 3 to 5 over Sitka Sound; Kasaan Bay and Clarence Strait; Metlakatla, Ketchikan and Revillagigedo Channel; Juneau, Gastineau Channel and Lynn Canal; and Cordova with eastern Prince William Sound. Not statewide and not the whole coast. The check reads the coordinate, so it happens after payment; a position off the charts is refused with outside_coverage, which does not settle. Every response repeats the coverage under `coverage`. WHAT AN EMPTY ANSWER MEANS, because this is the one that can hurt: an empty list means the chart shows nothing there, not that the water is clear. Read the zone of confidence from GET /v1/marine/depth for the same position before you read anything into a clean result. Zones C, D and U mean the area was never fully searched and NOAA says so; in zone D large depth anomalies may be expected. Only three S-57 classes are searched -- UWTROC, OBSTRN and WRECKS -- so a charted danger that is a buoy, a cable, a fish haven area, a spoil ground or a restricted area is not in this store and will not appear. WHICH CHART ANSWERS, and why it is not just one: the largest-scale cell covering each part of the search wins, applied per location rather than once for the whole circle. A harbour cell ends at its own edge and the water a kilometre outside it is charted by the approach cell, so a search that fixed on one chart would drop those hazards silently. `cellsSearched` lists every cell that contributed, and each hazard names the cell it came from. Where two cells chart the same rock at a seam it is returned once. DEPTHS are metres below Mean Lower Low Water. A null depth is a hazard the chart gives no sounding for; read `waterLevel` instead, since an object that covers and uncovers, or is awash, has no single depth. `chartedAsArea` marks a wreck or obstruction the chart carries as an area rather than a point, in which case the position given is its centroid and the thing itself is larger. LIMITS: the radius runs from 1 to 5000 metres and defaults to 1000. A search may read 100 chart tiles, counted from the geometry before any read, so an over-wide request is refused for free rather than part-served; the widest legal radius needs at most 81, so the radius is the limit you meet and the tile count is a backstop. At most 200 hazards come back, nearest first, and the response says when it truncated. VINTAGE is a 2026-05-02 snapshot of charts NOAA reissues weekly; the cell edition and issue date that answered are in each hazard's cell and in `source`. For how deep the water is rather than what is in it, use GET /v1/marine/depth.",
"pricing": {
"amount": "0.005",
"currency": "USDC",
"network": "eip155:8453",
"scheme": "exact",
"maxAmount": "",
"minAmount": ""
},
"method": "GET",
"providerName": "",
"parameters": [
{
"group": "query",
"name": "lat",
"type": "number",
"description": "",
"example": 57.0405,
"enumValues": [],
"default": null,
"required": false
},
{
"group": "query",
"name": "lon",
"type": "number",
"description": "",
"example": -135.3421,
"enumValues": [],
"default": null,
"required": false
},
{
"group": "query",
"name": "radiusMetres",
"type": "number",
"description": "",
"example": 250,
"enumValues": [],
"default": null,
"required": false
}
],
"serviceName": "geoprimitives",
"tags": [
"marine",
"hazards",
"wrecks",
"noaa-enc",
"alaska"
],
"quality": {
"l30DaysTotalCalls": "3",
"l30DaysUniquePayers": "1"
}
},
{
"url": "https://api.geoprimitives.dev/v1/pip",
"description": "Answer which of your points fall inside which of your polygons, in one call. Use it to tag events with the district, catchment or territory they belong to, to filter a point set down to a study area, or to join two datasets that share nothing but geography. The answer comes back twice, as a dense 0/1 matrix and as a per-point list of polygon indices, so neither shape needs rebuilding on your side, and polygon ids are echoed as column labels so the matrix joins straight back to your records. Rings must be closed and coordinates must be EPSG:4326: run POST /v1/geojson/repair first if they are not, and POST /v1/transform if they are not in degrees. Costs scale with polygon detail, not just polygon count, so POST /v1/geometry/simplify on the polygons is often the cheapest way under the caps. The matrix is dense: every point is tested against every polygon, one row per point and one column per polygon, 1 for inside and 0 for outside. A point exactly on an edge or vertex counts as inside unless \"ignoreBoundary\" is set. Two caps, both on products rather than on either side: at most 250000 point-polygon pairs, and at most 25000000 point-vertex tests, since a ray cast walks every edge. Input is EPSG:4326, longitude then latitude. Limits: 250000 points, 2000 polygons, 100000 polygon positions, and a 1 MiB request body.",
"pricing": {
"amount": "0.01",
"currency": "USDC",
"network": "eip155:8453",
"scheme": "exact",
"maxAmount": "",
"minAmount": ""
},
"method": "POST",
"providerName": "",
"parameters": [
{
"group": "body",
"name": "points",
"type": "array",
"description": "",
"example": [],
"enumValues": [],
"default": null,
"required": false
},
{
"group": "body",
"name": "polygons",
"type": "object",
"description": "",
"example": null,
"enumValues": [],
"default": null,
"required": false
}
],
"serviceName": "geoprimitives",
"tags": [
"pip",
"point-in-polygon",
"containment",
"matrix",
"wgs84"
],
"quality": {
"l30DaysTotalCalls": "1",
"l30DaysUniquePayers": "1"
}
},
{
"url": "https://api.geoprimitives.dev/v1/timezone",
"description": "Give it a longitude and latitude and it answers with the IANA timezone identifier (America/New_York), the UTC offset in force there right now, whether daylight saving is currently in effect and by how much, the local wall-clock time, and the zone's standard offset. Pass an optional ISO 8601 `at` instant to ask about any moment between 1900 and 2100 instead of now, which is what you want when scheduling something months out across a DST boundary. Parameters go in the query string: this is a GET, there is no body. Two halves, with different vintages, and the difference matters. The offset and the DST rules come from the runtime's own timezone database and are current: a rule change reaches this endpoint when Cloudflare updates ICU, with no redeploy here. The *boundaries* come from tz_world (efele.net), CC0 1.0 and derived from VMAP0, a public-domain US government product. That map was frozen at tzdb 2016d in May 2016, and this catalog sells public-domain and permissively-licensed data only, which is why it is used rather than the current OpenStreetMap-derived alternative. What the vintage costs, measured against IANA's own coordinates for all 312 zones in zone1970.tab rather than estimated: 293 agree. Of the 19 that do not, 7 are Antarctic research stations, which this map does not cover at all and which fall back to the open-water rule; 7 more return a different identifier for the identical offset and DST all year (Australia/Brisbane for Australia/Lindeman, America/Regina for America/Swift_Current, and five like them), so the time is right and only the name is old. That leaves four regions where the answer is up to one hour wrong because the zone was split after 2016: Chilean Magallanes and Aysen (answered as America/Santiago rather than America/Punta_Arenas or America/Coyhaique), the Mexican border municipalities around Ciudad Juarez, Saratov Oblast in Russia, and the weeks each spring when Asia/Hebron and Asia/Jerusalem change on different dates. If you query inside one of those, do not trust this endpoint. Boundary precision is a separate question from vintage. The map is indexed as compacted H3 cells at resolution 6, whose half-width is about 1.6 km, chosen to match VMAP0's own stated positional accuracy of roughly 2 km rather than to flatter the index. So a point within a couple of kilometres of a zone border may be attributed to the neighbour, and no amount of the source's precision would fix that because the source does not have it. Away from a border the answer is exact. Every answer says where it came from under `source`. `boundary` means the point is inside a mapped timezone polygon and is the answer you are paying for. `coastal` means it is over water or on land this map does not cover, and the identifier is the nearest zone within about 60 km -- correct for a harbour or a coastal city whose coordinate falls just offshore, and not a boundary result. `nautical` means open water more than about 60 km from mapped land, answered with the tz database's own rule for international waters, meridians 15 degrees apart; it is not the time kept aboard any vessel and it ignores territorial waters. Longitude and latitude are checked against EPSG:4326 bounds before anything else touches them, because H3 silently normalises an out-of-range latitude into a real but wrong cell rather than refusing it. Reproject with POST /v1/transform first if you are not already in WGS 84. The DST flag is decided on offsets rather than on the tz database's own isdst bit, so Europe/Dublin reads as daylight saving in summer like everywhere else, and it is measured over a window either side of the instant so that a zone which changes its standard offset mid-year -- Asia/Almaty in 2024 -- is not mistaken for one on daylight saving.",
"pricing": {
"amount": "0.005",
"currency": "USDC",
"network": "eip155:8453",
"scheme": "exact",
"maxAmount": "",
"minAmount": ""
},
"method": "GET",
"providerName": "",
"parameters": [
{
"group": "query",
"name": "at",
"type": "string",
"description": "",
"example": "2026-07-04T16:00:00Z",
"enumValues": [],
"default": null,
"required": false
},
{
"group": "query",
"name": "lat",
"type": "number",
"description": "",
"example": 40.7128,
"enumValues": [],
"default": null,
"required": false
},
{
"group": "query",
"name": "lon",
"type": "number",
"description": "",
"example": -74.006,
"enumValues": [],
"default": null,
"required": false
}
],
"serviceName": "geoprimitives",
"tags": [
"timezone",
"iana",
"dst",
"utc-offset",
"h3"
],
"quality": {
"l30DaysTotalCalls": "4",
"l30DaysUniquePayers": "1"
}
},
{
"url": "https://api.geoprimitives.dev/v1/transform",
"description": "Reproject coordinates between EPSG coordinate reference systems, with the datum shift applied. Accepts a bare position, an array of positions at any nesting depth, or a GeoJSON Feature, FeatureCollection or Geometry, and answers in whichever shape it was given, preserving properties, ids and foreign members. Covers 1380 codes: WGS 84, ITRF2020, NAD27 and every NAD83 realization as geographic, Web Mercator, all 120 WGS 84 UTM zones north and south, the NAD27 and NAD83 UTM zones, and the NAD27 and NAD83 State Plane and low-distortion zones, Oregon and Alaska included. Reach for it before calling /v1/geometry, /v1/h3/index, /v1/h3/polyfill or /v1/pip, which take EPSG:4326 only and refuse anything outside -180..180 and -90..90. One price per call whatever the payload: 10000 positions cost exactly what one position costs, so batch rather than sending a position at a time. Horizontal only: elevations pass through untouched. Datum shifts, and read this before buying: two mechanisms run between the two sides and the response names every step. Between NAD27 and the NAD83 realizations (NAD83, HARN, NSRS2007, 2011, PA11) the NOAA NGS NADCON 5.0 grids are applied, 2016-09-01 release, public domain, biquadratic interpolation as NGS specifies, covering the conterminous United States, Alaska, Hawaii and Puerto Rico with the US Virgin Islands; a position outside that coverage is refused with outside_datum_grid rather than answered without the shift. Between WGS 84 and NAD83(2011) or NAD83(PA11) the EPSG time-dependent Helmert transformations 10334 and 10336 are applied with WGS 84 read as ITRF2020, which the current WGS 84 (G2296) realizes to a few centimetres. That transformation depends on when the coordinates were observed, because the North American plate moves about 2 cm a year against ITRF2020: send \"epoch\" as a decimal year (2024.5) to say when, or it is evaluated at 2010.0, the NAD83(2011) reference epoch, and the response says which. Every response carries datumShiftApplied and a datumShift object listing the grids or Helmert that ran, the region, the epoch and the data provenance; when both sides share a datum it says so and applies nothing. Accuracy was measured against PROJ 9.5.1 running the same grids and the same EPSG operations: the projection arithmetic agrees to under 1e-8 m and the shifted result to under 1e-6 m, so the 1.29 m gap this route carried before the grids is closed to the limit of the source data, which NGS states as a few centimetres for the realization grids and decimetres for NAD27. If a grid cannot be read the request is answered 503 and not charged; it is never answered without the shift. A position that lands outside the target projection's valid domain is rejected with its index, never returned as a null or infinite coordinate. Limits: 10000 positions per call across the whole request, 512 datum-grid tiles per call (about one degree square per grid step; a state-sized batch is well under), and a 1 MiB request body.",
"pricing": {
"amount": "0.005",
"currency": "USDC",
"network": "eip155:8453",
"scheme": "exact",
"maxAmount": "",
"minAmount": ""
},
"method": "POST",
"providerName": "",
"parameters": [
{
"group": "body",
"name": "coordinates",
"type": "array",
"description": "",
"example": [],
"enumValues": [],
"default": null,
"required": false
},
{
"group": "body",
"name": "from",
"type": "string",
"description": "",
"example": "EPSG:4326",
"enumValues": [],
"default": null,
"required": false
},
{
"group": "body",
"name": "to",
"type": "string",
"description": "",
"example": "EPSG:3645",
"enumValues": [],
"default": null,
"required": false
}
],
"serviceName": "geoprimitives",
"tags": [
"transform",
"reprojection",
"datum",
"nadcon5",
"epsg"
],
"quality": {
"l30DaysTotalCalls": "1",
"l30DaysUniquePayers": "1"
}
},
{
"url": "https://api.geoprimitives.dev/v1/us/geographies",
"description": "Give it a longitude and latitude inside the United States and it answers with the Census FIPS codes for that point: the 2-digit state code, the 5-digit county GEOID, and the GEOID of the congressional district. Parameters go in the query string: this is a GET, there is no body. The codes are the product and the names are a courtesy. A county GEOID is the join key to essentially every geography-keyed federal dataset -- the American Community Survey, BLS Local Area Unemployment Statistics, BEA regional accounts, CDC WONDER, HUD, USDA, FEMA -- and the state code is its first two digits, so one call gives you both keys already in the form those tables expect. County GEOID is also the first five digits of a tract GEOID (11 digits) and of a block group GEOID (12), so it is the prefix you would filter a tract-level extract down with. COVERAGE, stated up front because the shortfall is the point: this endpoint answers state, county and congressional district, and it does NOT answer tract, block group, place or ZCTA. That is a size limit, not an oversight, and it was measured rather than guessed. A national compacted H3 index of tracts is 3196 KiB gzipped and of block groups 5335 KiB, at a resolution already too coarse for either; the whole Worker has 3 MiB compressed to spend and most of that was gone before this route existed. Those four tiers need an object store behind the Worker and will arrive with one. Every response repeats this under `coverage`, so an agent never has to infer it from a missing field. In the meantime geocoding.geo.census.gov answers all seven tiers, free, authoritative, at the cost of a round trip and a dependency. VINTAGE: TIGER/Line 2025, congressional districts for the 119th Congress. It is on every response under `vintage` and it matters more here than almost anywhere else in this catalog, because TIGER is re-cut annually and districts move with redistricting, so a stale district code is a wrong answer a caller cannot detect from the outside. Nothing here self-updates; a new vintage means a rebuild. The data is a work of the United States government and is in the public domain (17 U.S.C. 105) -- no attribution clause and no redistribution terms. ACCURACY, and this is the part to read before you trust a code. The boundaries themselves are full-resolution TIGER, the same geometry the Census Geocoder answers from, not a simplified copy. What is approximate is the lookup: the polygons are indexed as compacted H3 cells at resolution 7, about 1.22 km across, so a point within roughly 610 m of a boundary may be attributed to the neighbour. County equivalents and districts too small to describe at that resolution -- Falls Church city, the other Virginia independent cities, the congressional districts inside New York City -- carry a second index at resolution 9, about 87 m, and `boundaryAccuracyMetres` says which one answered you. Because a 610 m index is only useful if you know when not to trust it, every response carries `boundaryUncertain.county` and `boundaryUncertain.congressionalDistrict`. They are true when a neighbouring cell resolves to a different code, which means the boundary is close enough that the index cannot settle which side you are on. Measured against geocoding.geo.census.gov on 650 coordinates: on 400 population-weighted points the county code was right 389 times and the district code 383, and every one of the 11 and 17 disagreements was flagged; on 250 points drawn deliberately 100 m to 1500 m from a county line the county code was right 183 times, and every one of the 67 wrong county codes and 25 wrong district codes was flagged. Across all 650 it never returned a wrong code with the flag clear. It is generous in the other direction -- it flagged 74 of the 400 correct-heavy sample -- so treat `true` as \"verify this one if it matters\" rather than as \"this is wrong\". Outside the United States the answer is `found: false` with null codes and a paid 200, because \"this coordinate is not in the US\" is the answer you asked for. County polygons include their legal water area out to the three-mile limit and across the Great Lakes, so harbours, piers and lake coordinates resolve normally and only genuinely foreign or high-seas points fall out. Coverage is the 50 states, DC, Puerto Rico, American Samoa, Guam, the Northern Mariana Islands and the US Virgin Islands. A small amount of water is inside a county but inside no congressional district, which TIGER publishes as a real polygon; that answers with a district whose number is null rather than with a guess. Longitude and latitude are checked against EPSG:4326 bounds before anything else touches them, because H3 silently normalises an out-of-range latitude into a real but wrong cell rather than refusing it. Reproject with POST /v1/transform first if you are not already in WGS 84. TIGER publishes in NAD83, which this reads as WGS 84; the two differ by under two metres, which is a fortieth of the index's own resolution.",
"pricing": {
"amount": "0.01",
"currency": "USDC",
"network": "eip155:8453",
"scheme": "exact",
"maxAmount": "",
"minAmount": ""
},
"method": "GET",
"providerName": "",
"parameters": [
{
"group": "query",
"name": "lat",
"type": "number",
"description": "",
"example": 33.4484,
"enumValues": [],
"default": null,
"required": false
},
{
"group": "query",
"name": "lon",
"type": "number",
"description": "",
"example": -112.074,
"enumValues": [],
"default": null,
"required": false
}
],
"serviceName": "geoprimitives",
"tags": [
"census",
"fips",
"geoid",
"united-states",
"tiger"
],
"quality": {
"l30DaysTotalCalls": "3",
"l30DaysUniquePayers": "1"
}
}
],
"integrationType": "",
"isNew": false,
"priceSummary": {
"minAmount": "0.001",
"maxAmount": "0.01",
"avgCostPerTransaction": "0.007905",
"avgCostBasis": "exact",
"currency": "USDC"
},
"serviceName": "geoprimitives",
"tags": [
"elevation",
"dem",
"3dep",
"alaska",
"navd88",
"profile",
"geojson",
"repair",
"rfc7946",
"validation",
"geometry",
"polygon",
"area",
"measurement",
"wgs84",
"bbox",
"extent",
"buffer",
"turf",
"centroid",
"difference",
"overlay",
"intersect",
"simplify",
"douglas-peucker",
"union",
"h3",
"index",
"grid",
"hexagon",
"points",
"polyfill",
"test",
"x402",
"echo",
"marine",
"depth",
"nautical",
"noaa-enc",
"hazards",
"wrecks",
"pip",
"point-in-polygon",
"containment",
"matrix",
"timezone",
"iana",
"dst",
"utc-offset",
"transform",
"reprojection",
"datum",
"nadcon5",
"epsg",
"census",
"fips",
"geoid",
"united-states",
"tiger"
],
"iconUrl": ""
}
}