HTTP 402 · Payment Required
Open data, priced per request,
paid by the agent that wants it.
dataduct ingests public-domain and permissively-licensed datasets, curates them into its own store, and sells single records over x402. No account, no API key, no subscription — an agent asks, gets a price, pays, and is served.
We sell access and curation. The data stays open, and every licence obligation attached to it travels through to your response.
GET /v1/nvd/CVE-2024-12345
402 Payment Required
{ "x402Version": 1, "error": "X-PAYMENT header is required", "accepts": [{ "scheme": "exact", "network": "base", "maxAmountRequired": "100", "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", "payTo": "0x2096…287C", "resource": "https://…/v1/nvd/CVE-2024-12345", "maxTimeoutSeconds": 60, "extra": { "name": "USD Coin", "version": "2" } }]}
maxAmountRequired is atomic units, never a decimal.
0.0001 USDC → 100 at 6 decimals.
asset is the token contract address, not a symbol — it is what your client signs an EIP-3009 authorization against.
GET /v1/nvd/CVE-2024-12345
X-PAYMENT: eyJ4NDAyVmVyc2lvbiI6MSwic2NoZW1lIjoiZXhhY3QiLC…
200 OK
X-PAYMENT-RESPONSE: eyJzdWNjZXNzIjp0cnVlLCJ0cmFuc2FjdGlvbiI6…
What this is
dataduct ingests open datasets — public-domain and permissively-licensed — into its
own store, and sells individual records to whoever asks. Payment happens in the
request itself over x402: no account, no API key, no contract,
no minimum. An agent that has never seen this service before can discover a price
and complete a purchase in two HTTP round trips.
We are not selling the data. The data is open and you can go and get it yourself.
We are selling access and curation: one cleaned, deduplicated, licence-checked
copy behind a uniform interface, so you don't write and maintain 24 different
ingest pipelines against 24 providers who each change their formats without notice.
What you can rely on
You are never charged for a response you didn't get. Settlement runs only after
the handler produces a 2xx. A 404, a 400 or a 500 costs you nothing, and a
capability this deployment doesn't offer returns 501 *before* any payment is
requested.
A failed settlement gets you nothing. If your payment doesn't settle, you get
402 and no data — never a record you didn't pay for, and never a receipt for a
payment that didn't happen.
Retries don't double-charge. Present the same authorization for the same
resource and the original receipt is replayed rather than a second settlement
attempted. If your connection drops after paying, just retry.
Licence obligations travel with the data. Records that can't prove their licence
never reach the store, and a record from a CC-BY or OGL source carries the exact
attribution string you must pass downstream. That is a real obligation on you, not
a courtesy field.
What this isn't, yet
This deployment is not open for business. Pricing is placeholder, no payment
facilitator has been selected, and the analytical query tier is switched off. The
catalogue below shows exactly what is and isn't available rather than what is
planned — see Availability for the specifics.
Catalogue
Every dataset, its licence tier, what it costs, and whether this deployment will
actually sell it. A source that is not enabled returns 501 before any
payment is requested — you are never charged to discover that something is unavailable.
| id | dataset | licence | endpoints | lookup price | ingest |
|---|---|---|---|---|---|
| mitre_cve | MITRE CVE ListAuthoritative CVE identifier dictionary | CC-BYattr | lookup | 0.0001 | verified earlier |
| nvd | NVD (NIST)CVE records, CVSS scores, CPE platform enumeration - canonical vuln DB | PD | lookup | 0.0001 | verified earlier |
| usaspending | USAspending.govEvery federal contract, grant, and award; bulk export by agency, award type, fiscal year (back to FY2008) | PD | lookupquery | 0.001 | verified earlier |
| bls_comp | BLS NCS / ECI / ECECBenefits participation, employer costs, compensation-cost trends | PD | lookupquery | 0.0002 | verified live |
| bls_oews | BLS OEWSWage & employment data for 800+ occupations by state and metro area | PD | lookupquery | 0.0002 | verified live |
| cap | Caselaw Access Project (Harvard)6.7M US federal & state cases spanning ~360 years | PD | lookupsearch | 0.0001 | verified live |
| census_cps | Census / CPSHousehold income, demographics, labor-force statistics | PD | lookupquery | 0.0002 | blockedconnector still fetches the HTML catalog page. Target is https://api.census.gov/data/{year}/cps/asec/mar but api.census. |
| cfpb_ccdb | CFPB Consumer Complaint Database13.8M+ complaints vs financial companies: company responses, product type, issue narratives | PD | lookupsearchquery | 0.0001 | never verified |
| cfpb_hmda | CFPB HMDA (mortgage) dataHome Mortgage Disclosure Act lending data | PD | lookupquery | 0.0002 | verified live |
| circl_cve | CIRCL CVE feedDaily CVE/CPE JSON dumps | PD | lookup | 0.0001 | never verified |
| courtlistener | CourtListener (Free Law Project)Every US court opinion, dockets, oral arguments, judge bios, financial disclosures | CC0 | lookupsearch | 0.0002 | never verified |
| cuad | CUAD (Atticus Project)Expert-annotated commercial contract clauses & deal points | CC-BYattr | lookupsearch | 0.002 | verified live |
| cvefixes | CVEfixes12K+ vuln-fixing commits with before/after code across 4,249 OSS projects | MIXED | lookupquery | 0.0002 | blockedconnector still fetches the HTML record page. The JSON resolver is https://zenodo.org/api/records/4476563, but the only |
| cwe | CWE (MITRE)Common Weakness Enumeration taxonomy | CC-BYattr | lookup | 0.0001 | verified live |
| data_europa | data.europa.eu (EU Data Portal)Catalog aggregating tens of thousands of EU / member-state datasets | MIXED | lookupsearch | 0.001 | verified live |
| data_gov | data.govCatalog aggregating tens of thousands of US government datasets | MIXED | lookupsearch | 0.001 | never verified |
| fdic_ffiec | FDIC / FFIEC Call ReportsUS bank financial-condition & regulatory call-report data | PD | lookupquery | 0.0002 | never verified |
| fred | FRED (St. Louis Fed)800K+ economic time series aggregated from many providers | MIXED | lookupquery | 0.0002 | blockedendpoint requires FRED_API_KEY (400 "Variable api_key is not set" without one) and cannot enumerate series. No |
| govinfo | govinfo / eCFR / US CodeFederal legislation, CFR regulations, congressional records, US Code | PD | lookupsearch | 0.0002 | verified live |
| legislation_uk | legislation.gov.ukUK Acts of Parliament, statutory instruments, devolved legislation | OGLattr | lookup | 0.001 | verified live |
| onet | O*NET (US Dept of Labor)1,000+ occupations: skills, tasks, tools, knowledge, work context, education | CC-BYattr | lookupsearch | 0.001 | verified live |
| sam_fpds | SAM.gov Contracting / FPDSContract opportunities and award records | PD | lookupquery | 0.001 | never verified |
| sec_edgar | SEC EDGARAll US public-company filings since 1993: 10-K, 10-Q, 8-K, DEF 14A proxy, Forms 3/4/5 insider trades, XBRL fin | PD | lookupquery | 0.001 | verified earlier |
| world_bank | World Bank Open DataGlobal economic & development indicators, thousands of time series | CC-BYattr | lookupquery | 0.001 | verified live |
Prices are in the settlement asset's units, per request. enabled endpoints are live; greyed ones are registered in code but gated off on this deployment. Machine-readable: catalog.json.
The payment flow
Two round trips. The first tells you the price; the second pays it.
1 — Ask, and get a price
Any paid endpoint without an X-PAYMENT header answers 402 with an
x402 v1 challenge:
{
"x402Version": 1,
"error": "X-PAYMENT header is required",
"accepts": [{
"scheme": "exact",
"network": "base",
"maxAmountRequired": "100",
"asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"payTo": "0x209693Bc6afc0C5328bA36FaF03C514EF312287C",
"resource": "https://api.example.com/v1/nvd/CVE-2024-12345",
"description": "nvd record CVE-2024-12345",
"mimeType": "application/json",
"maxTimeoutSeconds": 60,
"extra": { "name": "USD Coin", "version": "2" }
}]
}
Three fields are worth reading carefully, because they are where integrations go
wrong:
maxAmountRequiredis in atomic units. USDC has 6 decimals, so a price of
0.0001 USDC is "100". It is a string because a uint256 does not fit in a
double. Do not parse it as a number.
assetis the token contract address, not a symbol. It is what your client
signs against.
extracarries the EIP-712 domain for that token on that chain. It differs
per network — USDC on Base mainnet is "USD Coin", on Base Sepolia it is
"USDC" — and a wrong value produces a signature the facilitator rejects.
2 — Pay, and be served
Sign an EIP-3009 transferWithAuthorization, wrap it in a PaymentPayload,
base64 it, and resend the identical request with an X-PAYMENT header. The
payload decodes to:
{
"x402Version": 1,
"scheme": "exact",
"network": "base",
"payload": {
"signature": "0x…",
"authorization": {
"from": "0xYourWallet",
"to": "0x209693Bc6afc0C5328bA36FaF03C514EF312287C",
"value": "100",
"validAfter": "1753500000",
"validBefore": "1753503600",
"nonce": "0x…32 bytes…"
}
}
}
On success you get 200, the record, and an X-PAYMENT-RESPONSE header — base64
of the settlement result:
{ "success": true, "transaction": "0x…", "network": "base", "payer": "0xYourWallet" }
What the server checks before it charges you
The facilitator is treated as a signature and settlement oracle, not as the
authorization decision. Before any facilitator call, the request is rejected
locally if:
| Condition | Response |
|---|---|
X-PAYMENT is not base64 JSON, or is structurally invalid | 402 — X-PAYMENT is not base64-encoded JSON |
scheme or network differ from the challenge | 402 — scheme "…" does not match required "exact" |
authorization.to is not the quoted payTo | 402 — authorization.to is not the required payTo address |
authorization.value is below the quote | 402 — authorization.value N is below the required M atomic units |
| the authorization window does not cover now | 402 — authorization has expired / is not yet valid |
Overpayment is accepted. Paying someone else and presenting the receipt here is
not.
Ordering, and why it matters to you
The handler runs first; settlement is the last thing before the body is released.
That single ordering is what produces the guarantees:
- Non-2xx is never settled. If the record isn't in our store you get
404and
are not charged.
- A failed settlement discards the body. You get
402and no data. - Verification moves no funds, so a payment that verifies but cannot settle
costs you nothing.
Idempotency and replay
Payments are keyed on the EIP-3009 nonce.
- Same nonce, same resource — treated as a retry. The stored receipt is
replayed, no second settlement is attempted, and the response carries
X-Payment-Idempotent-Replay: true. Safe to retry after a dropped connection.
- Same nonce, different resource — refused with
402. One authorization buys
one resource.
This is defence in depth rather than the primary control: under the exact scheme
the nonce is consumed on-chain at settlement, so the chain itself rejects a
genuine double-spend.
Status codes
Read these precisely — several mean "not your fault".
| Code | Meaning |
|---|---|
200 | Paid and served. |
402 | Payment required, invalid, or not settled. The body is a fresh challenge you can pay. |
404 | The record is not in our store. Not charged. We never proxy a provider, so a 404 means we don't have it, not that it doesn't exist. |
429 | Rate limited. Retry after the Retry-After interval. |
501 | This deployment does not offer that capability or source. Nothing was charged and no price was quoted. |
503 | Our payment provider or configuration is broken. Not your fault; retry later. |
The 402/503 split is deliberate: an outage on our side must not look like a
routine payment challenge.
API reference
Base path /v1. Everything under it is payment-gated; everything outside it is free.
GET / — catalogue
Free. Returns every source with its licence tier, registered capabilities, price
bands and whether this deployment will sell it.
{
"service": "dataduct",
"x402Version": 1,
"sourceCount": 24,
"enabledCount": 3,
"sources": [{
"id": "nvd",
"license_tier": "PD",
"enabled": true,
"capabilities": { "lookup": true, "search": false, "query": false, "attribution": false },
"price": { "lookup": "0.0001", "search": "0.001", "query": "0.002" }
}]
}
capabilities reflects this deployment, not the code: a capability the connector
registers but which is gated off reads false. Prefer
catalog.json if you are consuming this programmatically — it is
the same data plus resource-id formats and blocked reasons.
GET /readyz — readiness
Free. 200 when the deployment can safely take payment; 503 otherwise, with the
failing check named. **A 402 from a paid route does not imply the payment path
works** — the challenge is emitted before configuration is validated. This is the
endpoint that answers that question.
GET /v1/{source}/{resource} — point lookup
Returns one record. The {resource} format differs per source and is the single
thing most worth checking before you integrate — see the resourceIdFormat field
in catalog.json.
{
"sourceId": "nvd",
"recordId": "CVE-2024-12345",
"license": "PD",
"fetchedAt": "2026-07-20T06:00:00.000Z",
"data": { }
}
attribution is present only for CC-BY and OGL sources, and when present it is an
obligation you must carry downstream.
POST /v1/{source}/search — semantic search
{ "query": "privilege escalation in container runtimes", "topK": 10 }
query is required; an empty or missing one is 400. topK is clamped to the
source's configured maximum (25 unless the connector sets otherwise) — values
above it are silently reduced rather than rejected. The response carries
{ source, query, count, results[] }, each result shaped like a lookup response.
POST /v1/{source}/query — analytical
Returns 501 on this deployment, before any payment challenge. No analytical
engine is bound. Twelve connectors register the capability, and it is gated off
for all of them rather than accepting payment for a query it cannot run.
When an engine is bound, request bodies are whitelisted per source — never raw
SQL. Five connectors (usaspending, cvefixes, fdic_ffiec, fred,
sam_fpds) explicitly reject a sql key with 400. Results are clamped to
1,000 rows / 1 MiB by default, tightened per source, and a clamped response sets
truncated: true.
Headers
| Header | Direction | Notes |
|---|---|---|
X-PAYMENT | request | base64 PaymentPayload. |
X-PAYMENT-RESPONSE | response | base64 SettlementResponse, on settled requests. |
X-Payment-Idempotent-Replay | response | true when a retry replayed a stored receipt. |
X-Request-Id | both | Echoed if you send one, generated otherwise. Quote it in any support request. |
Cache-Control | response | private, no-store on paid bodies. Do not cache them in a shared cache. |
CORS is open, and X-PAYMENT-RESPONSE / X-Request-Id are exposed, so a
browser-based agent can both send payment and read the receipt.
Errors
Every error body is JSON with an error field. Those that represent a fault on
our side also carry requestId — that is what reconciliation and support are keyed
on, so log it.
error | Status | Meaning |
|---|---|---|
payment_required (as an x402 challenge body) | 402 | Pay and retry. |
unknown_source | 404 | Not a source we carry. |
not_found | 404 | We don't hold that record. Not charged. |
not_implemented | 501 | Capability or source unavailable here. Not charged. |
bad_request | 400 | Malformed body; reason says what. |
rate_limited | 429 | Retry after Retry-After. |
service_unavailable | 503 | Our payment provider or config. Includes requestId. |
internal_error | 500 | Unhandled. Includes requestId. Not charged. |
Integrate
Try it now, for free
The catalogue, readiness and the 402 challenge all work without paying anything.
# What's available, and what does it cost?
curl -s https://<host>/ | jq '{enabledCount, sources: [.sources[]|select(.enabled)]|map({id,price})}'
# Can this deployment safely take money?
curl -s https://<host>/readyz | jq
# Ask for a record without paying — this is the price quote.
curl -i https://<host>/v1/nvd/CVE-2024-12345
That last call returns 402 with the challenge. Nothing is charged, and you can
call it as often as the rate limit allows to discover pricing.
Build the payment header
The challenge is x402 v1, so the standard clients (x402-fetch, x402-axios)
apply. If you are constructing it yourself, the header is base64 of the payload —
sign the authorization with your wallet, then:
// Build X-PAYMENT from a signed EIP-3009 authorization.
function paymentHeader({ signature, authorization, network = 'base' }) {
const payload = { x402Version: 1, scheme: 'exact', network, payload: { signature, authorization } };
const bytes = new TextEncoder().encode(JSON.stringify(payload));
let binary = '';
for (const b of bytes) binary += String.fromCharCode(b);
return btoa(binary);
}
// Read the receipt back off a served response.
function settlement(res) {
const h = res.headers.get('X-PAYMENT-RESPONSE');
if (!h) return null;
const binary = atob(h);
const bytes = Uint8Array.from(binary, (c) => c.charCodeAt(0));
return JSON.parse(new TextDecoder().decode(bytes));
}
Take value, to, network and the EIP-712 domain straight from the challenge's
accepts[0] — do not hardcode them. maxAmountRequired is already in atomic
units; pass it through as authorization.value unchanged.
Retry safely
If a request fails after you sent payment, resend it **with the same nonce and the
same URL**. You will get the original receipt back and will not be charged twice.
Changing either the nonce or the resource makes it a new purchase.
Carry attribution downstream
Six sources are CC-BY or OGL. Their responses include an attribution string, and
passing it on is a licence condition — not a formatting nicety.
{
"sourceId": "onet",
"recordId": "15-1252.00",
"license": "CC-BY",
"attribution": "This product incorporates data from O*NET, developed by the U.S. Department of Labor/Employment and Training Administration (USDOL/ETA), used under the CC BY 4.0 license.",
"data": { }
}
The guarantee runs the other way too: a record that cannot prove its licence is
dropped before it ever reaches the store, and a CC-BY record that somehow lost its
attribution fails at the serving boundary rather than being served bare. So an
absent attribution on a CC-BY source is a bug worth reporting, not an optional
field.
Rate limits
120 requests per minute per IP on /v1/*. Over the limit returns 429 with
Retry-After: 60. The free catalogue and /readyz are not limited.
Before you build against this
Read Availability. Three sources are currently purchasable, the
analytical tier is off, prices are placeholder, and no payment facilitator is
configured — so the paid path cannot complete end to end today. The free surface,
the challenge format and the record shapes are stable enough to build against.
Availability
This page is generated from the repository, so the numbers here are the running
configuration rather than an intention.
Three of 24 sources are purchasable on this deployment (nvd, mitre_cve,
usaspending). The rest return 501 before any payment challenge. A source is
added to that list only after its ingest has actually been run — not when its
connector compiles.
Ingest verification is reported in four states, because collapsing them would
overstate the position:
- verified live — the provider endpoint was fetched and its bytes inspected on
2026-07-26.
- verified earlier — a live path was recorded before that pass but not
re-confirmed in it. All three currently-sellable sources are in this state.
- never verified — the connector has never been run against its live provider.
- blocked — cannot ingest, with the reason stated in the catalogue.
The analytical /query tier is off. No engine is bound, so it returns 501
rather than charging for a query it cannot run.
Pricing is placeholder. The bands shown are not commercial prices. One on-chain
settlement occurs per paid request, so a price below the facilitator's fee would be
loss-making; real numbers depend on a facilitator decision that has not been made.
No payment facilitator is configured. Until one is, the paid path cannot
complete: you will see a correct 402 challenge, and /readyz will tell you the
deployment is not ready to take money.
Three sources are blocked outright. cvefixes is blocked for a structural reason
worth stating plainly: its only distribution is a 12.6 GB gzip inside a 12.7 GB
ZIP, and gzip has no random access, so it cannot be ingested in a Workers runtime
at all. fred and census_cps need provider API keys.