# tamradar-agent.md

**👤 For humans**  Give this to your agent and it will set up TAMradar monitoring for you — create radars, check balance, retrieve findings.  **Give this link to your agent:** <a href="https://tamradar.readme.io/reference/tamradar-agentmd.md" target="_blank">https://tamradar.readme.io/reference/tamradar-agentmd.md</a>

You set up TAMradar monitoring for a user. Read this file top to bottom before starting.

**Base URL:** `https://api.tamradar.com` **Auth header:** `x-api-key: {TAMRADAR_API_KEY}` — required on every request. No Bearer prefix.

***

## Step 0 — Bootstrap

Do this before anything else — do not ask what they want to monitor yet.

1. Check `$TAMRADAR_API_KEY` in env. If unset, ask the user for it.
2. Ask: *"I'll create a `tamradar/` folder to store your config, radars, and findings. Should I go ahead?"*
3. On confirmation:

```bash
mkdir -p ./tamradar/updates ./tamradar/inputs
```

4. Write `./tamradar/.env`:

```
TAMRADAR_API_KEY=<key>
```

5. Append `tamradar/.env` to `.gitignore` if one exists.
6. Confirm to user:

```
tamradar/ created.
  tamradar/.env          ← API key
  tamradar/inputs/       ← drop target lists here
  tamradar/updates/      ← findings (local mode)
  tamradar/tamradar-radars.jsonl  ← created after first radar
```

Then ask:

> "How do you want to receive updates?
>
> **A) Webhook (recommended)** — TAMradar pushes findings instantly to a URL you provide. Works with Clay, Zapier, n8n, Make, or any webhook tool. Use [webhook.site](https://webhook.site) to inspect payloads quickly — but note: 100-request cap, and webhook URL cannot be changed after creation (must DELETE + recreate).
>
> **B) Local files** — I poll every hour and save findings to `tamradar/updates/`. No server needed."

* If **A**: collect webhook URL, include as `webhook_url` on every create call.
* If **B**: omit `webhook_url`. After radars are created, set up the polling schedule (see Phase 4).

***

**Base URL:** `https://api.tamradar.com` **Auth header:** `x-api-key: {TAMRADAR_API_KEY}` — required on every request. No Bearer prefix.

***

## Authentication

Check the environment for `TAMRADAR_API_KEY`. If the variable is unset or empty, ask the user: *"I need your TAMradar API key to proceed. You can find it in your account settings at tamradar.com."*

Never guess or construct the key. Do not proceed without it.

**Canonical request format — use this exact pattern for every call:**

```bash
curl -s -X POST "https://api.tamradar.com/v1/radars/companies" \
  -H "x-api-key: $TAMRADAR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "domain": "openai.com", "radar_type": "company_job_openings" }'
```

`x-api-key` is the auth header. `Content-Type: application/json` is required on all POST requests.

***

## How this works — overview

```
1. PRE-FLIGHT     GET  /v1/account              → check balance, block if $0
2. CREATE         POST /v1/radars/companies      → company signals
                  POST /v1/radars/contacts       → person signals
                  POST /v1/radars/industry       → industry-wide signals
                  POST /v1/radars/bulk           → >10 targets: 100 per request
3. RETRIEVE       GET  /v1/updates               → poll for findings (always available)
                  [webhook]                      → push delivery (faster, if webhook_url set)
4. MANAGE         DELETE /v1/radars/:id          → stop monitoring a target
```

Detailed breakdown follows.

***

## Inputs — collect before starting

| Input              | Required  | Notes                                                                                   |
| ------------------ | --------- | --------------------------------------------------------------------------------------- |
| `TAMRADAR_API_KEY` | MUST have | UUID format — check env first, ask if unset                                             |
| Target list        | MUST have | Companies (domain), people (name + LinkedIn URL + company domain), or industry keywords |
| `webhook_url`      | Optional  | Omit for poll-only. Add it for real-time push delivery.                                 |

### Webhook URL — default to polling

`webhook_url` is optional. **Default to polling** via `GET /v1/updates` unless the user explicitly provides a webhook URL.

