# Guidance for AI agents

> Rules for an AI agent that calls the Funnelfeedr API on someone's behalf — which calls spend credits, how to quote before revealing, and how to stop safely.

Every API key can spend credits, so an agent with a key can turn a customer's credits into organization and contact details. Credits are money, and a reveal cannot be undone. This page is the contract an agent should follow; the same rules are in [/developers/llms.txt](https://funnelfeedr.com/developers/llms.txt) and in the [snippet for your agent's prompt](https://funnelfeedr.com/developers/quickstart.md#tell-your-agent).

## The rules

1. Only reveals spend credits: `POST /contacts/reveal`, and `POST /organizations/match`, `POST /people/match` or `POST /organizations/switchboards/match` with `reveal` set and `dryRun` false. Matching, contacts, switchboards and the balance are free.
2. Before any reveal, send the same request with `dryRun: true`, show the user the quoted cost (`creditsQuoted`), and reveal only after they approve it.
3. Always set `maxCredits` on a reveal to the amount the user approved. A request that would cost more fails with `credit_cap_exceeded` and spends nothing.
4. Always send an `Idempotency-Key` header (a new UUID per logical request) on a call that spends, and on every `POST /contacts/reveal`, dry runs included. Reuse the same key, with the same body, only to retry that same request.
5. Never loop reveals one item at a time and never reveal the same contact or organization twice. Batch up to 25 items in one request.
6. On `402 insufficient_credits`, stop and tell the user. Nothing was spent; do not retry until they have topped up.
7. On an `ambiguous` match, ask the user which candidate they mean, or pick one by `id` from your own data. Never reveal every candidate.
8. On `429 rate_limited`, wait the number of seconds in `Retry-After` before retrying. Retry other errors only when the body says `"retryable": true`.
9. Read the key from the `FUNNELFEEDR_API_KEY` environment variable. Never print, log or commit it, and never send it from a browser or mobile app.

## Which calls spend credits

| Call | Spends? |
|---|---|
| `POST /organizations/match` without `reveal`, or with `dryRun: true` | Never. The basics come back; the details only if the account already revealed them. |
| `POST /organizations/match` with `reveal: ["details"]` and without `dryRun` | Yes, for the organizations whose details it reveals |
| `POST /organizations/contacts/match` | Never. Details come back masked unless the account already revealed them. |
| `POST /organizations/switchboards/match` without `reveal`, or with `dryRun: true` | Never |
| `POST /organizations/switchboards/match` with `reveal` and without `dryRun` | Yes, for the switchboard numbers and emails it unmasks |
| `POST /people/match` without `reveal`, or with `dryRun: true` | Never |
| `POST /people/match` with `reveal` and without `dryRun` | Yes, for the email and phone numbers it unmasks |
| `POST /contacts/reveal` with `dryRun: true` | Never (the call still needs an `Idempotency-Key`) |
| `POST /contacts/reveal` | Yes |
| `GET /credits` | Never |

Only an organization's details (0.2 credits per organization by default), a contact's email (0.2) and phone (1.0) cost anything; a contact's LinkedIn URL is never masked and never charged. Details the account has already revealed cost nothing again, and a request that finds nothing charges nothing. The [OpenAPI spec](https://funnelfeedr.com/developers/openapi.json) marks the operations that can spend with an `x-credit-cost` extension.

## A safe reveal, step by step

1. **Find the people for free.** Use `people/match` without `reveal`, or `organizations/contacts/match` (which lists only the people matching the account's target contact roles, for up to 25 organizations a call), to get contact ids. Keep only the people the user actually needs. To call an organization rather than a person, find its switchboard with `organizations/switchboards/match`.
2. **Quote.** Send the reveal with `dryRun: true`. The response's `creditsQuoted` is the exact price, and each result's `reveal.creditCost` that person's price. A person who appears twice is charged once but shows the price on both results, so show the user `creditsQuoted`, not a sum of the results.
3. **Ask.** Show the user the number of people, what will be revealed and the price, and wait for a yes. A user's standing instruction ("reveal emails for up to 50 credits a day") counts — keep a running total against it.
4. **Reveal once.** Send the same request with `dryRun: false`, `maxCredits` set to the approved amount and a new `Idempotency-Key`.
5. **Check the answer.** Read `creditsCharged` and `creditsRemaining` and report them. Do not reveal the same contacts again.

The reveal in step 4, as a `POST /contacts/reveal` body:

```json
{
  "items": [
    { "ref": "anna", "contactId": "c0a8012e-5b1f-4e3d-9a7c-2f6b8d4e1a90" },
    { "ref": "johan", "contactId": "d41d8cd9-8f00-4b20-9e98-0ecf8427e1b2" }
  ],
  "infoTypes": ["email", "phone"],
  "maxCredits": 1.4,
  "dryRun": false
}
```

Organization details follow the same steps. Match for free first and check `detailsRevealed`: an organization the account already revealed returns its details without a reveal. Quote the rest with `organizations/match`, `reveal: ["details"]` and `dryRun: true` (`creditsQuoted` is the total; each result's `creditCost` is that organization's price, shown on every result for an organization that appears more than once but charged once), ask, then reveal once with `maxCredits` and an `Idempotency-Key`.

## When to stop

| You get | Do this |
|---|---|
| `402 insufficient_credits` | Stop. Nothing was spent. Tell the user the balance is too low; they top up in the app. Do not retry in a loop. |
| `422 credit_cap_exceeded` | Stop. Nothing was spent. The body's `creditsRequired` is the real cost; ask the user before raising `maxCredits`. |
| `429 rate_limited` | Wait `Retry-After` seconds, then continue. |
| `409 idempotency_key_in_use` | The same request is still running. Wait a moment, then retry with the same key to get its answer. |
| `5xx` | Retry a few times with backoff. On a spend call, reuse the same `Idempotency-Key` so a retry cannot charge twice. |

Branch on the `code` field, never on the `detail` text: `detail` is for humans and may change.

## Ambiguous and missing matches

- `ambiguous` means more than one person fits. The response lists up to five `candidates` with their id, name, job title and organization. Ask the user which one they mean, or pick by `id` if your own data settles it (a matching title, say). Never reveal all of them to be safe.
- `not_found` is free and final for that input. Funnelfeedr does not guess: nicknames ("Kalle" for "Karl") do not match in v1. Try another identifier you have — an email or a LinkedIn URL — rather than variations of the name. An email only matches an address the account has already revealed or owns, so a `not_found` email says nothing about whether the address exists; try the name and organization or the LinkedIn URL.

See [Matching](https://funnelfeedr.com/developers/matching.md) for full examples of each.

## Batch, don't loop

Every lookup takes up to 25 items. Send one request with 25 people or organizations rather than 25 requests with one, and use `ref` to line answers up with your rows. A reveal is all-or-nothing per request, so a batch is either fully charged or not charged at all. See [Batching](https://funnelfeedr.com/developers/batching.md).
