Documentation
Domain Name Proof issues signed, machine-verifiable proofs that control of a domain was demonstrated. REST/JSON, no account, pay per proof with x402. Machine-readable: OpenAPI 3.1, llms.txt, service descriptor.
What a proof means
Control of the specified domain was demonstrated using the specified verification method at verified_at. This is not evidence of legal ownership, registrant identity, authorization beyond the verification surface, or trustworthiness, and control may have changed since.
It does not prove: legal ownership of the domain; identity of the registrant, person or company; authorization beyond control of the verification surface (DNS zone or web server); legitimacy, reputation or trustworthiness of the domain; that the same party still controls the domain after verified_at; control of any other name: the apex, subdomains and www are separate domains.
Where "domain ownership" verification is requested, this service provides technical CONTROL verification (DNS or web server control), not legal ownership.
Quick start
1. Create a challenge (free)
curl -s https://domainnameproof.online/api/v1/verifications \
-H 'content-type: application/json' \
-d '{"domain":"example.com","method":"dns_txt"}'Response 201: verification_id, access_key (secret, shown once), challenge.value, instructions, verify_url, expires_at. Optional fields: audience, nonce (copied into the signed proof), proof_validity_seconds (3600–2592000, default 604800), preferred_output_language (en, es, pt, fr, de, ru, zh, ja).
2. Publish the challenge
DNS: a TXT record whose full value equals the challenge.
_domain-name-proof.example.com. 300 IN TXT "domain-name-proof=dnproof_<token>"
HTTP: serve the challenge value as the body of https://example.com/.well-known/domain-name-proof/<challenge_id>.
3. Verify
curl -s -X POST "$VERIFY_URL" -H "authorization: Bearer $ACCESS_KEY"
- Not found yet:
200{"status":"verification_pending","check":{"code":"dns_txt_not_found",…}}— free; retry later. - Found:
402with the x402PAYMENT-REQUIREDheader. Retry the same request with aPAYMENT-SIGNATUREheader. - Paid:
200{"status":"verified","proof":{…}}and aPAYMENT-RESPONSEheader. Calling verify again returns the same proof without a new charge.
Status without side effects: GET /api/v1/verifications/{id} with the same bearer key. A full agent script is at /examples/agent-flow.ts.
Verification methods
dns_txt
- Record name
_domain-name-proof.<domain>. In most DNS panels the host is_domain-name-proof(apex) or_domain-name-proof.sub(subdomain); the API returns it asrecord_host_relative_to_registrable_domain. - The zone and its nameservers are discovered through DNSSEC-validating DNS-over-HTTPS resolvers; the TXT record is then read directly from up to 4 authoritative nameservers, so resolver caches never delay or fake a result.
- Every authoritative server that answers must serve a TXT record whose complete value (multi-string records are joined) equals the challenge exactly. Other TXT records are ignored. Substrings, case changes and quotes never match.
- A CNAME at the record name is followed up to 3 hops (delegated challenges).
http_well_known
GET https://<domain>/.well-known/domain-name-proof/<challenge_id>must return HTTP 200 with the challenge value as the body (trailing whitespace ignored), at most 1 KiB, without Content-Encoding.- HTTPS with a valid certificate is tried first. Plain HTTP is used only if the HTTPS connection fails; the proof then says
"scheme":"http". A plain-HTTP result is weaker (an on-path network attacker could forge it); relying parties can refuse it withrequire_https. - Up to 3 redirects, only to the same host or its
www/apex twin, never HTTPS→HTTP, never other ports or protocols.
Scope is always the exact domain: proving example.com says nothing about www.example.com or any other subdomain, and vice versa.
Payment
0.019 USDC per issued proof, x402 v2 scheme exact, network eip155:8453, facilitator Coinbase CDP. The price is charged only in the request that issues the proof, after a fresh successful check. See Pricing.
Proof format (version 1)
{
"proof_version": "1",
"proof_type": "domain_control",
"proof_id": "prf_…",
"issuer": "domainnameproof.online",
"issuer_url": "https://domainnameproof.online",
"operator": "Active Life Hub LLC",
"domain": "xn--e1afmkfd.xn--p1ai", // canonical ASCII: compare this
"domain_unicode": "пример.рф",
"original_domain": "Пример.РФ", // exactly as submitted
"scope": "exact_domain",
"verification_method": "dns_txt",
"challenge_id": "chl_…",
"audience": "marketplace.example", // or null
"nonce": "7f3a…", // or null
"verified_at": "2026-09-29T14:02:11Z",
"valid_until": "2026-10-06T14:02:11Z",
"statement": "Control of the specified domain was demonstrated …",
"evidence": { "record_name": "…", "expected_value": "…", "match": "exact",
"nameservers": [ … ], "observed_at": "…" },
"key_id": "<RFC 7638 thumbprint>",
"signature_algorithm": "Ed25519",
"canonicalization": "JCS-RFC8785",
"signature": "<base64url>"
}The signature is Ed25519 (RFC 8032) over the UTF-8 bytes of the RFC 8785 canonical JSON of the proof object with the signature member removed. Changing any other member — domain, timestamps, method, evidence, issuer, audience, nonce — invalidates it. Proofs contain only strings, integers, booleans, null, arrays and objects.
Validation
Three separate questions; a good relying party asks all three:
- Cryptographically valid? The signature verifies with a key from /.well-known/domain-name-proof-key.json.
- Within its validity period?
now < valid_until. - Fresh and bound enough for you? Domain, method, audience, nonce and
max_age_seconds. Remember: a proof shows control atverified_at, not now.
Offline, no dependencies: verify-proof.mjs (Node.js 18+) · verify_proof.py (Python, cryptography).
node verify-proof.mjs proof.json --domain example.com \ --audience marketplace.example --nonce "$NONCE" --max-age 3600
Or use the stateless convenience endpoint (no database, no account):
curl -s https://domainnameproof.online/api/v1/proofs/validate -H 'content-type: application/json' \
-d '{"proof":{…},"expected_domain":"example.com","max_age_seconds":86400}'
→ {"valid":true,"cryptographically_valid":true,"within_validity_period":true,
"requirements_met":true,"errors":[],…}Relying-party integration
Example: a marketplace, referral platform or SaaS onboarding flow needs to know that a seller agent controls seller.example. It never has to integrate with any registrar.
- Generate a random nonce for this onboarding session. Tell the seller: “Prove control of
seller.examplevia domainnameproof.online with audiencemarketplace.exampleand nonceN.” For humans, link tohttps://domainnameproof.online/verify?domain=seller.example&audience=marketplace.example&nonce=N. - The seller creates the verification, publishes the challenge, verifies and pays.
- The seller hands you the proof JSON (or its proof_url).
- You validate it offline with the cached JWK Set: expected domain, audience, nonce, and a
max_age_secondsthat fits your risk.
import { validateProof } from './verify-proof.mjs';
const jwks = await fetch('https://domainnameproof.online/.well-known/domain-name-proof-key.json').then(r => r.json());
const r = validateProof(proof, { keys: jwks.keys, expected: {
domain: 'seller.example', audience: 'marketplace.example', nonce, maxAge: 3600 } });
if (!r.valid) throw new Error(r.errors.join(','));from verify_proof import validate_proof result = validate_proof(proof, jwks["keys"], "seller.example", "marketplace.example", nonce) assert result["valid"], result["errors"]
A proof may be retrieved again at GET /api/v1/proofs/{proof_id}: the proof ID works as a bearer link, so share it only with parties that should see the proof.
Error codes
Errors are {"error":{"code","message",…}}. Branch on code; messages are English and may change.
| Code | Meaning |
|---|---|
domain_expected_not_url | A URL was sent; `suggested_domain` shows the host to send instead. |
ip_address_not_allowed / port_not_allowed / credentials_not_allowed | Only bare domain names are accepted. |
domain_invalid / invalid_punycode | Malformed labels, lengths or punycode. |
domain_not_public / unknown_tld / public_suffix_not_allowed | Special-use, private, unknown or public-suffix names. |
domain_not_found | NXDOMAIN at creation or check time. |
unauthorized (401) | Missing or wrong access key. |
verification_not_found / proof_not_found (404) | Unknown, forged or mistyped ID. |
challenge_expired (410) | Challenges live 48 h; create a new one. |
dns_txt_not_found / dns_txt_mismatch | Record absent, or present without an exact match (see `hints`). |
dns_propagation_incomplete | Some authoritative nameservers do not serve the record yet. |
dns_unreachable / dns_resolution_failed | Nameservers did not answer, or resolution failed (incl. DNSSEC). |
dns_nameserver_not_public | The zone’s nameservers resolve only to non-public addresses. |
dns_cname_loop / dns_response_too_large | CNAME chain > 3 hops or looping; TXT set > 100 records / 16 KiB. |
http_status_not_ok / http_content_mismatch | Not HTTP 200, or body not exactly the challenge. |
http_redirect_* / http_too_many_redirects | Redirect off-site, downgrade, other port/protocol, or > 3. |
http_address_not_public | Host resolves to a private, loopback, link-local or metadata address. |
http_response_too_large / http_content_encoding_not_allowed | Body > 1 KiB, or compressed. |
http_timeout / http_connection_failed / http_tls_error | Server unreachable or too slow. |
invalid_payment / payment_wrong_* / payment_expired (402) | Payment header does not match the requirements. |
payment_reused / verification_in_progress / settlement_pending_reconciliation (409) | Replay or concurrency protection. |
settlement_unknown (503) | Do not pay again; retry with the same header or contact the operator. |
not_configured (503) | The service fails closed when payment or signing is unavailable. |
rate_limited (429) | Slow down; see Retry-After. |
Security model
- Domains are canonicalized once (UTS 46 non-transitional, NFC, lowercase A-labels, one trailing dot removed); every security decision uses the canonical form. Mixed-script and Latin look-alike labels are flagged in
domain.warnings. - Challenges carry 160-bit tokens derived with HMAC from an authenticated-encrypted, server-sealed verification ID bound to domain, method, expiry and nonce. They cannot be guessed, transplanted to another domain or method, or used after expiry.
- HTTP checks resolve once, refuse any non-public address (loopback, RFC 1918, CGNAT, link-local and cloud metadata, IPv6 ULA, mapped and translated forms), and connect to the pinned address, defeating DNS rebinding. Every redirect is re-validated.
- DNS checks never send packets to non-public nameserver addresses, use random query IDs and ports, reject mismatched answers and fall back to TCP for truncated responses. We do not validate DNSSEC on the authoritative answers themselves; delegation is discovered through validating resolvers.
- Payments are checked locally (network, asset, amount, recipient, time window) before the facilitator is called; one EIP-3009 authorization can pay for at most one proof; one challenge yields at most one charged proof.
Limitations
- Control can change after verification; no continuous monitoring is performed.
- Checks run from one network vantage point. A plain-HTTP result, or an attacker able to intercept traffic to the domain’s nameservers or web server, weakens a proof.
- Proofs are not revocable; rely on valid_until and your own max age.
- Human-readable text is English; API codes are language-independent.