# tools.yuanzhixiang.com - Domain APIs

Machine-readable usage notes for AI agents and scripts. Two JSON endpoints, no
authentication, no API key. Base URL: `https://tools.yuanzhixiang.com`.

Every response is an envelope:

```json
{ "ok": true,  "data": { ... } }
{ "ok": false, "error": { "code": "...", "message": "..." } }
```

Timestamps are milliseconds since the Unix epoch. Missing values are `null`,
never omitted and never `0`.

---

## 1. Domain availability - `GET /api/domain-check`

Checks whether domains are registered by querying the registry (RDAP / WHOIS).

**Request** (three equivalent forms; prefer the path form if your fetch tool
drops query strings)

- `GET /api/domain-check/acme-tools.com,acme-tools.net` (path; `/a.com/b.net` also works)
- `GET /api/domain-check?domains=acme-tools.com,acme-tools.net`
- `POST /api/domain-check` with body `{ "domains": ["acme-tools.com", "acme-tools.net"] }`
- Separate domains with commas, spaces or newlines. URLs and mixed case are
  accepted and normalised to a lowercase hostname.
- Pass exact domains. Keyword-to-domain expansion (`"acme tools"` -> `acme-tools.com`)
  is a page-only feature; do it yourself: lowercase, replace spaces with `-`,
  strip anything that is not `a-z0-9-`, then append `.tld`.
- Max **100** domains per request.

**Response** - `data.items[]`, same order as the input after de-duplication:

| field             | type           | meaning                                                            |
| ----------------- | -------------- | ------------------------------------------------------------------ |
| `domain`          | string         | normalised hostname                                                |
| `status`          | string         | `available` / `registered` / `unknown` / `unsupported` / `invalid` |
| `registrar`       | string \| null | only for `registered`                                              |
| `domainCreatedAt` | number \| null | only for `registered`                                              |
| `domainExpiresAt` | number \| null | only for `registered`                                              |
| `message`         | string \| null | explanation for `unknown`, `unsupported`, `invalid`                |

Status semantics:

- `available` - the registry confirmed there is no such domain. It is very
  likely purchasable (premium / reserved names are not detected).
- `registered` - a registration record exists.
- `unknown` - could not confirm either way (registry refused automated lookups
  or timed out). **Do not treat as available.** Re-query after ~1 minute;
  the backend hands failed lookups to a residential retry worker.
- `unsupported` - that TLD has no public RDAP/WHOIS service.
- `invalid` - not a valid registrable domain.

**Errors**: `400 DOMAIN_CHECK.EMPTY`, `400 DOMAIN_CHECK.TOO_MANY`,
`400 DOMAIN_CHECK.INVALID_BODY`.

**Example**

```bash
curl -s "https://tools.yuanzhixiang.com/api/domain-check/example.com,example-fresh-name.net"
```

---

## 2. Domain traffic - `GET /api/domain-traffic`

Estimated monthly visits, engagement and traffic-source mix for one domain,
sourced from Similarweb estimates. Up to the last 15 published months.

**Request**

- `GET /api/domain-traffic/example.com` (path form, preferred for agents) or
  `GET /api/domain-traffic?domain=example.com` - one domain per request.
- `www.` is stripped; subdomains are separate sites (`blog.example.com` is not
  merged into `example.com`).

**Response** - `data`:

| field             | type           | meaning                                                     |
| ----------------- | -------------- | ----------------------------------------------------------- |
| `domain`          | string         | normalised hostname                                         |
| `status`          | string         | `ready` / `pending` / `empty` / `failed`                    |
| `message`         | string \| null | human explanation for non-`ready` states                    |
| `workerOnline`    | boolean        | whether a collector is online (only matters when `pending`) |
| `domainCreatedAt` | number \| null | registration date from WHOIS, when known                    |
| `latestMonth`     | string \| null | `YYYY-MM` of the newest month with data                     |
| `latestVisits`    | number \| null | visits in that month                                        |
| `syncedAt`        | number \| null | when the data was last collected                            |
| `months[]`        | array          | ascending by month, see below                               |

Each entry of `months[]`:

| field                  | type           | meaning                                                   |
| ---------------------- | -------------- | --------------------------------------------------------- |
| `month`                | string         | `YYYY-MM`                                                 |
| `monthStart`           | number         | first day of the month, UTC                               |
| `visits`               | number \| null | estimated visits                                          |
| `bounceRate`           | number \| null | 0-1                                                       |
| `pagesPerVisit`        | number \| null |                                                           |
| `visitDurationSeconds` | number \| null |                                                           |
| `sources`              | object \| null | shares 0-1 per channel, keys below; `null` = not reported |

Source keys: `direct`, `searchOrganic`, `searchPaid`, `referrals`,
`socialOrganic`, `socialPaid`, `mail`, `genAi`, `affiliate`, `displayAds`.

**Status semantics and polling**

- `ready` - `months` is populated. Data is cached; a domain that already has
  the latest published month is not re-collected until the next month is out.
- `pending` - the domain was just queued for collection (first request, or a
  refresh is due). **Poll again every 4-5 seconds**, give up after about a
  minute. If `workerOnline` is `false` nothing will arrive soon; try later.
- `empty` - collected, but Similarweb has no data for this site (too small or
  unindexed).
- `failed` - the last collection failed; `message` has the reason. Retry later.

Requesting a domain has a side effect: it schedules collection if the stored
data is behind. Ask for domains you actually need.

**Errors**: `400 DOMAIN_TRAFFIC.INVALID_DOMAIN`, `503 DOMAIN_TRAFFIC.NOT_CONFIGURED`
(service misconfigured), `502 DOMAIN_TRAFFIC.UPSTREAM`.

**Example**

```bash
curl -s "https://tools.yuanzhixiang.com/api/domain-traffic/example.com"
```

---

## Suggested agent workflow

1. Brainstorm names -> expand to domains -> `POST /api/domain-check`.
2. Keep only `status == "available"`; re-check `unknown` once after a minute.
3. For competitor or reference sites, call `/api/domain-traffic` and poll while
   `pending`; compare `months[-1].visits` and the `sources` mix.

Human-facing pages for the same data: `/domain-check` and `/domain-traffic`.
