URL: /capabilities/earn-first-links/listing-check · Id: `page:/capabilities/earn-first-links/listing-check`

# Listing check

> Every week, each live listing read back: still up, still linking to you, and still followed, with what changed since you submitted it.

Every week, each live listing read back: still up, still linking to you, and still followed, with what changed since you submitted it.

```text title="Run it · Ask"
Check that all my directory listings are still up and followed, spend up to 50¢, and show me the plan first.
```

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

const result = await $.try.Post.check({ goal: 'Check that all my directory listings are still up and followed, spend up to 50¢, and show me the plan first.', budget: { amount: 0.5, currency: 'USD' } })
```

```json title="Run it · Response"
{
  "mode": "try",
  "stored": false,
  "estimate": "About 0.8¢ per 40 listings",
  "sample": {
    "listing": "SaaSHub",
    "field": "rel",
    "was": "follow",
    "now": "nofollow",
    "since": "2026-11-02"
  }
}
```

- **Kind:** Workflow
- **Price:** 0.8¢ per 40 listings
- **Your time:** 2 minutes
- **Approval:** Runs on its own
- **Deepens:** [`process:launches`](https://startupbuilder.dev/workflows/processes/launches)

## What you get
- **40 listings** · Every week, each live listing read back: still up, still linking to you, and still followed, with what changed since you submitted it. · [`noun:post`](https://startupbuilder.dev/business/nouns/post)
- `checked` · Listings read this week.
- `live` · Listings up and linking to you.
- `drifted[]` · Each change: the link’s `rel` (follow to nofollow), the URL, the name, or `gone`, with what it was and what it is now.

**How long:** About 30 seconds.

## Trigger
It runs weekly, and daily from Directory submissions until each new listing is live.

```ts
every.week(($) => $.Post.check({ posts: 'listings' }))
```

## Steps
Your agent does these steps, one row per step; each step’s check is part of its row and its price.

| Step | Verb | Returns | Price | Approval |
| --- | --- | --- | --- | --- |
| 1. List the live listings | `Collection.query` | Each listing’s URL and the fields you submitted | Free | Runs on its own |
| 2. Read each listing page | [`Page.audit`](/code/data/pages-and-sites/audit-page) | The live page: your link, its `rel`, the name and the description | About 0.8¢ | Runs on its own |
| 3. Compare with what you submitted | `Collection.query` | A drift row for each changed field; a listing missing two weeks running becomes `rejected` | Free | Runs on its own |
| 4. Record each drift | `Post.update` | The post updated with the change, whose Event any Trigger can act on | Free | Runs on its own |

Calls: `Collection.query`, then `Page.audit`, then `Collection.query`, then `Post.update`.

About 0.8¢ per 40 listings, each time it runs, capped at 10¢.

## Example: give it a goal
> Check that all my directory listings are still up and followed, spend up to 50¢, and show me the plan first.

The plan it shows runs 4 steps, priced before it spends anything; Run it above shows the result it returns.

## When something fails

| When | Then |
| --- | --- |
| A listing page cannot be read | It is retried the next day, not marked gone; one bad read never reports a lost link. |
| A listing is missing two weeks running | It becomes `rejected` with a task to resubmit. |
| You call it twice in one day | The second call returns the stored reading at $0: it is idempotent per post and day. |

Each step retries three times, then falls through to the next route; one item's failure lands in `skipped` with its reason, never failing the call. Before a step would cross `maxCost` it stops and returns what it has, with a `resume` link. Each step's idempotency key is the call's key plus the step and the item: `leaseline-post-check-1-<step>-<item>`.

## Change it
| Ask | Compiles to |
| --- | --- |
| Resubmit any listing that has been gone for two weeks. | [`Post.submit`](/capabilities/earn-first-links/directory-submissions) |
| Tell me the day a followed link turns nofollow. | [`Monitor.create`](/code/data/monitors/create) |
| Check my launch posts too, not only directories. | [`Post.check`](/capabilities/earn-first-links/listing-check) |

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

export default workflow({
  name: 'listing-drift-check',
  verb: 'Post.check',
  input: { posts: "'listings' | string[]" },
  trigger: { schedule: 'weekly' },
  maxCost: 0.1,
  execute: async (ctx, { posts = 'listings' }) => {
    const list = await ctx.step('list', () => $.Collection.query({ from: 'post', where: posts === 'listings' ? "kind = 'listing' and status = 'published'" : `$id in ${posts}` }))
    const read = await ctx.step('read', () => $.Page.audit({ urls: list.map((p) => p.url), checks: ['links'], maxCost: 0.02 }))
    const drifted = diffListings(list, read)                                  // the fields you submitted: a declared rule
    if (drifted.length) await ctx.step('record', () => $.Post.update({ changes: drifted }))
    return { checked: list.length, live: read.filter((r) => r.linksTo).length, drifted }
  },
})
```

