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: 402 with the x402 PAYMENT-REQUIRED header. Retry the same request with a PAYMENT-SIGNATURE header.
  • Paid: 200 {"status":"verified","proof":{…}} and a PAYMENT-RESPONSE header. 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 as record_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 with require_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:

  1. Cryptographically valid? The signature verifies with a key from /.well-known/domain-name-proof-key.json.
  2. Within its validity period? now < valid_until.
  3. Fresh and bound enough for you? Domain, method, audience, nonce and max_age_seconds. Remember: a proof shows control at verified_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.

  1. Generate a random nonce for this onboarding session. Tell the seller: “Prove control of seller.example via domainnameproof.online with audience marketplace.example and nonce N.” For humans, link to https://domainnameproof.online/verify?domain=seller.example&audience=marketplace.example&nonce=N.
  2. The seller creates the verification, publishes the challenge, verifies and pays.
  3. The seller hands you the proof JSON (or its proof_url).
  4. You validate it offline with the cached JWK Set: expected domain, audience, nonce, and a max_age_seconds that 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.

CodeMeaning
domain_expected_not_urlA URL was sent; `suggested_domain` shows the host to send instead.
ip_address_not_allowed / port_not_allowed / credentials_not_allowedOnly bare domain names are accepted.
domain_invalid / invalid_punycodeMalformed labels, lengths or punycode.
domain_not_public / unknown_tld / public_suffix_not_allowedSpecial-use, private, unknown or public-suffix names.
domain_not_foundNXDOMAIN 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_mismatchRecord absent, or present without an exact match (see `hints`).
dns_propagation_incompleteSome authoritative nameservers do not serve the record yet.
dns_unreachable / dns_resolution_failedNameservers did not answer, or resolution failed (incl. DNSSEC).
dns_nameserver_not_publicThe zone’s nameservers resolve only to non-public addresses.
dns_cname_loop / dns_response_too_largeCNAME chain > 3 hops or looping; TXT set > 100 records / 16 KiB.
http_status_not_ok / http_content_mismatchNot HTTP 200, or body not exactly the challenge.
http_redirect_* / http_too_many_redirectsRedirect off-site, downgrade, other port/protocol, or > 3.
http_address_not_publicHost resolves to a private, loopback, link-local or metadata address.
http_response_too_large / http_content_encoding_not_allowedBody > 1 KiB, or compressed.
http_timeout / http_connection_failed / http_tls_errorServer 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.