**If no webhook URL provided:** omit `webhook_url` entirely — the radar will be poll-only. Retrieve findings via `GET /v1/updates`. No need to ask.

**If user provides a real webhook URL** (Make.com, Clay, Zapier, n8n): include it. Real-time delivery will work automatically.

⚠️ Once set, `webhook_url` cannot be changed without deactivating and recreating the radar. Avoid ephemeral test URLs like `webhook.site` for production radars.

***

## Phase 1: Pre-flight — ALWAYS run first

```
GET /v1/account
```

**Sample response:**

```json
{
  "status": "success",
  "code": 200,
  "data": {
    "balance_remaining_usd": 42.50,
    "account": {
      "active_radars": 12,
      "inactive_radars": 2,
      "failed_radars": 0,
      "total_radars": 14,
      "total_updates_found": 3847
    },
    "usage": {
      "month": "2026-05",
      "radars_activated": 3,
      "radars_deactivated": 0,
      "radars_failed": 0,
      "balance_used_usd": 7.50,
      "updates_found": 142
    }
  }
}
```

**Balance gate — enforce strictly:**

* IF `data.balance_remaining_usd == 0` → **STOP.** Tell user: *"Your TAMradar balance is $0. Add funds at tamradar.com before I can create any radars."*
* IF `data.balance_remaining_usd < 5` → **WARN.** Tell user the balance, ask if they want to continue.
* IF `data.balance_remaining_usd >= 5` → Proceed. Report balance to user.

### Recommend: balance monitoring cron (to user only — do not auto-install)

Tell the user:

> *"To avoid radars being deactivated unexpectedly, I recommend setting up a daily job that calls `GET /v1/account` and alerts you if `balance_remaining_usd` drops below a threshold (e.g. $10). TAMradar auto-deactivates radars when balance hits $0 and sends a `radar_failure` webhook — but proactive monitoring is better than reacting after the fact."*

Only suggest this. Do not install cron jobs, crontab entries, or scheduled tasks without explicit instruction.

***

## Phase 2: Create radars

### Idempotency — check local store first

Before every POST, look up `(domain, radar_type)` in `./tamradar/tamradar-radars.jsonl` (your local mapping store). If a matching entry exists, skip the POST and use the stored `radar_id`. This avoids wasteful requests and eliminates a whole class of 409 errors.

### Singles vs bulk

* **≤10 targets** → create one by one (single POSTs). Simple, easy to debug.
* **>10 targets** → use `POST /v1/radars/bulk`, 100 per request.

### Single radar creation

**`custom_fields` recommendation:** always pass your internal IDs here. They echo back in every finding, making it easy to route data back to your CRM, database, or workflow.

```json
"custom_fields": {
  "crm_account_id": "0011U00000TFV7MQAX",
  "owner": "alice@yourcompany.com",
  "label": "enterprise-tier"
}
```

#### Company radar

```bash
curl -s -X POST "https://api.tamradar.com/v1/radars/companies" \
  -H "x-api-key: $TAMRADAR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "domain": "openai.com",
    "radar_type": "company_job_openings",
    "webhook_url": "https://hook.make.com/your-scenario-url",
    "departments": ["Engineering", "Research"],
    "seniorities": ["Senior", "Director", "Vice President", "CXO"],
    "custom_fields": { "crm_account_id": "0011U00000TFV7MQAX" }
  }'
```

#### Contact radar

```bash
curl -s -X POST "https://api.tamradar.com/v1/radars/contacts" \
  -H "x-api-key: $TAMRADAR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "radar_type": "contact_job_changes",
    "domain": "openai.com",
    "profile_url": "https://www.linkedin.com/in/samaltman",
    "full_name": "Sam Altman",
    "webhook_url": "https://hook.make.com/your-scenario-url",
    "custom_fields": { "crm_contact_id": "003Dn00000AHtLQIA1" }
  }'
```

#### Industry radar

