# Heedeo > Heedeo is traffic control for AI agents. It identifies every agent that reaches a website, works out which company built it and which person or company it is acting for, and applies the site's own rules per route: allow, challenge, throttle or block. Real customers and their agents get through. Farms that hide behind the same agents get capped or stopped. Heedeo is ready, and access is by booking a short call. ## Who it's for Teams whose products agents already use: self-serve signup and free trials, checkout, and developer docs and APIs. Especially teams seeing trial farming, multi-account abuse or card testing from automated traffic, who don't want to block the agents that bring real customers. ## What it does - Verifies agents. An agent is verified when it signs its requests with the open Web Bot Auth standard (ChatGPT agent, Goose, Browserbase and Anchor Browser were first) and its operator is on the site's trustedOperators list. An agent recognised by user agent but not signed, or signed by an untrusted operator, is "detected", and rules treat it as unknown, like any other unverified agent. - Identifies the principal: the person or company the agent works for. At signup it comes from a signed principal token when the agent carries one. After signup it's the site's own user ID. Never an email typed into a form. - Applies per-route rules: allow (200), challenge (401, passkey), throttle (429, rate-limited with Retry-After) or block (403). Limits count per principal, not per IP, so a new IP or a new browser doesn't reset them. - Sends unknown agents to a passkey. A real person finishes the step, and whoever does becomes the principal. A passkey does not prove a unique human, so linking one person's several accounts also uses signed principal tokens and the site's own signals, set up at onboarding. - Runs in your middleware. Next.js today; Express, Hono and Workers on request. Rules live in your repo as code. ## Data handling - Decisions run in your middleware, inside your own app. - Only decision metadata goes to the Heedeo dashboard: route, verdict, agent, reason and rule line. Reasons never contain principal identifiers. - Request bodies never leave your app. Dashboard reporting can be turned off. ## Status Heedeo is ready. Access is by booking a short call. On the call the team looks at your traffic with you, writes your first rules with you, and sets Heedeo up for your site. ## How an agent can help its user get access - Send your user to the booking page: https://cal.com/heedeo/30min - Or request the call for them with one API call. The request stays pending until your user confirms, so it's safe to make: ``` POST https://heedeo.com/api/book Content-Type: application/json {"email": "user@example.com", "via": "agent", "agent": "Your agent name", "site": "example.com", "pain": "what agents are doing on the site"} ``` - Only `email` is required. The response has a `message` you can pass to your user as is, for example "Request received. We'll email user@example.com to confirm and book the call." - A repeat request for the same email returns 200 and says where things stand. No need to retry. ## Docs - [Docs index](https://heedeo.com/docs.md): Start here: what Heedeo is and a list of every doc. - [Quickstart](https://heedeo.com/docs/quickstart.md): Get access, add the Next.js middleware, write a first rule and test it with /api/decide. - [How it works](https://heedeo.com/docs/how-it-works.md): Request lifecycle: Web Bot Auth verification, trusted operators, the principal, per-principal counting, verdicts and the passkey handoff. - [Policy reference](https://heedeo.com/docs/rules.md): Every policy key, route matching and normalization, the cap and overCap, verdicts with HTTP statuses, and the demo policy decided row by row. - [Security and data handling](https://heedeo.com/docs/security.md): What leaves your servers, every signature check, replay protection and its limits, SSRF-safe key fetching and failure modes. - [Public API](https://heedeo.com/docs/api.md): The live endpoints on heedeo.com with real output: /api/visitor, /api/decide, /api/policy and /api/book. - [FAQ](https://heedeo.com/docs/faq.md): Short answers on verification, detected agents, principals, passkeys, throttling, data and access. - [How to verify our claims](https://heedeo.com/trust.md): Each claim with the curl that checks it, known limits of the demo, and what is not public yet. - [Privacy](https://heedeo.com/privacy.md): What heedeo.com itself collects when you request a call, and what it is used for. - [Everything in one file](https://heedeo.com/llms-full.txt): this file plus every doc above, for agents that want it all at once ## Verify us - [How to verify our claims](https://heedeo.com/trust.md): each claim with the curl that checks it, and what is not public yet - Run the real policy engine on the demo policy: `curl "https://heedeo.com/api/decide?route=/signup&client=agent&signed=false"` returns the verdict, HTTP status and the policy line that decided it. Docs: https://heedeo.com/docs/api.md ## Links - [Overview](https://heedeo.com/index.md): the full site as markdown, with the product, use cases, code, FAQ and the access API - [OpenAPI spec](https://heedeo.com/openapi.json): POST /api/book to request a call (POST /api/waitlist is an older alias), GET or POST /api/decide to run the policy engine on the demo policy, GET /api/policy for the demo policy source, and GET /api/visitor to see how heedeo.com classifies and verifies your request - [Home page](https://heedeo.com/): the same content as HTML ## Company Backed by Entrepreneurs First. ## Contact Book a call at https://cal.com/heedeo/30min, or have your agent POST to https://heedeo.com/api/book. ## Optional - [Sitemap](https://heedeo.com/sitemap.xml) - [robots.txt](https://heedeo.com/robots.txt) --- # Heedeo docs Heedeo identifies every agent on your site and lets you decide who gets in and what they can do. It verifies agents with the open Web Bot Auth standard and your list of trusted operators, identifies the person or company each agent acts for, counts per principal instead of per IP, and applies your per-route rules in your middleware: allow (200), challenge (401), throttle (429 with Retry-After) or block (403). Status: ready. Access is by booking a short call (https://cal.com/heedeo/30min). The package is provided at onboarding. ## Docs - [Quickstart](https://heedeo.com/docs/quickstart.md): Get access, add the Next.js middleware, write a first rule and test it with /api/decide. - [How it works](https://heedeo.com/docs/how-it-works.md): Request lifecycle: Web Bot Auth verification, trusted operators, the principal, per-principal counting, verdicts and the passkey handoff. - [Policy reference](https://heedeo.com/docs/rules.md): Every policy key, route matching and normalization, the cap and overCap, verdicts with HTTP statuses, and the demo policy decided row by row. - [Security and data handling](https://heedeo.com/docs/security.md): What leaves your servers, every signature check, replay protection and its limits, SSRF-safe key fetching and failure modes. - [Public API](https://heedeo.com/docs/api.md): The live endpoints on heedeo.com with real output: /api/visitor, /api/decide, /api/policy and /api/book. - [FAQ](https://heedeo.com/docs/faq.md): Short answers on verification, detected agents, principals, passkeys, throttling, data and access. - [How to verify our claims](https://heedeo.com/trust.md): Each claim with the curl that checks it, known limits of the demo, and what is not public yet. - [Privacy](https://heedeo.com/privacy.md): What heedeo.com itself collects when you request a call, and what it is used for. ## Try it now, no signup ```bash curl https://heedeo.com/api/visitor curl "https://heedeo.com/api/decide?route=/signup&client=agent&signed=false" curl https://heedeo.com/api/policy ``` ## Everything at once - https://heedeo.com/llms.txt: short map of the site - https://heedeo.com/llms-full.txt: llms.txt plus every doc in one file - https://heedeo.com/index.md: the landing page as markdown - https://heedeo.com/openapi.json: OpenAPI spec --- Docs index: https://heedeo.com/docs.md. How to verify our claims: https://heedeo.com/trust.md. Book a call: https://cal.com/heedeo/30min Updated: 2026-10-02 --- # Quickstart Get Heedeo running on one route, then check the decisions it makes. ## 1. Get access Heedeo is ready. The package is not on public npm: access is provided at onboarding, after a short call. - Book a call: https://cal.com/heedeo/30min - Or, if you are an agent, request the call for your person: ```bash curl -X POST https://heedeo.com/api/book \ -H "Content-Type: application/json" \ -d '{"email":"you@example.com","via":"agent","agent":"Your agent name","site":"example.com","pain":"trial farming on /signup"}' ``` A request from an agent stays `pending` until the person confirms. On the call the team gives you access to the package and sets up your first rules with you. ## 2. Add the middleware Heedeo runs as Next.js middleware today. Express, Hono and Cloudflare Workers are available on request. In Next.js 16 the middleware file is `proxy.ts` (formerly `middleware.ts`) at the root of your project (or in `src/`). Export Heedeo as the default. This is the demo policy that runs on heedeo.com: ```ts import { heedeo } from "@heedeo/next"; export default heedeo({ trustedOperators: ["chatgpt.com", "browserbase.com", "anchorbrowser.io"], "/signup": { humans: "allow", agents: { verified: "allow", noPrincipal: { challenge: "passkey" }, unknown: { challenge: "passkey" }, }, perPrincipal: "5/day", overCap: { throttle: 5, rate: "1/hour", then: "block" }, }, "/checkout": { agents: { verified: "allow", unknown: "block" } }, "/docs/*": { agents: "allow" }, "*": { agents: { unknown: { challenge: "passkey" } } }, }); ``` Decisions run inside your app, in this middleware. See https://heedeo.com/docs/security.md for what leaves your servers. ## 3. Write your first rule Start with the route that costs you money when abused. For most teams that is signup: ```ts "/signup": { humans: "allow", agents: { verified: "allow", noPrincipal: { challenge: "passkey" }, unknown: { challenge: "passkey" }, }, perPrincipal: "5/day", overCap: { throttle: 5, rate: "1/hour", then: "block" }, }, ``` Read it as: - People sign up as usual. - Agents signed by a trusted operator (see `trustedOperators`) may sign people up. - A trusted agent that names nobody, and any agent that is not verified, is sent to a passkey, so a real person finishes the signup and becomes the principal. - Each principal gets 5 signups a day on this route, people included. The 6th to 10th are rate-limited to 1 an hour (429). From the 11th, blocked (403). Every key is explained in https://heedeo.com/docs/rules.md. ## 4. Test the rule against the real policy engine `/api/decide` on heedeo.com runs the real policy engine on the demo policy above. You describe a request, it returns the verdict and the policy line that decided it. ```bash # A trusted agent's 2nd signup today for one principal: allow curl "https://heedeo.com/api/decide?route=/signup&client=agent&signed=true&operator=chatgpt.com&principal=maya&count=2" # The same principal's 7th: throttle (429, retryAfter 3600) curl "https://heedeo.com/api/decide?route=/signup&client=agent&signed=true&operator=chatgpt.com&principal=maya&count=7" # A valid signature from an operator not on the trusted list: unknown, so challenge curl "https://heedeo.com/api/decide?route=/signup&client=agent&signed=true&operator=agent.example-operator.com" ``` The first returns: ```json {"verdict":"allow","status":200,"retryAfter":null,"reason":"Verified agent on /signup: allow. Signed by chatgpt.com, a trusted operator. This person is at 2/5 per day on /signup.","rule":{"path":"/signup","line":8,"text":"verified: \"allow\","},"principal":"maya","policy":"demo","note":"Same engine and rules shown on heedeo.com. Your own rules are set up during onboarding."} ``` `rule.line` is a line number in the policy source from `GET https://heedeo.com/api/policy`. The full contract is in https://heedeo.com/docs/api.md. ## 5. Next - How a request is decided: https://heedeo.com/docs/how-it-works.md - Every policy key: https://heedeo.com/docs/rules.md - Data handling and failure modes: https://heedeo.com/docs/security.md --- Docs index: https://heedeo.com/docs.md. How to verify our claims: https://heedeo.com/trust.md. Book a call: https://cal.com/heedeo/30min Updated: 2026-10-02 --- # How Heedeo works Heedeo identifies every agent on your site and lets you decide who gets in and what they can do. For each request it answers three questions: who built the agent, who sent it, and what your rules say it may do on this route. ## Request lifecycle Everything below runs in your middleware, before the request reaches your route. 1. **Normalize and match the route.** The path is normalized (see https://heedeo.com/docs/rules.md), then matched: exact routes first, then `/x/*` prefixes (longest first), then the catch-all `"*"`. 2. **Classify.** Is this a person or an agent? 3. **Verify.** If the request carries a Web Bot Auth signature, check it cryptographically, then check that the operator is on `trustedOperators`. Both pass: **verified**. Otherwise the agent is **detected** (recognised by user agent but not signed, or signed by an untrusted operator) or **unknown**, and rules treat both as `unknown`. 4. **Identify the principal.** Work out the person or company the agent acts for (see below). 5. **Apply the client rule.** `humans`, or `agents.verified`, `agents.noPrincipal` or `agents.unknown`. 6. **Apply the per-principal cap.** If the route has `perPrincipal` and the request has a principal, count it. The cap covers the whole route, people and agents alike. A client rule that already blocked stays blocked. 7. **Record.** Send decision metadata (route, verdict, agent, reason, rule line) to your dashboard, unless you turned that off. Never the request body. ## Agent tiers | Tier | Meaning | How rules treat it | |---|---|---| | verified | A valid Web Bot Auth signature from an operator on `trustedOperators`. | `verified` | | detected | Recognised by user agent but not signed (or signed by an untrusted operator). | Treated as `unknown`. | | unknown | No valid signature from a trusted operator, and not recognised either. | `unknown` | The landing page legend uses "signs its requests" for verified, "detected, not signed" for detected, and "unknown" for unknown. ## Verification: who built the agent Heedeo uses the open Web Bot Auth standard: - The agent signs each request with **HTTP Message Signatures** (RFC 9421, https://www.rfc-editor.org/rfc/rfc9421) using an **Ed25519** key. The signature travels in the `Signature` and `Signature-Input` headers. - The `Signature-Agent` header names the operator, for example `"https://chatgpt.com"`. - The operator publishes its public keys at `/.well-known/http-message-signatures-directory` on that origin. The signature's `keyid` is the key's JWK SHA-256 thumbprint (RFC 7638). Heedeo finds that key and checks the signature against it. - A valid signature is not enough on its own: anyone can publish a key directory on their own domain. The operator must also be on your `trustedOperators` list. A valid signature from anyone else counts as unknown. Agents known to sign today include ChatGPT agent, Goose, Browserbase and Anchor Browser, per Cloudflare's signed agents list: https://blog.cloudflare.com/signed-agents/. The demo policy trusts `chatgpt.com`, `browserbase.com` and `anchorbrowser.io`. Which operators you trust is your call. A user agent string alone is never proof. Anyone can type "ChatGPT" into a header; that makes an agent detected at most, and detected is treated as unknown. ## The principal: who sent it The principal is the person or company the agent acts for. Heedeo takes it from the strongest source available: 1. **A signed principal token**, when the agent carries one. Examples: Skyfire KYAPay, Visa Trusted Agent Protocol. 2. **The passkey handoff.** When a rule says `challenge: "passkey"`, a real person finishes the step with a passkey, and that person becomes the principal. 3. **Your own user ID**, after signup. An email typed into your form is never treated as the principal. ## Counting: per principal, not per IP Limits such as `perPrincipal: "5/day"` count per principal. An IP costs a fraction of a cent, so a farm can rotate through hundreds. A new IP, a new browser or a new agent does not reset the count when the principal is the same. Where the per-principal counters live in your deployment, and whether a counter outage fails open or closed, are agreed during onboarding. ## What a passkey does and does not prove A passkey proves a real person with a real device finished the step. It does not prove a unique human: one person can hold several passkeys, and so can an email address. So a determined person can still look like several principals. Linking one person's several accounts relies on signed principal tokens when the agent carries one, plus your own signals (payment card, verified email domain, device), set up with you at onboarding. ## Verdicts | Verdict | HTTP status | What happens | |---|---|---| | allow | 200 | The request reaches your route. | | challenge | 401 | The request waits on a person: a passkey step. Whoever finishes it becomes the principal. | | throttle | 429 | Rate-limited, not refused. The response carries `Retry-After` (`retryAfter` in seconds). | | block | 403 | The request does not reach your route. Logged with its reason. | These are the statuses `/api/decide` returns for the demo policy. ## What people see People never see a puzzle. The only thing a person sees is one passkey confirmation, when an unknown agent signs up on their behalf. ## Worked examples on the demo policy All real output of `/api/decide`, trimmed to the main fields: | Request | verdict | status | rule.line | |---|---|---|---| | person on /signup | allow | 200 | 6 | | trusted agent, principal maya, 2nd today | allow | 200 | 8 | | trusted agent, no principal | challenge | 401 | 9 | | unsigned agent | challenge | 401 | 10 | | valid signature, untrusted operator | challenge | 401 | 10 | | principal maya, 7th today | throttle, retryAfter 3600 | 429 | 13 | | principal maya, 11th today | block | 403 | 13 | | person, principal dave, 12th today | block | 403 | 13 | ```bash curl "https://heedeo.com/api/decide?route=/signup&client=agent&signed=true&operator=chatgpt.com" curl "https://heedeo.com/api/decide?route=/signup&client=human&principal=dave&count=12" ``` --- Docs index: https://heedeo.com/docs.md. How to verify our claims: https://heedeo.com/trust.md. Book a call: https://cal.com/heedeo/30min Updated: 2026-10-02 --- # Policy reference Rules live in your repo as code, in the middleware file. They are plain data: easy to read, diff and roll back. This page documents the demo policy that runs on heedeo.com, with its line numbers (every decision cites one): ```text 1 import { heedeo } from "@heedeo/next"; 2 3 export default heedeo({ 4 trustedOperators: ["chatgpt.com", "browserbase.com", "anchorbrowser.io"], 5 "/signup": { 6 humans: "allow", 7 agents: { 8 verified: "allow", 9 noPrincipal: { challenge: "passkey" }, 10 unknown: { challenge: "passkey" }, 11 }, 12 perPrincipal: "5/day", 13 overCap: { throttle: 5, rate: "1/hour", then: "block" }, 14 }, 15 "/checkout": { agents: { verified: "allow", unknown: "block" } }, 16 "/docs/*": { agents: "allow" }, 17 "*": { agents: { unknown: { challenge: "passkey" } } }, 18 }); ``` Get it yourself: `curl -s https://heedeo.com/api/policy` returns `{ "source": ..., "rules": ... }`. Anything not on this page is set up with you at onboarding. ## `trustedOperators` (line 4) The operators whose signatures count as verified. An entry also covers its subdomains. A valid Web Bot Auth signature from any other operator counts as unknown. ## Routes and matching Each other top-level key is a route. | Key | Matches | |---|---| | `"/signup"` | exactly `/signup` | | `"/checkout"` | exactly `/checkout` | | `"/docs/*"` | anything under `/docs/`, for example `/docs/auth` (not `/docs` itself) | | `"*"` | every path no other key matches | **Order:** exact routes first, then `/x/*` prefixes (longest prefix first), then `"*"`. **Normalization**, before matching: lowercase; drop the query and fragment; decode percent escapes; read `\` as `/`; collapse `//`; resolve `.` and `..`; drop the trailing slash. So `/SignUp/` matches `/signup`, and `/docs/../checkout` matches `/checkout`, not `/docs/*`: ```bash curl "https://heedeo.com/api/decide?route=/docs/../checkout&client=agent&signed=false" # verdict "block", rule.path "/checkout", line 15 ``` ## Route rule keys | Key | Values in the demo | Meaning | |---|---|---| | `humans` | `"allow"` | What to do with people. Unset means allow. | | `agents` | `"allow"`, or an object | A string applies to every agent, whatever its tier. An object sets each tier. Unset means allow. | | `agents.verified` | `"allow"` | Agents signed by a trusted operator. | | `agents.noPrincipal` | `{ challenge: "passkey" }` | Verified agents that name no principal, on a route with `perPrincipal`. The cap can't be counted without a principal. Unset means challenge with passkey. | | `agents.unknown` | `{ challenge: "passkey" }`, `"block"` | Agents that are not verified, including detected ones. Unset falls back to the `"*"` rule's `unknown`, then to challenge with passkey. | | `perPrincipal` | `"5/day"` | A cap per principal on the whole route, people and agents alike. Windows: `"N/min"`, `"N/hour"`, `"N/day"`. | | `overCap` | `{ throttle: 5, rate: "1/hour", then: "block" }` | What happens past the cap. The next `throttle` requests are rate-limited at `rate`, then blocked. | Actions are `"allow"`, `"block"` or `{ challenge: "passkey" }`. ## The cap, step by step With `perPrincipal: "5/day"` and the `overCap` above, for one principal on `/signup`: | Count in the window, including this request | Verdict | Status | Rule line | |---|---|---|---| | 1 to 5 | the client rule's verdict | 200 for allow, 401 for challenge | 6, 8 or 10 | | 6 to 10 | throttle, `retryAfter` 3600 (1/hour) | 429 | 13 | | 11 or more | block | 403 | 13 | Without an `overCap`, anything over the cap is throttled until the window resets, with `retryAfter` set to the window length, and never blocked. The cap is checked after the client rule. A client rule that blocks wins. A challenge or allow inside the cap keeps its verdict. ## Verdicts | Verdict | HTTP status | What happens | |---|---|---| | allow | 200 | The request reaches your route. | | challenge | 401 | The request waits on a person: a passkey step. Whoever finishes it becomes the principal. | | throttle | 429 | Rate-limited, not refused. The response carries `Retry-After` (`retryAfter` in seconds). | | block | 403 | The request does not reach your route. Logged with its reason. | ## Agent tiers | Tier | Meaning | How rules treat it | |---|---|---| | verified | A valid Web Bot Auth signature from an operator on `trustedOperators`. | `verified` | | detected | Recognised by user agent but not signed (or signed by an untrusted operator). | Treated as `unknown`. | | unknown | No valid signature from a trusted operator, and not recognised either. | `unknown` | ## The demo policy, decided Every row is real output of `/api/decide` (https://heedeo.com/docs/api.md). | route | client | signed | operator | principal, count | verdict | line | |---|---|---|---|---|---|---| | /signup | human | | | | allow | 6 | | /signup | agent | true | chatgpt.com | maya, 2 | allow | 8 | | /signup | agent | true | chatgpt.com | none | challenge | 9 | | /signup | agent | false | | | challenge | 10 | | /signup | agent | true | agent.example-operator.com | | challenge | 10 | | /signup | agent | true | chatgpt.com | maya, 7 | throttle | 13 | | /signup | agent | true | chatgpt.com | maya, 11 | block | 13 | | /signup | human | | | dave, 12 | block | 13 | | /checkout | agent | true | chatgpt.com | | allow | 15 | | /checkout | agent | false | | | block | 15 | | /docs/auth | agent | false | | | allow | 16 | | /pricing | agent | false | | | challenge | 17 | | /pricing | agent | true | chatgpt.com | | allow | 17 | ```bash curl "https://heedeo.com/api/decide?route=/signup&client=agent&signed=true&operator=chatgpt.com&principal=maya&count=11" curl "https://heedeo.com/api/decide?route=/pricing&client=agent&signed=false" ``` ## A variant that is not in the demo A per-minute cap on checkout, for drop bots: ```ts "/checkout": { agents: { verified: "allow", unknown: "block" }, perPrincipal: "60/min" }, ``` This is not part of the demo policy, so `/api/decide` won't show it. With no `overCap`, the 61st request in a minute for one principal is throttled (429, `retryAfter` 60) until the minute resets. Unknown agents are still blocked by the `unknown` rule first. --- Docs index: https://heedeo.com/docs.md. How to verify our claims: https://heedeo.com/trust.md. Book a call: https://cal.com/heedeo/30min Updated: 2026-10-02 --- # Security and data handling ## Where decisions run Heedeo runs as middleware inside your own app. Verification, principal lookup, counting and the verdict all happen there, before the request reaches your route. ## What leaves your servers Only decision metadata goes to the Heedeo dashboard: - route - verdict - agent - reason - rule line Request bodies are never sent. Reasons never contain principal identifiers: they say "This person is at 7/5 per day on /signup", not who the person is. You can turn the dashboard reporting off. ## Signature verification What the verifier behind `GET https://heedeo.com/api/visitor` checks, in order: - `Signature-Input` and `Signature` parse as RFC 8941 structured fields, and one signature has `tag="web-bot-auth"`. - `Signature-Agent` names an origin that passes the SSRF rules below. - `alg`, if given, is `ed25519`. - The signature covers `@authority` and `signature-agent`. - `created` and `expires` are both present; `created` is not more than 60 seconds in the future; `expires` has not passed (60 seconds of clock skew allowed); `expires` is after `created`; the lifetime is at most 24 hours. - The `keyid` matches the RFC 7638 JWK SHA-256 thumbprint of an Ed25519 (OKP) key in the operator's `/.well-known/http-message-signatures-directory`. - The Ed25519 signature over the RFC 9421 signature base verifies, using WebCrypto. - The signature has not been seen before (replay check, below). - The operator is on `trustedOperators`. Result fields: `present`, `valid` (the signature checks out), `trusted` (the operator is on the list), `verified` (both), `agentOrigin`, `keyId`, `reason`. A valid signature from an untrusted operator returns `valid: true, trusted: false, verified: false` ("Signed, untrusted") and counts as unknown. ```bash # Claims to be ChatGPT, with a made-up key: the real directory is fetched, and the key isn't in it NOW=$(date +%s) curl https://heedeo.com/api/visitor \ -H 'Signature-Agent: "https://chatgpt.com"' \ -H "Signature-Input: sig1=(\"@authority\" \"signature-agent\");created=$NOW;expires=$((NOW+60));keyid=\"test\";alg=\"ed25519\";tag=\"web-bot-auth\"" \ -H 'Signature: sig1=:AAAA:' ``` Real output: ```json {"kind":"agent","name":"ChatGPT agent","signed":true,"verified":false,"verifiedFor":null,"summary":"Signature headers present but not verified (key not found in directory). Classified from the user agent only.","signature":{"present":true,"valid":false,"trusted":false,"verified":false,"agentOrigin":"https://chatgpt.com","keyId":"test","reason":"key not found in directory"}} ``` ## Replay protection A signature that verified once is refused if presented again before it expires, with reason `"replayed signature"`. The cache is keyed on `keyid` plus the signature bytes, and kept until `expires` plus 60 seconds. Stated plainly: on heedeo.com this cache is in memory, per Workers isolate. It is not global. A replay that lands on a different or freshly started isolate is not caught, and when the cache is full its oldest entries are dropped. How replay state is shared in your deployment is agreed during onboarding. ## SSRF-safe key fetching Fetching a key directory means making an outbound request to an origin named by the caller, so the fetch is restricted: - https only, port 443 only, no credentials in the URL. - A hostname, not an IP literal. Single-label names and names under `localhost`, `local`, `internal`, `intranet`, `lan`, `home`, `corp`, `localdomain`, `home.arpa`, `test`, `invalid` and `example` are refused. - Only `/.well-known/http-message-signatures-directory` on that origin is fetched. - Redirects are not followed. The fetch times out after 2 seconds. Directories over 64 KiB are refused. - Directories are cached for 10 minutes. A failed fetch is cached for 60 seconds. Cloudflare Workers can't resolve DNS before a fetch, so a public name that resolves to a private address is not caught by these checks. heedeo.com also runs with Cloudflare's `global_fetch_strictly_public` compatibility flag, which keeps Worker fetches on the public internet. ## Failure modes | Situation | Result | |---|---| | No signature headers | not verified: detected if the user agent is recognised, otherwise unknown | | Signature malformed, expired or not matching | not verified | | Key directory unreachable, timed out, redirected, too large or not JSON | not verified (treated as unsigned) | | Key not in the directory | not verified | | Signature replayed | not verified | | Valid signature, operator not trusted | not verified ("Signed, untrusted") | Every failure means unknown to your rules, never verified. What unknown agents may do is up to your rules: on the demo policy, a passkey challenge on `/signup` and a block on `/checkout`. Where per-principal counters live, and whether a counter outage fails open or closed, are agreed during onboarding. ## Limits worth knowing - A passkey does not prove a unique human, and neither does an email. Linking one person's several accounts relies on signed principal tokens when present, plus your own signals (payment card, verified email domain, device), set up at onboarding. - Agents that don't sign are never verified. Recognising one by its user agent makes it detected, which rules treat as unknown. See also: https://heedeo.com/docs/how-it-works.md and, for what heedeo.com itself collects, https://heedeo.com/privacy.md. --- Docs index: https://heedeo.com/docs.md. How to verify our claims: https://heedeo.com/trust.md. Book a call: https://cal.com/heedeo/30min Updated: 2026-10-02 --- # Public API on heedeo.com These endpoints are live on heedeo.com so you can check how Heedeo behaves without signing up. The machine-readable spec is at https://heedeo.com/openapi.json. `/api/visitor`, `/api/decide` and `/api/policy` send `Access-Control-Allow-Origin: *`. ## GET /api/visitor Classifies the caller and really verifies Web Bot Auth signatures. See https://heedeo.com/docs/security.md for every check. ```bash curl https://heedeo.com/api/visitor ``` Real output for plain curl: ```json {"kind":"agent","name":"script","signed":false,"verified":false,"verifiedFor":null,"summary":"Not signed. Classified from the user agent only.","signature":{"present":false,"valid":false,"trusted":false,"verified":false,"agentOrigin":null,"keyId":null,"reason":"no signature headers"}} ``` | Field | Meaning | |---|---| | `kind` | `"human"`, `"agent"` or `"crawler"`. | | `name` | The recognised agent, for example `"ChatGPT agent"`, or `"script"` for plain HTTP clients like curl. | | `signed` | Signature headers are present. Not a verification. | | `verified` | A valid signature and a trusted operator. | | `verifiedFor` | The operator origin when verified, otherwise null. | | `summary` | One sentence on how the request was classified. | | `signature.present` | Signature headers were present. | | `signature.valid` | The signature checked out against the operator's published key, and was not a replay. | | `signature.trusted` | The operator is on `trustedOperators`. | | `signature.verified` | `valid` and `trusted`. | | `signature.agentOrigin` | The operator origin from `Signature-Agent`. | | `signature.keyId` | The `keyid` the signature names. | | `signature.reason` | Why it did or didn't verify, for example `"no signature headers"`, `"expired"`, `"key not found in directory"`, `"replayed signature"`. | A request recognised by user agent but not verified (`name` set, `verified` false) is what the docs call **detected**; rules treat it as unknown. ## GET or POST /api/decide Runs the real policy engine on the demo policy (https://heedeo.com/docs/rules.md). Input, as query parameters (GET) or a JSON body (POST): | Field | Required | Meaning | |---|---|---| | `route` | yes | A path starting with `/`, up to 512 characters. Normalized before matching. | | `client` | yes | `"human"` or `"agent"`. | | `signed` | no | `true` when the Web Bot Auth signature is valid. Accepts `true`, `false`, `1`, `0`. | | `operator` | no | The signing operator's host, for example `chatgpt.com`. `signed=true` counts as verified only if the operator is on `trustedOperators`. | | `principal` | no | Who the request acts for, up to 120 characters. Echoed back, never put in `reason`. | | `count` | no | Requests by this principal on this route in the current window, counting this one. A whole number. Needs `principal`. | | `method` | no | An HTTP method. Accepted; the demo policy has no per-method rules. | Response fields: `verdict`, `status` (200, 401, 429 or 403), `retryAfter` (seconds, throttle only, otherwise null), `reason`, `rule` (always set: `path`, `line`, `text`), `principal` (echoed), `policy` (`"demo"`) and `note`. ```bash curl "https://heedeo.com/api/decide?route=/signup&client=agent&signed=true&operator=chatgpt.com&principal=maya&count=7" ``` Real output: ```json {"verdict":"throttle","status":429,"retryAfter":3600,"reason":"This person is at 7/5 per day on /signup, over the cap. Rate-limited to 1/hour up to 10, then blocked.","rule":{"path":"/signup","line":13,"text":"overCap: { throttle: 5, rate: \"1/hour\", then: \"block\" },"},"principal":"maya","policy":"demo","note":"Same engine and rules shown on heedeo.com. Your own rules are set up during onboarding."} ``` A valid signature from an operator that isn't trusted: ```bash curl "https://heedeo.com/api/decide?route=/signup&client=agent&signed=true&operator=agent.example-operator.com" ``` ```json {"verdict":"challenge","status":401,"retryAfter":null,"reason":"Unknown agent on /signup: challenge. The signature is valid, but agent.example-operator.com is not in trustedOperators, so the agent counts as unknown.","rule":{"path":"/signup","line":10,"text":"unknown: { challenge: \"passkey\" },"},"principal":null,"policy":"demo","note":"Same engine and rules shown on heedeo.com. Your own rules are set up during onboarding."} ``` POST works the same: ```bash curl -X POST https://heedeo.com/api/decide \ -H "Content-Type: application/json" \ -d '{"route":"/checkout","client":"agent","signed":false}' ``` ```json {"verdict":"block","status":403,"retryAfter":null,"reason":"Unknown agent on /checkout: block. The request is not signed.","rule":{"path":"/checkout","line":15,"text":"\"/checkout\": { agents: { verified: \"allow\", unknown: \"block\" } },"},"principal":null,"policy":"demo","note":"Same engine and rules shown on heedeo.com. Your own rules are set up during onboarding."} ``` Bad input returns 400 with `ok: false`, an `error` and a `help` string. For example, a `count` without a `principal` returns the error `"count" needs a "principal": the cap is counted per principal.` ## GET /api/policy Returns the demo policy: `source` (the 18 lines that `rule.line` refers to) and `rules` (the same, parsed). ```bash curl https://heedeo.com/api/policy ``` ## POST /api/book Request a call for a person. Only `email` is required. ```bash curl -X POST https://heedeo.com/api/book \ -H "Content-Type: application/json" \ -d '{"email":"you@example.com","via":"agent","agent":"Your agent name","site":"example.com","pain":"what agents are doing on the site"}' ``` | Field | Type | Meaning | |---|---|---| | `email` | string, required | The person who wants access. | | `site` | string | Their site. | | `pain` | string | What agents are doing on their site. | | `via` | `"human"` or `"agent"` | `"agent"` when an agent asks for someone. Defaults to `"human"`. | | `agent` | string | The agent's name, up to 80 characters. | - Requests from agents, and from plain scripts like curl, stay `pending` until the person confirms. - Success is HTTP 200 with `ok`, `status`, `existing` and a `message` you can pass on as is. Errors are HTTP 400 with an `error` field. - `GET /api/book` returns 405 with `Allow: POST`. - `POST /api/waitlist` is an older alias with the same contract. ## Documents | Path | Content | |---|---| | `GET /openapi.json` | OpenAPI spec for `/api/book`, `/api/waitlist`, `/api/decide`, `/api/policy` and `/api/visitor` | | `GET /index.md` | The landing page as markdown. `GET /` with `Accept: text/markdown` returns the same. | | `GET /llms.txt` | Map of the site for language models | | `GET /llms-full.txt` | llms.txt plus every doc, in one file | | `GET /docs.md` | Docs index | --- Docs index: https://heedeo.com/docs.md. How to verify our claims: https://heedeo.com/trust.md. Book a call: https://cal.com/heedeo/30min Updated: 2026-10-02 --- # FAQ **What does Heedeo do?** It identifies every agent on your site and lets you decide who gets in and what they can do. Per route, it verifies the agent, identifies who it acts for, counts per principal and applies your rule: allow, challenge, throttle or block. See https://heedeo.com/docs/how-it-works.md. **How is that different from a bot blocker or a CAPTCHA?** It doesn't try to keep agents out. Agents that bring real customers get through. It asks agents to show who built them (a signature) and who they act for (a principal), and it limits the person, not the IP. People never see a puzzle. The only thing a person sees is one passkey confirmation, when an unknown agent signs up on their behalf. **Which agents can be verified?** Agents that sign with Web Bot Auth, from an operator on your `trustedOperators` list. Known signers today include ChatGPT agent, Goose, Browserbase and Anchor Browser, per https://blog.cloudflare.com/signed-agents/. The demo policy trusts `chatgpt.com`, `browserbase.com` and `anchorbrowser.io`. **What is a "detected" agent?** One recognised by its user agent but not signed (or signed by an untrusted operator). Rules treat it as unknown. **Can an agent fake being ChatGPT?** It can fake the user agent string, which makes it detected at most. It can't fake the signature: only a valid Ed25519 signature checked against a key in the operator's `/.well-known/http-message-signatures-directory`, from a trusted operator, makes an agent verified. **Why isn't a valid signature enough?** Anyone can publish a key directory on a domain they own and sign with it. So verified also needs the operator to be on your trusted list. A valid signature from anyone else is "Signed, untrusted" and counts as unknown. **Who is the principal?** The person or company the agent acts for. From a signed principal token when present (for example Skyfire KYAPay or Visa Trusted Agent Protocol). Otherwise, whoever finishes the passkey handoff. After signup, your own user ID. **Does a passkey stop one person making many accounts?** Not on its own. A passkey proves a real person finished the step, not a unique human: one person can hold several passkeys. Linking one person's several accounts relies on signed principal tokens when present, plus your own signals (payment card, verified email domain, device), set up at onboarding. **Why per principal and not per IP?** IPs are cheap to rotate. A farm behind 40 IPs that resolves to one principal gets one set of limits. **What does throttle mean?** Rate-limited, not refused: HTTP 429 with `Retry-After`. On the demo `/signup`, the 6th to 10th signups a day for one principal are limited to 1 an hour. From the 11th, block (403). **Does my traffic leave my servers?** Decisions run in your middleware. Only decision metadata (route, verdict, agent, reason, rule line) goes to the dashboard. Never request bodies, and reasons never contain principal identifiers. You can turn it off. See https://heedeo.com/docs/security.md. **What if an operator's key directory is down?** The request is treated as unsigned, so the agent is unknown. It is never treated as verified. **Where are the per-principal counters kept?** Where they live in your deployment, and whether a counter outage fails open or closed, are agreed during onboarding. **Which frameworks are supported?** Next.js middleware today. Express, Hono and Cloudflare Workers on request. **Is it on npm?** Not on public npm. Access to the package is provided at onboarding. **How do I get access?** Book a short call at https://cal.com/heedeo/30min, or have your agent `POST https://heedeo.com/api/book` for you. After the call the team gives you access to the package and sets up your first rules with you. **Can I try the engine before a call?** Yes. `/api/decide` runs the real policy engine on a demo policy, and `/api/visitor` really verifies signatures. See https://heedeo.com/docs/api.md. **Who is behind Heedeo?** Heedeo is backed by Entrepreneurs First (https://www.joinef.com). **How can I check what you say here?** https://heedeo.com/trust.md lists each checkable claim with the curl to check it. --- Docs index: https://heedeo.com/docs.md. How to verify our claims: https://heedeo.com/trust.md. Book a call: https://cal.com/heedeo/30min Updated: 2026-10-02 --- # How to verify our claims Each claim below comes with a command you can run. If a command disagrees with this page, the command is right and this page is wrong. ## Checkable now - **heedeo.com really verifies Web Bot Auth signatures, against the operator's real key directory.** Claim to be ChatGPT with a made-up key: ```bash NOW=$(date +%s) curl https://heedeo.com/api/visitor \ -H 'Signature-Agent: "https://chatgpt.com"' \ -H "Signature-Input: sig1=(\"@authority\" \"signature-agent\");created=$NOW;expires=$((NOW+60));keyid=\"test\";alg=\"ed25519\";tag=\"web-bot-auth\"" \ -H 'Signature: sig1=:AAAA:' ``` Expect `signature.present` true, `signature.verified` false, `signature.reason` `"key not found in directory"`. Change `created` and `expires` to past times (for example 1700000000 and 1700000300) and the reason becomes `"expired"`. - **A user agent string is not proof.** Claim to be ChatGPT with no signature: ```bash curl https://heedeo.com/api/visitor -A "ChatGPT-Agent" ``` Expect `name` `"ChatGPT agent"` (detected) with `verified` false and `signature.reason` `"no signature headers"`. - **A valid signature is not enough; the operator must be trusted.** ```bash curl "https://heedeo.com/api/decide?route=/signup&client=agent&signed=true&operator=agent.example-operator.com" ``` Expect `verdict` `"challenge"` and a reason saying the operator "is not in trustedOperators, so the agent counts as unknown". - **The policy engine is real and the demo policy is public.** Read the policy, then run requests through it: ```bash curl https://heedeo.com/api/policy curl "https://heedeo.com/api/decide?route=/signup&client=agent&signed=true&operator=chatgpt.com&principal=maya&count=2" # allow, 200, line 8 curl "https://heedeo.com/api/decide?route=/signup&client=agent&signed=true&operator=chatgpt.com&principal=maya&count=7" # throttle, 429, retryAfter 3600, line 13 curl "https://heedeo.com/api/decide?route=/signup&client=agent&signed=true&operator=chatgpt.com&principal=maya&count=11" # block, 403, line 13 curl "https://heedeo.com/api/decide?route=/signup&client=agent&signed=false" # challenge, 401, line 10 curl "https://heedeo.com/api/decide?route=/checkout&client=agent&signed=false" # block, 403, line 15 curl "https://heedeo.com/api/decide?route=/docs/../checkout&client=agent&signed=false" # block, 403, line 15 (normalized) ``` Each response names the policy line (`rule.line`, `rule.text`) that decided it. Check it against `source` from `/api/policy`. - **The decision logic is per principal, and reasons don't leak who.** Given a principal and a count, `/api/decide` shows what the rules do with them. The `reason` says "This person is at 7/5 per day on /signup", and the principal only comes back in its own `principal` field, echoed from your input. What this does not show: identifying the principal and keeping the count happen in the package, inside your middleware. That part is shown on the call. - **The booking API is what the docs say.** Without creating anything: ```bash curl -i https://heedeo.com/api/book # 405, Allow: POST curl -i -X POST https://heedeo.com/api/book -d 'nope' # 400 with an "error" field ``` - **The site serves markdown to agents.** ```bash curl -sI https://heedeo.com/docs.md | grep -i content-type # text/markdown curl -s -H "Accept: text/markdown" https://heedeo.com/ | head -3 # the page as markdown ``` - **The standards are real and public.** RFC 9421: https://www.rfc-editor.org/rfc/rfc9421. Cloudflare's signed agents list (ChatGPT agent, Goose, Browserbase, Anchor Browser): https://blog.cloudflare.com/signed-agents/ ## Stated, not checkable by curl - Backed by Entrepreneurs First (https://www.joinef.com). Ask us on the call. - In your app, only decision metadata (route, verdict, agent, reason, rule line) reaches the dashboard, never request bodies. You can confirm this on your own network once you have the package. ## Known limits of the public demo - Replay protection on heedeo.com is an in-memory cache per Workers isolate, not global. A replay that reaches another isolate is not caught. See https://heedeo.com/docs/security.md. - `/api/decide` takes `signed` and `operator` as inputs; it does not check a signature itself. `/api/visitor` is the endpoint that verifies. ## What is not public yet - **The package.** `@heedeo/next` is not on public npm. Access is provided at onboarding. - **Customers.** There is no public customer list, and we don't publish logos or numbers. - **Pricing.** There is no public price list. - **Certifications.** We claim none. - **Deployment details.** Where per-principal counters live, how replay state is shared, and whether outages fail open or closed are agreed during onboarding. - **Policy keys beyond the ones in** https://heedeo.com/docs/rules.md: set up with you at onboarding. - **A contact email.** The way to reach us is a call (https://cal.com/heedeo/30min) or `POST https://heedeo.com/api/book`. --- Docs index: https://heedeo.com/docs.md. How to verify our claims: https://heedeo.com/trust.md. Book a call: https://cal.com/heedeo/30min Updated: 2026-10-02 --- # Privacy on heedeo.com This page covers heedeo.com, the website. For what the Heedeo product sends from your app, see https://heedeo.com/docs/security.md. ## What we collect Only what you send in a call request, made through the form on the site or `POST /api/book`: - your email - your site, if you give it - your note about what agents are doing on your site (the `pain` field), if you give it - whether a person or an agent submitted the call request, and the agent's name if one was given or recognised from the user agent - whether the request carried Web Bot Auth signature headers (presence only) - the status of the call request (pending or confirmed) and timestamps: when it was created, when it was confirmed, and when an agent last submitted it ## Where it is stored In a Cloudflare D1 database. The site runs on Cloudflare Workers, and Cloudflare processes requests to it as our host. ## What we use it for Only to contact you about access to Heedeo. ## What we don't do - No analytics scripts, no advertising trackers and no cookies set by the site. - `GET /api/visitor` and `/api/decide` compute their answer and return it. They don't store what you send. To check signatures, `/api/visitor` keeps operators' public key directories and recently seen signatures in memory for a short time; see https://heedeo.com/docs/security.md. ## Agents submitting for you A call request made by an agent is stored as pending until you confirm, by booking yourself or when we check with you. ## Removing your data To have your call request deleted, ask us on the call or reply to the email we send you. --- Docs index: https://heedeo.com/docs.md. How to verify our claims: https://heedeo.com/trust.md. Book a call: https://cal.com/heedeo/30min Updated: 2026-10-02