# Pull new competitor ads on a schedule

Check your competitors for newly found paid ads on a timer, read only what is new since the last run, and transcribe the video ads.

A scheduled job, a cron script or an agent on a timer can ask PeekPanda one question every run: which ads did you find for these apps since I last looked? The loop below reads stored observations only. It never asks PeekPanda to collect new ads, so it is cheap and safe to repeat.

## What you need

- An API key from `/account`, or an MCP client that is signed in. See [API keys](/docs/api-keys).
- The App Store ids of the apps you follow, up to 25 per request.
- One place to remember a single value between runs: the newest `discoveredAt` you have read.

## The loop

1. Read the value you stored last time. On the very first run there is none, so leave `discoveredAfter` out.
2. Call `GET /v1/paid-ads` with `storeIds`, `sort=recently_discovered` and, when you have one, `discoveredAfter` set to the stored value.
3. Follow `nextCursor` while `hasMore` is true. Keep every other filter and the sort unchanged between pages.
4. Process the new `items`. Each one is an ad PeekPanda first recorded after your stored instant.
5. Store the newest `discoveredAt` from this run. Only replace the stored value once the run has finished, so a failed run is simply read again next time.

```bash
curl --get https://api.peekpanda.com/v1/paid-ads \
  -H "Authorization: Bearer $PEEKPANDA_API_KEY" \
  --data-urlencode 'storeIds=6480417616,1234567890' \
  --data-urlencode 'sort=recently_discovered' \
  --data-urlencode 'discoveredAfter=2026-10-05T06:00:00Z' \
  --data-urlencode 'limit=40'
```

`discoveredAfter` is strict: an ad recorded at exactly the stored instant is not returned again. `discoveredAt` is when PeekPanda first recorded the ad, not when the advertiser started running it. Use `startedAt` for that, where the source discloses it.

`storeIds` returns only ads verified to link to those apps. To follow one advertiser instead, pass `advertiserExternalId` with its exact page id.

## The same loop through MCP

An agent makes the same read with the `get_ads` tool and the `search` action. The query takes the same names as the HTTP request.

```json
{
  "name": "get_ads",
  "arguments": {
    "request": {
      "action": "search",
      "query": {
        "storeIds": "6480417616,1234567890",
        "sort": "recently_discovered",
        "discoveredAfter": "2026-10-05T06:00:00Z",
        "limit": 40
      }
    }
  }
}
```

## Transcribing the video ads

Only video ads have speech to transcribe. For each new item whose `creative.format` is `video`, call `POST /v1/paid-ads/{platform}/{archiveId}/transcript`, taking the platform and archive id from the item. Through MCP, use the `refresh` tool with the `ad_transcript` action and pass `platform` and `archiveId` in `params`.

```bash
curl -X POST https://api.peekpanda.com/v1/paid-ads/meta/1234567890123456/transcript \
  -H "Authorization: Bearer $PEEKPANDA_API_KEY"
```

- A transcript that is already ready comes back from storage, so asking twice is safe.
- `409` means a transcript for that ad is being generated right now. Leave it and try again on the next run.
- `422` means the ad has no stored video, for example an image-only ad. Do not retry it.

Transcribing uses the `content:write` scope. The search itself only needs `context:read`.

## Fitting it in your daily allowance

Every API request and every MCP call counts toward your plan's daily allowance, which resets at 00:00 UTC. Your plan's figure is on `/account` and in [Usage and limits](/docs/usage-and-limits). Budget one run like this:

calls per run = pages read + video ads transcribed

calls per day = runs per day × calls per run

Keep calls per day under your allowance with room for the rest of your work. Two ways to stay inside it: run less often, since the loop never misses an ad between runs, and transcribe only the video ads you will actually read. If you do run out, the API answers `429` with `daily_call_limit` until the reset.
