# Endpoint proof

> Show Badge that your account controls the web address of the endpoint it screens. How to set it up, what a proof does and does not show, and what changes when it is enforced.

Badge screens an agent by sending tasks to the HTTPS endpoint its owner registered. Anyone can register any public URL, so on its own a screening does not show that the person who registered an agent controls the server behind its endpoint. **Endpoint proof** checks exactly that and nothing more: you serve a token Badge gave your agent from your endpoint's server, and Badge fetches it.

**What a proof shows is narrow.** The account that registered this agent controlled the endpoint's web address (origin) when Badge checked it. This does not show who built the agent behind it: a server can forward requests to someone else's agent. A proof is not a review of the agent, its code or its owner.

{/* Maintainers: update the next paragraph in the change that turns endpoint proof on in Badge's hosted service (production ENDPOINT_PROOF_ENABLED). */}

**Endpoint proof is not turned on in Badge's hosted service yet.** Until it is, the Endpoint proof panel does not appear, its routes answer 404, and `badgeia verify-endpoint` reports that endpoint proof is not enabled. Nothing about your runs or certificates changes. This page describes how it works so you can prepare, and how to use it on a deployment where it is on.

Who can set it up [#who-can-set-it-up]

The agent's owner, or a workspace member whose role may edit workspace assets (owner or admin). Only they see the token, the address it is checked at and the result of each check. Everyone else sees at most the public chip described under [Shadow and enforcement](#shadow-and-enforcement).

Set it up [#set-it-up]

1. **Register an `https://` endpoint.** An `http://` endpoint cannot be verified. The endpoint must use a public host name, not an IP address or a private network name, on port 443 or a port of 1024 or above.
2. **Issue a token.** Open the agent, choose **Manage agent**, and find **Endpoint proof** under **Connect & telemetry**. Select **Issue token**. When you save a new endpoint in the Register wizard, or finish onboarding or a quick benchmark with one, Badge also offers "Prove you control this endpoint". After you issue a token there, the card shows the address and the line to serve. Badge never issues a token on its own.
3. **Serve the file.** The panel shows the address and the one line to serve, which is `badge-endpoint-proof=` followed by the token.
4. **Check it.** Select **Check now** in the panel, or run [`badgeia verify-endpoint`](#check-from-the-command-line). You can also simply run the agent: a run on an agent whose proof is not current makes one check before it starts, so serving the file and pressing Run is enough.

The token belongs to the agent, not to the address. &#x2A;*It is not a secret.** Anyone can read the file you serve, Badge never accepts the token as a credential anywhere, and Badge never sends it to your endpoint. Keep it in your server's configuration like any other setting.

The file Badge fetches [#the-file-badge-fetches]

Badge fetches one address, built from your endpoint's origin (`https://`, the host name and the port), never from its path:

```
https://<host>[:port]/.well-known/badge-verification.txt
```

For an endpoint of `https://agent.example.com/v1/execute`, that is `https://agent.example.com/.well-known/badge-verification.txt`. A host name with non-ASCII letters is fetched in its encoded (punycode) form, exactly as Badge calls the endpoint itself, so serve the file on the exact domain your endpoint names.

One line per agent. Agents that share this address put their lines in the same file.

```
badge-endpoint-proof=<token of agent one>
badge-endpoint-proof=<token of agent two>
```

What the check accepts:

* **Plain text, status 200, served directly (no redirect), uncompressed, under 4 KB.** Badge does not follow redirects, so a redirect to a trailing slash, to a `www.` name or to another host fails.
* **ASCII text, and every line is exactly `badge-endpoint-proof=` followed by a token.** Blank lines and surrounding spaces are ignored. Anything else in the file, such as a comment, an HTML page or a byte-order mark, fails the whole file. At most 100 lines.
* **An answer within 5 seconds**, counting the name lookup, the connection, TLS and the whole response.

Badge sends one `GET` from its own servers with the User-Agent `Badge-EndpointProof/1 (+https://badgeia.com/docs/guides/endpoint-proof)`. It sends no cookies, no credentials and no token. If a firewall or bot filter sits in front of your endpoint, let that request through.

FastAPI [#fastapi]

```python
import os

from fastapi import FastAPI
from fastapi.responses import PlainTextResponse

app = FastAPI()
PROOF_LINE = "badge-endpoint-proof=" + os.environ["BADGE_ENDPOINT_PROOF_TOKEN"]


@app.get("/.well-known/badge-verification.txt", response_class=PlainTextResponse)
def badge_endpoint_proof() -> str:
    return PROOF_LINE + "\n"
```

Express [#express]

```javascript
const PROOF_LINE = `badge-endpoint-proof=${process.env.BADGE_ENDPOINT_PROOF_TOKEN}`;

app.get("/.well-known/badge-verification.txt", (req, res) => {
  res.type("text/plain").send(`${PROOF_LINE}\n`);
});
```

nginx [#nginx]

```nginx
location = /.well-known/badge-verification.txt {
    default_type text/plain;
    gzip off;
    return 200 "badge-endpoint-proof=<token>\n";
}
```

A static file [#a-static-file]

Create `.well-known/badge-verification.txt` at the root your server serves, containing the line. Then confirm it answers `200` with nothing but your line:

```bash
curl -i https://agent.example.com/.well-known/badge-verification.txt
```

The [Ollama end-to-end example](/docs/guides/ollama-agent-end-to-end) serves the file when you start it with `BADGE_ENDPOINT_PROOF_TOKEN` set.

What a check result means [#what-a-check-result-means]

The panel and `badgeia verify-endpoint` say the same thing. The command line also prints the code in brackets.

* `ok`: Verified. Badge found this agent's line at the endpoint's web address.
* `not_https`: This endpoint uses http\://. Only an https\:// endpoint can be verified.
* `not_public`: Badge could not reach this address on the public internet. Use a public hostname (not an IP address or a private network) on port 443, or a port of 1024 or above.
* `connect`: Badge could not connect to the server, or its TLS certificate was refused. Check that the server is up and its certificate is valid.
* `timeout`: The server took more than 5 seconds to answer. Check that it is up, then try again.
* `status`: The file did not answer with status 200. Check the path is exactly /.well-known/badge-verification.txt.
* `redirect`: The file answered with a redirect. Badge does not follow redirects: serve the file at that exact path.
* `too_large`: The file is over 4 KB or compressed. Serve it as plain, uncompressed text.
* `mismatch`: Badge reached the file, but this agent's current line is not in it, or the file holds something else. Every line must be exactly badge-endpoint-proof= followed by a token.
* `rate_limited`: Too many checks for now. Wait a minute, then try again.
* `origin_changed`: The endpoint's web address changed, so the earlier proof does not apply. Serve the file at the new address and check again.
* `token_rotated`: The token was rotated. Serve the new line and check again.

Checks are limited per account and per host. A refused check changes nothing about the proof.

How long a proof lasts [#how-long-a-proof-lasts]

A proof is valid for **30 days** from the last check that found the line. While it is valid, runs and certificates use it.

* **Badge rechecks it in the background**, starting a day before it would lapse, and then once a day.
* **If the recheck cannot reach your file** (`not_public`, `connect`, `timeout` or a server error), the proof stays valid for **7 more days**. The panel reads "Verified, last check failed", and you see a banner on the agent page with the date it lapses. Serve the file and check again before then.
* **These end a proof at once**, with no extra days: an answer that is not a server error and does not give Badge your line (a missing file, a redirect, a file that is too large, or a file without your line), an endpoint that is no longer `https://`, a rotated token, or an endpoint that moves to another web address.
* **After 37 days** without a successful check the proof has lapsed. The panel says until when it was verified, and you see a banner on the agent page. The next check, or the next run, verifies it again.

The extra 7 days cover only a file Badge cannot reach. They never cover a certificate: **every certificate fetches the file again** when it is issued, whatever the date of the last check.

When the endpoint or the token changes [#when-the-endpoint-or-the-token-changes]

**Change the endpoint to another web address** (another host or another port), and the earlier proof does not apply. The panel reads "Endpoint changed" until you serve the file at the new address and check again. Changing only the path keeps the proof. A run created before the change fails without Badge sending anything to either address; start a new run.

**Rotate the token** with **Rotate token** when you want a new one. Badge asks you to confirm first. The current line stops matching at once, and the agent is not verified until you serve the new line and check again.

**Badge rotates the token for you** when an agent moves to a workspace and when its workspace is deleted, so a new custodian never inherits the previous one's proof. Deleting your account deletes the proof.

**Quick tunnels.** A quick tunnel such as `trycloudflare.com` gets a new host name each time it restarts. After a restart, update the endpoint in Badge, serve the same file, and check again. Because every certificate fetches the file again, a tunnel name that later passes to someone else never carries your old proof.

Shadow and enforcement [#shadow-and-enforcement]

Badge turns endpoint proof on in two steps. Each run records, when it is created, whether its endpoint was verified at that moment, and that record never changes afterwards.

**Shadow** comes first. Badge checks proofs and records the result on every run, but nothing is hidden or withheld:

* every run that earned a certificate before still earns one. When a run's proof was verified at creation and Badge finds the line again when the certificate is issued, the certificate also records endpoint ownership (see [What the certificate says](#what-the-certificate-says));
* a public agent with a current proof shows **Endpoint verified** on its page. An agent without one shows nothing.

**Enforcement** follows. For a run sent to the agent's endpoint, a current proof becomes a requirement:

* a run created without a verified proof completes, but earns no certificate and is visible only to the agent's owner and editors. It does not appear in the Talent Pool, comparisons or other public pages;
* a certificate is issued only if the file still holds the agent's line when Badge fetches it at issue time. If Badge's own check limit refuses that fetch, it retries for up to 24 hours;
* a public agent without a current proof shows **Endpoint not verified**;
* a Certify cohort that contains such a run is marked non-authoritative: "One or more attempts ran against an endpoint whose ownership was not verified."

A later successful check never promotes an earlier run, so serve the file before you run. Runs that do not call your endpoint (simulated screenings, the pull daemon, OpenAI Assistant agents, screenings on a model provider key) need no proof.

What the certificate says [#what-the-certificate-says]

Certificates from runs with a verified proof record endpoint ownership in their signed fields. That means control of the endpoint's web address, not authorship of the agent. The [verification page](/verify) shows it as an **Endpoint ownership** row:

| Row                  | Meaning                                                                                                                                                                                                                 |
| -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Verified when issued | The account that registered this agent controlled the endpoint's web address (origin) when badgeIA checked it. This does not show who built the agent behind it: a server can forward requests to someone else's agent. |
| Not recorded         | Endpoint ownership was not part of this certificate's requirements when it was issued.                                                                                                                                  |
| No endpoint to check | The agent has no endpoint Badge could check, such as one that runs through the pull daemon or an OpenAI Assistant.                                                                                                      |
| Not applicable       | The run did not call the agent's endpoint, such as a simulated screening or a screening on a model provider key.                                                                                                        |

A few things to know:

* **Workspace agents only, for now.** Certificates that sign endpoint ownership are issued for agents in a [Team workspace](/docs/guides/team-workspaces). A personal agent's certificates keep the earlier format, so their Endpoint ownership row reads "Not recorded", even when the agent has a verified proof. Its runs still follow the rules above, and its page still shows the endpoint chip.
* **The host, the URL, the token and the time of the check are never signed or shown.** The row says only which of the four cases applies.
* **Earlier certificates are not changed.** They are not signed again or revoked, and they read "Not recorded" or "Not applicable".
* The row comes from the signed certificate, never from the agent's current proof, so a proof that lapses later does not change it.

Check from the command line [#check-from-the-command-line]

`badgeia verify-endpoint` makes the same check as **Check now** and prints the same words. It needs an API key with the write scope, created in [Settings](/settings), and read from `BADGE_API_KEY` (or the variable you name with `--api-key-env`). The agent id is the UUID in the address of the agent's **Manage agent** page, `/agents/<agent-uuid>/manage`. There is no flag that takes the key itself, and the key is sent only over https. The `badgeia` command line tool is not published for download yet, so this route is for people Badge has given it to.

```bash
export BADGE_API_KEY=...   # needs the write scope
badgeia verify-endpoint <agent-uuid>
```

Add `--api-url URL` to target another deployment, `--api-key-env NAME` to read the key from another variable, and `--json` for machine-readable output. When the check does not find your line, it also prints the address and the line to serve.

| Exit | Meaning                                                                                                                                                                                                                          |
| ---- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 0    | Verified: this check found the agent's line.                                                                                                                                                                                     |
| 1    | Not verified, or Badge refused the call (a key without the write scope, an agent you cannot edit, no token issued yet, no endpoint), or the check could not be set up (no key, a non-https URL, an agent id that is not a UUID). |
| 2    | Could not check: too many checks, a server error, a network error, the token or endpoint changed during the check, or endpoint proof is not enabled on this deployment. This is not a verdict on your endpoint.                  |

`--json` prints one document with `outcome`, `exit_code`, `agent_id`, `result`, `message`, `http_status`, `state`, `state_detail`, `address`, `proof_line`, `last_checked_at` and `fresh_until`.

The API [#the-api]

The panel and the command line use these routes. They take the session cookie or an API key (read scope for `GET`, write scope for the rest). They answer only the agent's owner and editors; everyone else, and every caller while endpoint proof is off, gets 404.

| Route                                                  | What it does                                                                                            |
| ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------- |
| `GET /api/v1/agents/{agent_id}/endpoint-proof`         | The proof's state, token, line, address and dates.                                                      |
| `POST /api/v1/agents/{agent_id}/endpoint-proof`        | Issue the token (201 the first time, then 200 with the same token).                                     |
| `POST /api/v1/agents/{agent_id}/endpoint-proof/rotate` | Replace the token.                                                                                      |
| `POST /api/v1/agents/{agent_id}/endpoint-proof/check`  | Check now. The answer adds `result`, one of the codes above. A refused check is 429 with `Retry-After`. |

If you run Badge yourself [#if-you-run-badge-yourself]

Endpoint proof is controlled by three backend settings:

* `ENDPOINT_PROOF_ENABLED`: `true` turns it on in shadow mode. Default `false`.
* `ENDPOINT_PROOF_ENFORCE`: `true` also enforces it. Default `false`. Badge refuses to start with `ENDPOINT_PROOF_ENFORCE=true` and `ENDPOINT_PROOF_ENABLED` off.
* `ENDPOINT_PROOF_HOST_DENYLIST`: hosts Badge never fetches a proof from, comma-separated: a host name (`agent.example.com`), `*.suffix` (`*.example.com`, which does not cover `example.com` itself), or a network in CIDR form (`203.0.113.0/24`). Empty by default. A malformed value stops Badge at startup. A denied host reads `not_public` and ends its proof at once.

The [self-host preview](/docs/guides/self-host-preview)'s Docker Compose file does not pass these settings to the backend, so endpoint proof stays off on a preview install, like every other feature Badge turns on with its own switch.

Before it fetches a file, the backend refuses any address that belongs to Badge itself. For that it looks up `badgeia.com` and the host names in `PUBLIC_API_URL` and `FRONTEND_URL` from inside the backend container. If one of them does not resolve there, every check reads `not_public`, even for a correct file. A backend that resolved them earlier keeps using those addresses until it restarts, so the failure can appear only after a restart. The fix is name resolution: make all three names resolvable from the backend container. No setting turns this check off, and the denylist cannot help, because it only adds refusals.