Extend it with `extends: 'Post.check'` and `base()`, or [compose your own](/code/compose) from these steps.

## Works with
**Plays that order it:** [Agent registries](/plays/agent-registries), [Directory listings](/plays/directory-listings), [Marketplace listings](/plays/marketplace-listings).

It serves [Earn first links](/capabilities/earn-first-links), and runs in [Launch and search](/strategy/launch-and-search), [Borrowed](/strategy/borrowed). It deepens [`process:launches`](https://startupbuilder.dev/workflows/processes/launches).

## For agents

| Fact | Value |
| --- | --- |
| Guide | `Post.check` |
| Guide approval | `limits` |
| Goal | Check that all my directory listings are still up and followed, spend up to 50¢, and show me the plan first. It may change counts, the order of steps, soft filters and the route a read takes, and it stops early when the goal is met. It never relaxes your words, raises its own budget, answers its own approval, calls a Play or acts outside your standing approval. |
| Trigger | `every.week(($) => $.Post.check({ posts: 'listings' }))` |
| Retry | Three times with backoff, then the next route; the same idempotency key never charges, sends or posts twice |
| Idempotency key | `leaseline-post-check-1` |
| Lookups | A `find:` input asks unless exactly one record fits |

| Step | Action class | Seat rule that would hold it |
| --- | --- | --- |
| 1. List the live listings | None | None. |
| 2. Read each listing page | None | None. |
| 3. Compare with what you submitted | None | None. |
| 4. Record each drift | None | None. |

The amounts live once, on the hub ([approvals](https://startupbuilder.dev/agents/approvals)).

| Step | Function | Doer | Checker | Assurance | Inputs |
| --- | --- | --- | --- | --- | --- |
| 1. List the live listings | `code` | Code | Code checks each result against its declared type | `measured` | `$0.posts` |
| 2. Read each listing page | `code` | Code | Code checks each result against its declared type | `measured` | `$1.urls` |
| 3. Compare with what you submitted | `code` | Code | Code checks each result against its declared type | `measured` | `$1, $2` |
| 4. Record each drift | `code` | Code | Code reads the result back from where it landed | `measured` | `$3.drifted` |

The doer is never the checker, and the weakest assurance on the path is the whole call’s.

## Under the hood

| Kind | What it touches |
| --- | --- |
| Systems | [Social](/code/apps/social) |
| Nouns | [`noun:collection`](https://startupbuilder.dev/business/nouns/collection), [`noun:page`](https://startupbuilder.dev/business/nouns/page), [`noun:post`](https://startupbuilder.dev/business/nouns/post) |
| Events | `Collection.queried`, `Post.updated` |

## Reference

| Name | Type | Default | Meaning |
| --- | --- | --- | --- |
| `posts` | `'listings' \| string[]` | `'listings'` | Every live listing, or the posts you name. |
| `second` | `boolean` | `true` | Read a page that looks gone a second time, on another route, before reporting it. |
| `maxCost` | `number` | `0.10` | The most this call spends each time it runs, in USD. It stops before a step would cross it and returns what it has, with a `resume` link. |

Emits `Post.checked`. Refuses with [`max-cost`](/code/refusals#max-cost) before it spends, and with the Data calls and Accounts groups ([refusals](/code/refusals)).