```bash
curl -s -X POST "https://api.tamradar.com/v1/radars/industry" \
  -H "x-api-key: $TAMRADAR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "radar_type": "industry_funding_rounds",
    "webhook_url": "https://hook.make.com/your-scenario-url",
    "custom_fields": { "label": "ai-funding-watch" }
  }'
```

### ⚠️ CRITICAL: persist the mapping after every 201

Store immediately to `./tamradar/tamradar-radars.jsonl` (append one JSON object per line):

```json
{ "input_domain": "openai.com", "input_radar_type": "company_job_openings", "radar_id": "c70813b3-7e87-4ca7-90fd-8c64574d911b", "created_at": "2026-05-01T09:05:00Z" }
```

This file IS your monitoring state. Without it you cannot poll by specific radar, deactivate targets, or check idempotency before creating.

### Bulk creation — for >10 targets

Send up to 100 radars per request. Envelope `webhook_url` cascades to all items; per-item `webhook_url` overrides it.

```bash
curl -s -X POST "https://api.tamradar.com/v1/radars/bulk" \
  -H "x-api-key: $TAMRADAR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "webhook_url": "https://hook.make.com/your-scenario-url",
    "radars": [
      { "domain": "openai.com", "radar_type": "company_job_openings", "custom_fields": { "crm_account_id": "001" } },
      { "domain": "anthropic.com", "radar_type": "company_new_hires", "custom_fields": { "crm_account_id": "002" } }
    ]
  }'
```

**Bulk response — HTTP 207 partial:**

Check each item in `results[]` by `index`:

* `code: 201` → success, store mapping to `./tamradar/tamradar-radars.jsonl`
* `code: 409` → already exists. Extract `radar_id` from `errors[0].reason`. Store it. Treat as active.
* `code: 402` + `retryable: true` → insufficient balance. Tell user to top up, retry this item.
* `code: 400` + `retryable: false` → validation error. Show `errors[].reason`. Ask user to fix.

***

## Phase 3: Error handling

MUST handle every error before reporting success.

| Code  | Meaning              | Action                                                                                                                     |
| ----- | -------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| `401` | Invalid API key      | STOP. Ask user to verify key.                                                                                              |
| `402` | Insufficient balance | STOP. Tell user to add funds via their account manager or [support@tamradar.com](mailto:support@tamradar.com), then retry. |
| `409` | Radar already exists | NOT an error — it's already running. Extract `radar_id` from `errors[0].reason`. Store it. Move on.                        |
| `400` | Validation error     | Show `errors[].field` + `errors[].reason` verbatim. Ask user to correct.                                                   |
| `429` | Rate limited         | Read `Retry-After` header — wait exactly that many seconds, then retry. Max 3×.                                            |
| `5xx` | Server error         | Retry once. If still failing, report `error_id` to user and STOP.                                                          |

***

## Phase 4: Retrieve findings (polling)

`GET /v1/updates` returns the same data as webhooks. The response uses `updates[]` — not `data[]`.

**Note:** findings are retained for 60 days. Data older than 60 days is purged.

**Note:** `radar_failure` events are excluded by default. Default `update_type` is `radar_finding` only.

**Sample response:**

```json
{
  "status": "success",
  "code": 200,
  "updates": [
    {
      "update_id": "550e8400-e29b-41d4-a716-446655440000",
      "update_type": "radar_finding",
      "discovered_at": "2026-05-01T08:30:00Z",
      "data": {
        "radar_id": "c70813b3-7e87-4ca7-90fd-8c64574d911b",
        "radar_type": "company_job_openings",
        "domain": "openai.com",
        "custom_fields": { "crm_account_id": "001" }
      },
      "content": { }
    }
  ],
  "has_more": false,
  "next_cursor": null
}
```

`content` is the actual signal payload — shape varies by `radar_type` (job posting details, person change, funding round, etc.). See the full API docs for per-type schemas.

```json
```

### Fetch findings for specific radars (max 10 radar\_ids per call)

```
GET /v1/updates?radar_id={id1},{id2}&limit=50
```

