---
name: satsdns
description: Host DNS for a domain you control (DNSSEC-signed, record API), run a secondary for your own primary, get a dynamic-DNS hostname under dynkite.com, or register a .com/.net/.org domain for a registrant you act for, with SatsDNS (https://api.satsdns.com), paying in sats over Lightning (L402), Cashu ecash, or on-chain with a Nostr account. Use when you need authoritative DNS, ACME DNS-01 records, a hostname that follows a changing IP address, or a domain name, with no email signup for DNS.
---

# SatsDNS: DNS hosting paid in sats

Base URL: `https://api.satsdns.com`. Nameservers: `ns1.satsdns.com`, `ns2.satsdns.com`. Dynamic-DNS domain:
`dynkite.com`. Full reference: https://satsdns.com/docs . OpenAPI: https://satsdns.com/openapi.yaml . Prices,
nameservers and limits as JSON: https://api.satsdns.com/v1/info (free).

## When to use it

- A domain needs authoritative DNS you can change by API, with DNSSEC and TTLs down to 60 seconds.
- You need a stable name (`myhost.dynkite.com`) for a machine whose address changes, or a TXT record for ACME DNS-01.
- Someone runs their own primary DNS server and wants two more nameservers.
- Someone you act for wants a `.com`, `.net` or `.org` domain registered, and has given you their real contact details
  and the authority to accept the registration agreement for them (see "Registering a domain" below).

Do not use it for a domain you do not control, to register a domain without the registrant's authority or with invented
contact details, or for anything in the acceptable use policy (https://satsdns.com/acceptable-use): phishing and malware
names are refused or removed.

## Spending

Each order costs real money. Spend only within the budget your user gave you. If you have no budget, ask before paying.
Check that the amount in the 402 or invoice matches the price below before you pay.

| Order | Route (POST) | 30 days | 1 year |
|---|---|---|---|
| Hosted zone | `/v1/zones/create/{30d,1y}` | 500 | 5,000 |
| Renew a zone (hosted or secondary) | `/v1/zones/renew/{30d,1y}` | 500 | 5,000 |
| Secondary zone | `/v1/secondary/create/{30d,1y}` | 500 | 5,000 |
| Dynamic-DNS hostname | `/v1/ddns/create/{30d,1y}` | 200 | 2,000 |
| Renew a hostname | `/v1/ddns/renew/{30d,1y}` | 200 | 2,000 |
| 1,000,000 extra queries this UTC month | `/v1/zones/queries/1m` | 200 | |

Management calls (records, updates, status) are free. Cashu tokens from coinos or macadamia must also cover the mint's
swap fee (make them with "include fees"). Lightning routing fees are paid by your wallet.

## Step 1: for a zone, get its nameservers and delegate first

Each zone gets its own pair of nameservers under `ns1.satsdns.com` and `ns2.satsdns.com`. POST the create body without
payment, `{"zone": "example.org"}`: the free check answers `409 delegation_missing` with `detail.expected_ns` (the
pair, for example `k7qd.ns1.satsdns.com.` and `m3x9.ns2.satsdns.com.`), `detail.ns_ticket` (a secret, shown once; keep
it) and `detail.ns_offer_expires_at` (48 hours; 7 days for an account's pair). The domain must be registered first:
for a name its registry does not know the answer has no `expected_ns` (`detail.registered` false). At the domain's registrar, set exactly that pair as the nameservers
(others may stay listed). Send the order again with `{"zone": "example.org", "ns_ticket": "nst_..."}`: it is refused
with `409 delegation_missing` or `delegation_mismatch` until the registry shows both names (`detail.found_ns` says
what it sees). These checks are free and make no invoice. Without the ticket you get another pair (at most two per
network and zone). Hostnames need no delegation. With an account, the pair belongs to the account and no ticket is
needed. After deleting a zone, remove its nameservers at the registrar; the name stays reserved for its last owner
for 90 days (send the old zone token as `"previous_token"` to host it again on the same names).

## Step 2: ask, and get the 402

```
curl -i -X POST https://api.satsdns.com/v1/zones/create/30d \
  -H "Content-Type: application/json" -d '{"zone": "example.org", "ns_ticket": "nst_..."}'
```

- Any 4xx other than 402 means the order would fail: fix it (see the error `code` and `message`), nothing was charged.
- `402` carries `WWW-Authenticate: L402 macaroon="...", invoice="lnbc..."` and `X-Cashu: creqA...`.

## Step 3: pay one way

a. **L402:** pay the invoice, keep the preimage (hex), repeat the identical request with
   `Authorization: L402 <macaroon>:<preimage>`. Valid 1 hour, one request.

b. **Cashu:** make a token for the amount from one of the listed mints (at coinos or macadamia, include the fee) and
   repeat the identical request with `X-Cashu: cashuB...`. A short or refused token gets `400` and `X-Cashu-Error` and is
   not spent.

## Step 4: keep the token

`201` returns the status and `"token": "jdz_..."` (zones) or `"jdh_..."` (hostnames). It is shown once and cannot be
recovered. Store it where your user wants secrets kept. For zones, the answer's `dnssec.ds` is the DS record to give the
registrar to turn on DNSSEC validation (optional; also at `GET /v1/zones/{zone}/dnssec`).

If the paid request is refused after payment (`422`) or fails (`503`): with L402 the same header works once more within
the hour (`X-L402-Retry: allowed`); with Cashu the answer has a refund token in `X-Cashu-Refund`. Do not pay again.
If a paid zone create times out, repeat the identical request with the same L402 header within 15 minutes: you get
the zone back with a new token.

## Step 5: manage

Zone records (`Authorization: Bearer jdz_...`):

```
curl -X POST https://api.satsdns.com/v1/zones/example.org/records \
  -H "Authorization: Bearer jdz_..." -H "Content-Type: application/json" \
  -d '{"name": "www", "type": "A", "ttl": 300, "records": ["203.0.113.10"]}'
curl -H "Authorization: Bearer jdz_..." https://api.satsdns.com/v1/zones/example.org/records
curl -X DELETE -H "Authorization: Bearer jdz_..." https://api.satsdns.com/v1/zones/example.org/records/www/A
```

- `POST` replaces that name and type. `PATCH /v1/zones/{zone}/records` with `{"rrsets": [...]}` changes up to 100 sets
  at once (`"changetype": "REPLACE"` or `"DELETE"`).
- Names: `@` for the apex, `www`, or a full name. Content in zone-file form; names in content are absolute.
- TTL 60 to 86,400. At most 1,000 records and 1,000 record writes a UTC day per zone (`429 write_quota_exceeded`).

Dynamic DNS (`Authorization: Bearer jdh_...`):

```
curl -X POST https://api.satsdns.com/v1/ddns/myhost/update \
  -H "Authorization: Bearer jdh_..." -H "Content-Type: application/json" \
  -d '{"a": "auto"}'
curl -X POST https://api.satsdns.com/v1/ddns/myhost/update \
  -H "Authorization: Bearer jdh_..." -H "Content-Type: application/json" \
  -d '{"acme_txt": "<challenge value>"}'
```

Fields `a`, `aaaa`, `txt`, `https`, `acme_txt`; a value or list replaces, `null` deletes, absent fields stay; `"auto"` is
the caller's address. Only public addresses: private, loopback, link-local, CGNAT, unique-local, multicast and reserved
ones get `400 address_not_public` (dyndns2: `dnserr`). Routers and ddclient: `GET /nic/update?hostname=myhost&myip=1.2.3.4` with Basic auth (any user,
the token as password). HTTPS only: plain http gets `badauth` (401) and no redirect; a client that sent its token over
http should switch to https and rotate the token.

## Step 6: renew before it ends

Read `days_left` from `GET /v1/zones/{zone}` or `GET /v1/ddns/{hostname}`. Nothing renews by itself and no reminder is
sent to token-only zones. Renew with the paid route (`/v1/zones/renew/30d`, no token needed) or create a Lightning
address with `POST /v1/zones/{zone}/lnurl` that anyone can pay (500 sats = 30 days). After the end there are 7 days of
grace (still answering), then the zone is removed and its name released.

## Optional: a Nostr account

If you hold a Nostr key, sign requests with NIP-98 (`Authorization: Nostr <base64 kind-27235 event>`, u tag = the exact
URL, method tag, payload tag = sha256 of the body on non-GET, created_at within 60 s, each event once). Then:

- `POST /v1/account/orders {"product": "zone_create", "period": "1y", "zone": "example.org", "pay": "lightning"}`
  returns an invoice (Lightning valid 15 minutes; `"pay": "onchain"` for orders of at least 5,000 sats, exact amount,
  24 hours, counted as paid at 3 confirmations). Poll `GET /v1/invoices/{id}` until `state` is `fulfilled`; `result` has the token once.
- `"pay": "check"` runs the free checks only (`204` = would succeed).
- `POST /v1/account/claim {"zone": "...", "token": "jdz_..."}` puts a token-bought zone in the account.
- Accounts get renewal reminders by Nostr direct message.

## Registering a domain

Needs a Nostr account (NIP-98). Read https://satsdns.com/registration-agreement first; the registrant (the person or
organization you act for) accepts it, and you confirm that with `"attestation": true`, which means: "I am authorised by
the named registrant to register this domain for them, and the registrant accepts the registration agreement." Do not
send it unless that is true. (A person can do the same steps in the dashboard at https://app.satsdns.com, under
Register domain and Domains.)

1. `GET /v1/domains/check?domain=example.com&years=1` (free): availability and the price in sats. List prices: `.com`
   18,000, `.net` 20,000, `.org` 13,500 (renew 17,500); +3,000 with `"host_zone": true` (a year of hosting here).
2. `POST /v1/account/orders` with `"product": "domain_register"`, `"period": "1y"`, `"domain"`, `"registrant"`
   (`name`, `email`, `phone` as `+CC.NUMBER`, `address1`, `city`, `postal_code`, `country`; optional `organization`,
   `address2`, `state`), `"attestation": true`, and `"nameservers"` (`"ours"` or a list). Use `"pay": "check"` first:
   `200` with the quote, no invoice. Real data only: placeholders are refused and wrong data can get the domain
   suspended.
3. Pay the invoice (check the amount against the quote and your budget). Poll `GET /v1/invoices/{id}`: when
   `fulfilled`, `result` has the domain token (`jdd_...`, once). Keep it secret: it is as strong as the owner's key
   (nameservers, unlock, registrant), except that the transfer code of an account-owned domain needs the owner's
   NIP-98 signature.
4. Tell the registrant to click the registrar's confirmation email within 15 days, or the domain is suspended (not
   refunded). Watch `registrant_verification` on `GET /v1/domains/{domain}`.
5. New names stay on our nameservers for about 3 hours; then your nameservers are set. Nothing renews by itself: renew
   with `"product": "domain_renew"` before `expires_at` (possible until `renew_until`).

## Errors worth knowing

| Code | Meaning | What to do |
|---|---|---|
| `delegation_missing`, `delegation_mismatch` | Registry does not list both of the zone's nameservers (`detail.expected_ns`) | Set that pair at the registrar, wait for the registry, retry with the `ns_ticket` |
| `ns_ticket_invalid`, `ns_offer_limit` | Ticket unknown or expired; too many new pairs asked | Use the ticket from the first answer; otherwise wait |
| `zone_reserved`, `domain_owner_only` | Name held for the last owner of a removed zone, or registered here by someone else | Last owner: send `previous_token` or use the same account; otherwise pick another name |
| `zone_exists`, `zone_clash`, `hostname_taken` | Name already hosted | Pick another name |
| `name_blocked`, `hostname_refused`, `reserved_name` | Refused name | Pick another name; do not try variants of a brand |
| `too_far_ahead` | Already paid over 1,125 days ahead | Do not renew now |
| `rate_limited`, `account_rate_limited`, `*_quota_exceeded` | Too many requests or changes | Wait `Retry-After` seconds |
| `dns_backend_unavailable`, `lightning_unavailable` | Our side is down | Retry after `Retry-After`; check https://api.satsdns.com/healthz |
