URL: /code/data/pages-and-sites/audit-site · Id: `page:/code/data/pages-and-sites/audit-site`

# Site.audit

> Crawl a whole site, yours or a rival’s, and rank every issue by how many pages it hits, each with an example and its fix.

`POST https://api.sb/{your-domain}/sites/audit`

Crawl a whole site, yours or a rival’s, and rank every issue by how many pages it hits, each with an example and its fix.

- **Price:** 1.5¢ per 100 pages
- **Success rate:** 100% of 90 calls
- **Freshness:** Fresh for 7 days
- **Emits:** `Site.audited`
- **Capability:** [Web](https://startupbuilder.dev/code/platform/web)

```text title="Run it · Ask"
Crawl leaseline.app and tell me which fix helps the most pages. Spend up to 5¢.
```

```ts title="Run it · SDK"
import { $ } from 'startup-builder'

const result = await $.try.Site.audit({ hostname: 'leaseline.app', maxPages: 200, maxCost: 0.05 })
```

```json title="Run it · Response"
{
  "hostname": "leaseline.app",
  "pages": 48,
  "complete": true,
  "issues": [
    {
      "id": "image-alt",
      "pages": 31,
      "example": "https://leaseline.app/rent-ledger-template",
      "fix": "Add alt text that says what each image shows."
    },
    {
      "id": "thin-content",
      "pages": 6,
      "example": "https://leaseline.app/tags/renewals",
      "fix": "Merge or remove tag pages under 200 words."
    },
    {
      "id": "orphan",
      "pages": 4,
      "example": "https://leaseline.app/guides/late-fees",
      "fix": "Link each page from at least 2 indexed pages."
    }
  ],
  "observedAt": "2026-10-27T14:02:11Z",
  "method": "sampled",
  "cost": {
    "usd": 0,
    "maxUsd": 0.0272,
    "units": 48,
    "misses": 0
  }
}
```

## Options and prices
| You get | Input that selects it | Price | Success rate |
| --- | --- | --- | --- |
| Start a crawl | `{"mapOnly":false}` | About 2¢ a crawl | 100% of 90 calls |
| Each page crawled and checked | `{"maxPages":500}` | About 1.5¢ per 100 pages | 100% of 90 calls |
| Every URL on a site, without checks | `{"mapOnly":true}` | About 0.015¢ a site map | 99.98% of 10,138 calls |

## Inputs
| Name | Type | Default | Meaning |
| --- | --- | --- | --- |
| `hostname` | [`Hostname`](/code/data/types#hostname) | Required | The site to crawl: yours or a rival’s. |
| `maxPages` | `number` | `500` | Pages to crawl, at most 10,000. The crawl stops here and says `complete: false`. |
| `mapOnly` | `boolean` | `false` | List every URL with its status and no checks. |
| `render` | `'none' \| 'js'` | `'none'` | Run each page’s scripts before checking it, for sites built in the browser. |
| `maxCost` | `number` | `0.50` | The most this crawl may spend, in USD. Over it, the call is refused before it spends, or stops early with what it has. |

## Outputs
| Field | Type | What `null` means |
| --- | --- | --- |
| `hostname` | [`Hostname`](/code/data/types#hostname) | Never null. |
| `pages` | `number`, in pages crawled | Never null. |
| `complete` | `boolean` | Never null: `false` when the crawl stopped at `maxPages` or `maxCost`. |
| `issues` | `{ id: string; pages: number; example: string; fix: string }[]` | Never null: most pages first; empty when every check passed. |
| `urls` | `{ url: string; status: number; score: number \| null }[]` | Present with `mapOnly`; `score: null` because a map runs no checks. |
| `(crawl)` | [`Observed`](/code/data/types#observed) | The audit carries `observedAt` and `method`. |
| `cost` | [`Cost`](/code/data/types#cost), in USD | Never null. In try mode `usd` is 0 and `maxUsd` is what the call would have cost. |

## Read the result
| Field | Value | What it means | What to do |
| --- | --- | --- | --- |
| `issues` | The first row | The fix that helps the most pages at once. | Fix it site-wide in the template, not page by page. (the site-audit order) |
| `issues` | `thin-content` on tag or location pages | Near-empty pages a search engine judges not worth indexing. | Merge or remove them; they drag the whole site’s crawl. (the index-state playbook) |
| `issues` | `orphan` | No page links to them, so crawlers find them late or never. | Link each from 2 indexed pages, then check them with `Domain.verify`. (the index-state playbook) |
| `complete` | `false` | The site is bigger than `maxPages`. | Run `mapOnly` first to count the URLs, then size `maxPages` to them. (the contract every data call keeps) |

## Effects
**Action class**

None: it reads, so no approval rule names it

**Modes**

- `try`: Crawls and returns the audit; nothing is saved and nothing is billed.
- `send`: Returns a pending Action at once; `Site.audited` carries the audit when the crawl ends.
- `do`: Waits for the crawl to end, which can take minutes, then returns the audit.

**Idempotency**

The same `Idempotency-Key` with the same input replays the first audit at $0; with other input it returns 409.

**Saved or shared**

The audit is kept with your Site for 7 days; audits of other sites are yours alone.

**Emits**

`Site.audited` with `{ hostname, pages, complete, issues }`

**Performed by**

[Analyst](https://startupbuilder.dev/agents/marketing/analyst)

**Price**

About 1.5¢ per 100 pages. Free: pages past `maxPages`, which are never crawled. `maxCost` defaults to 50¢. A 500-page crawl: 500 × 0.015¢ a page is about 7.5¢.

**Reliability**

The first route succeeds 100% of 90 calls. If it fails, the call refuses with `sources-busy` and nothing is spent.

**Freshness**

A reading younger than 7 days returns at $0. Readings about people are never shared across startups.

**Batch and pages**

Up to 10,000 pages a crawl. Every crawl: `send` returns a pending Action at once and `Site.audited` delivers the audit

**Rate**

[The shared rate rules](/code/data#rate): 10 calls a second per key, and an array input counts as one call.

## Refuses when
This call has no refusals of its own. Every data call can also meet [the shared data-call refusals](/code/refusals#data-calls): [`filter-unsupported`](/code/refusals#filter-unsupported), [`input-missing`](/code/refusals#input-missing), [`limit-exceeded`](/code/refusals#limit-exceeded), [`max-cost`](/code/refusals#max-cost), [`rate-limited`](/code/refusals#rate-limited), [`sources-busy`](/code/refusals#sources-busy), [`venue-unsupported`](/code/refusals#venue-unsupported).

## When it waits
| Fact | Value |
| --- | --- |
| `send` returns | A pending Action, `status: running`, with its `$id`. |
| Delivered by | `Site.audited` |

```ts
await $.Watch.create({
  selector: 'Site.audited',
  url: 'https://leaseline.app/hooks/audits',
})
```

## Example

**Ask**

```text
Crawl leaseline.app and tell me which fix helps the most pages. Spend up
to 5¢.
```

**API**

```bash
curl -X POST "https://api.sb/leaseline.app/sites/audit?mode=try" \
  -H "Authorization: Bearer $SB_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"hostname":"leaseline.app","maxPages":200,"maxCost":0.05}'
```

The response is the one in the Run it window above.

**CLI**

```bash
npx startup-builder do Site.audit --try --data '{"hostname":"leaseline.app","maxPages":200,"maxCost":0.05}'
```

**MCP**

```json
{
  "name": "do",
  "arguments": {
    "mode": "try",
    "code": "return await $.Site.audit({ hostname: 'leaseline.app', maxPages: 200, maxCost: 0.05 })"
  }
}
```

**SDK**

```ts
import { $ } from 'startup-builder'

const result = await $.try.Site.audit({
  hostname: 'leaseline.app',
  maxPages: 200,
  maxCost: 0.05,
})
```

To retry, send the same `Idempotency-Key`: the retry returns the first answer for $0. A new key reads again.

A `sk_test_` key answers these the same way every time, at $0 ([every test value](/code/data#test-values)).

| Test value | Always returns | What it is for |
| --- | --- | --- |
| `maxCost: 0.0001` | Refuses with [`max-cost`](/code/refusals#max-cost) | Any priced call with a maxCost of 0.0001 is refused before it spends: the price is above the cap. |
| `rate-limited.test` | Refuses with [`rate-limited`](/code/refusals#rate-limited) | Any call naming this hostname answers 429 with Retry-After, so your retry logic runs. |
| `sources-busy.test` | Refuses with [`sources-busy`](/code/refusals#sources-busy) | Any call naming this hostname answers 503 as if every route were busy, and nothing is spent. |

Watch it on a schedule: a Monitor runs this call and records an Event only when the reading changes.

```ts
await $.Monitor.create({
  verb: 'Site.audit',
  input: { hostname: 'leaseline.app', maxPages: 200 },
  change: 'new-issue',
  every: '1w',
})
```

## Used by
- **Jobs:** [Get indexed](/capabilities/get-indexed) · [Publish and post](/capabilities/publish-and-post)

## Related
[`Page.audit`](/code/data/pages-and-sites/audit-page)