If you have more than 10 radars, split into multiple calls of up to 10 IDs each.

### Fetch by signal type

```
GET /v1/updates?radar_type=company_job_openings,company_new_hires&limit=50
```

### Incremental — only new since last check

```
GET /v1/updates?since={last_discovered_at}&limit=100
```

ALWAYS store `discovered_at` from the last update you processed. Pass it as `since` on the next call.

### Pagination

IF `has_more: true` → fetch next page:

```
GET /v1/updates?cursor={next_cursor}
```

Repeat until `has_more: false`.

### To include radar failures in polling

```
GET /v1/updates?update_type=radar_finding,radar_failure
```

### Presenting findings to the user

Group by `data.radar_type`. Always include `discovered_at`. Example:

> **New job at openai.com** — Senior Research Engineer, Alignment Detected: 2026-04-29 · [View posting](https://openai.com/careers/...)

***

## Phase 5: Deactivate radars

```bash
curl -s -X DELETE "https://api.tamradar.com/v1/radars/{radar_id}" \
  -H "x-api-key: $TAMRADAR_API_KEY"
```

* Irreversible. Must create a new radar to resume monitoring.
* No further charges after deactivation.

IF user says "stop monitoring openai.com for job openings" → look up `radar_id` from `./tamradar/tamradar-radars.jsonl` and DELETE it.

***

## Phase 6: Retrieving findings

**Default: use polling.** `GET /v1/updates` always works regardless of `webhook_url`. Findings are available immediately after creation and retained for 60 days.

If the user later wants real-time push delivery:

> *"Your radars are active and I can fetch findings anytime you ask. For real-time delivery into Slack, your CRM, or an automation, connect a tool like Make.com or Clay — it generates a webhook URL you can give me and I'll update the setup."*

***

## Summary template — ALWAYS present when done

```
TAMradar setup complete.

Balance: $42.50 remaining

Radars active:
  company_job_openings     · openai.com     · c70813b3-7e87-4ca7-90fd-8c64574d911b
  contact_job_changes      · Sam Altman     · a4f28c91-3b12-4e70-9d55-7e3dda58f100  (already existed — skipped POST)
  industry_funding_rounds  · —              · f9d01c55-82ba-4f3e-b461-2a9e7c11d830

Failures:
  None

Mapping saved to:
  ./tamradar/tamradar-radars.jsonl

To fetch findings now:
  GET /v1/updates?radar_id=c70813b3-...,a4f28c91-...&limit=50
  (max 10 IDs per call — split into multiple requests if needed)

Findings retained for 60 days.

Webhook: Make.com scenario (https://hook.make.com/...) ✓
  — OR —
Polling only. Ask me anytime to check for new findings.
```

***

## Radar type reference

### Company — `POST /v1/radars/companies`

Required: `domain`, `radar_type` — `webhook_url` optional (omit for poll-only)

| `radar_type`                 | What it tracks                | Filterable? |
| ---------------------------- | ----------------------------- | ----------- |
| `company_job_openings`       | New job postings              | Yes         |
| `company_new_hires`          | People joining the company    | Yes         |
| `company_promotions`         | Internal promotions           | Yes         |
| `company_reviews`            | Glassdoor/employer reviews    | No          |
| `company_mentions`           | Web mentions of the company   | No          |
| `company_social_posts`       | Company LinkedIn/social posts | No          |
| `company_social_posts_cxo`   | Executive social posts        | Yes         |
| `company_social_engagements` | Engagements on company posts  | No          |

#### Domain normalization

The API accepts any format — bare domain, URL, or with www/path/port. All are normalized automatically:

* `openai.com` ✅
* `https://www.openai.com/about` ✅ → normalized to `openai.com`
* `www.openai.com` ✅ → normalized to `openai.com`

Best practice: pass the bare domain (`openai.com`).

#### Filters for filterable types

**`departments[]`** — OR within the array. Omit to match any department. Do not send an empty array (400 error).

**`seniorities[]`** — OR within the array. Omit to match any seniority. Do not send an empty array (400 error).

When both are provided, findings must match the department filter AND the seniority filter.

**Valid `departments`:** Accounting, Administrative, Business Development, Consulting, Customer Success, Design, Education, Engineering, Finance, Human Resources, Information Technology, Legal, Manufacturing, Marketing, Media and Communication, Operations, Product Management, Project Management, Purchasing, Quality Assurance, Real Estate, Research, Sales, Support

**Valid `seniorities`:** Owner, CXO, Vice President, Director, Manager, Senior, Entry, Training, Partner

NEVER invent department or seniority values. Use ONLY the values listed above or you will get a `400` error.

#### `job_titles` — boolean expression string (optional, overrides departments + seniorities)

When `job_titles` is set, it replaces `departments` and `seniorities` — you cannot combine them.

Supported operators: `OR`, `AND`, `NOT`. Multi-word terms must be double-quoted.

```
"CEO OR CTO"
"VP Engineering OR VP Sales"
"(CEO OR CTO) AND NOT Manager"
"\"Head of Engineering\" OR \"VP of Engineering\""
```

### Contact — `POST /v1/radars/contacts`

Required: `radar_type` + type-specific identifiers — `webhook_url` optional (omit for poll-only):

| `radar_type`                 | Additional required fields                                                   |
| ---------------------------- | ---------------------------------------------------------------------------- |
| `contact_job_changes`        | `domain` + at least one of: `email`, `profile_url`, `full_name`              |
| `contact_social_engagements` | `profile_url` (must contain `linkedin.com/in/`, `x.com/`, or `twitter.com/`) |
| `contact_social_posts`       | `profile_url` (same URL formats)                                             |

#### Profile URL normalization

Pass the full URL. The API normalizes automatically:

* `https://www.linkedin.com/in/samaltman` ✅ (preferred)
* `https://linkedin.com/in/samaltman` ✅ → normalized to `https://www.linkedin.com/in/samaltman`
* `linkedin.com/in/samaltman` ✅ → `https://` added automatically
* Trailing slash stripped automatically

Uniqueness for contacts is keyed on `profile_url` + radar type — different radar types for the same person ARE allowed.

### Industry — `POST /v1/radars/industry`

Required: `radar_type`. No `domain` field — `webhook_url` optional (omit for poll-only).

| `radar_type`              | Additional required fields                               |
| ------------------------- | -------------------------------------------------------- |
| `industry_funding_rounds` | None                                                     |
| `industry_new_companies`  | None                                                     |
| `industry_mentions`       | `keyword` (min 3 chars)                                  |
| `industry_job_openings`   | `keyword` (min 3 chars) + optional `countries[]` (max 5) |

***

## Key fields reference

\| Field | Found in | Purpose | |-------|----------|---------|| | `radar_id` | Creation response → `data.radar_id` | MUST store with input. Used for polling, deactivation. | | `update_id` | Every finding payload | Deduplicate on this. NEVER process the same `update_id` twice. | | `discovered_at` | Every finding payload | Store latest value. Use as `since` param for incremental polling. | | `custom_fields` | Request body → echoed in every finding | Your internal IDs. Route findings back to your CRM using these. | | `data.radar_type` | Every finding payload | Route your processing logic on this. | | `content` | Every finding payload | The actual finding. Shape varies by `radar_type`. |

***

## Hard limits

* `webhook_url` is optional — omit for poll-only radars; include only when real-time push delivery is needed
* Rate limit: **100 radar creates / minute, 10/sec** (writes) · **200 reads / minute, 10/sec** — separate buckets. Need more? contact [support@tamradar.com](mailto:support@tamradar.com)
* Bulk max: **100 radars** per request
* Polling `radar_id` filter: **max 10 IDs** per call — split into multiple requests if you have more
* Polling data retention: **60 days** — older data is purged
* One radar per `domain + radar_type` per account — duplicates get `409`
* Contact uniqueness: per `profile_url + radar_type` — different contact radar types for the same person ARE allowed
* Persistence: save mappings to `./tamradar/tamradar-radars.jsonl` by default