# badge.md — the embeddable reputation badge

> What the API serves and what it costs:
> [api.md](https://describe.net/api.md) ·
> [api.html](https://describe.net/api.html) ·
> how you pay: [auth.md](https://describe.net/auth.md) ·
> the configurator with a live preview: [badge.html](https://describe.net/badge.html).
> This page is only about the badge.

**One URL per wallet, one SVG, free.** The badge is the wallet's global score, the
number of distinct raters behind it, and the date the index was last refreshed —
inside the pixel. You embed it with an `<img>` tag. No script, no iframe, no SDK,
no widget: the browser's image mode does the work, and that is the whole design.

Everything on this page was measured with `curl` against the live API on
**2026-08-30** (build `a6f49ca`). Where a claim was read from the code and not
reproduced live, it says so on its own line — a guide that dresses a reading up
as a measurement is worse than one that admits the difference.

---

## The route — there is exactly one

| Route | Price | Answers |
|---|---|---|
| `GET https://api.describe.net/badge/{wallet}.svg` | **free** | `image/svg+xml`, `HTTP 200`, a 20 px-high badge for that wallet |

There is no per-agent badge, no PNG, no JSON variant and no extension-less form —
`/badge/agent/base/1.svg`, `/badge/{wallet}.png`, `/badge/{wallet}.json` and
`/badge/{wallet}` all answer `404` (measured 2026-08-30). And the badge lives on
**`api.describe.net`, not on the site**:

```
GET https://describe.net/badge/0x715dc035ffb97dd7bb4095c6670138ba05bb4e6d.svg   → 404   (measured 2026-08-30)
GET https://api.describe.net/badge/0x715dc035ffb97dd7bb4095c6670138ba05bb4e6d.svg → 200
```

The site proxies exactly one thing from the API (`/.well-known/agent-card.json`),
and the badge is not it. **A snippet pointing at the wrong host is worse than no
snippet at all** — it renders as a broken image on somebody else's page, which is
the one page where you cannot see it break. Every snippet below uses
`api.describe.net`.

### Why it is free

Not generosity — the same cost rule that draws the free/paid line everywhere
else in this service. A badge is a *cache entry*, not a visitor: one URL serving
the same bytes to everybody, `max-age=3600` at the edge, so a page with a million
views an hour costs this origin roughly one request per CDN location per hour.
Charging would also defeat the point: the badge is how a subject *shows* its
reputation around the web, and every impression links back here.

What stays paid is what it always is — the decomposition behind the number: who
rated, when, in which transaction (`GET /reputation/wallet/{wallet}`, see
[auth.md](https://describe.net/auth.md)). `GET https://api.describe.net/pricing`
is the authority on both, and it carries the reason for each price in the
response.

---

## Copy this

Replace the wallet in **both** places — the image and the link. The example below
is a real wallet and the snippets render today.

### Markdown (docs, READMEs, forum posts)

```markdown
[![reputation on describe.net](https://api.describe.net/badge/0x715dc035ffb97dd7bb4095c6670138ba05bb4e6d.svg)](https://describe.net/agent.html?wallet=0x715dc035ffb97dd7bb4095c6670138ba05bb4e6d)
```

### HTML

```html
<a href="https://describe.net/agent.html?wallet=0x715dc035ffb97dd7bb4095c6670138ba05bb4e6d">
  <img src="https://api.describe.net/badge/0x715dc035ffb97dd7bb4095c6670138ba05bb4e6d.svg"
       alt="reputation on describe.net" height="20">
</a>
```

`height="20"` is not decoration: it reserves the row before the image lands, so
the badge does not shove your paragraph down when it arrives.

### A GitHub README

Same Markdown line as above — but know what GitHub does with it. **GitHub does
not let the reader's browser fetch your images: it rewrites every README image
through its own proxy** (`camo.githubusercontent.com` — verified 2026-08-30 by
fetching a public repository page and finding the rewritten hosts). Two
consequences worth knowing before you file a bug:

- The reader's IP never reaches our CDN. Neither does yours. Whatever this
  service could see, on GitHub it sees less.
- **The refresh cadence stops being ours.** Our `Cache-Control` says one hour;
  what the proxy actually honours is the proxy's business, so a README badge can
  show an older number than the same URL opened directly. The date inside the
  pixel is what tells you which you are looking at — which is exactly why the
  date is inside the pixel.

The link half of the snippet is untouched: the click still lands on the profile,
where every rating has its transaction.

---

## What is inside the pixel

The real response for the wallet above, on 2026-08-30 (`Content-Length: 607`):

```svg
<svg xmlns="http://www.w3.org/2000/svg" width="329" height="20" role="img" aria-label="describe.net: 98.27 · 467 raters · 2026-08-30"><title>describe.net: 98.27 · 467 raters · 2026-08-30</title><clipPath id="r"><rect width="329" height="20" rx="3"/></clipPath><g clip-path="url(#r)"><rect width="98" height="20" fill="#3a3a44"/><rect x="98" width="231" height="20" fill="#7c4dcc"/></g><g fill="#ffffff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" font-size="11"><text x="49" y="14">describe.net</text><text x="213.5" y="14">98.27 · 467 raters · 2026-08-30</text></g></svg>
```

Reading it left to right:

| Piece | What it is |
|---|---|
| `98.27` | The wallet's **global score**, in the canonical display format — two decimals, trailing zeros trimmed (the rule, and the measurement that chose it, are in [api.md](https://describe.net/api.md), *Displaying a score*). The index serves `98.270763`; the pixel shows `98.27`. Cross-checked the same minute against the free `GET /wallets/{wallet}/chains`, which answered `98.270763` and `distinct_raters: 467`. |
| `467 raters` | `distinct_raters` — **how many different counterparties** wrote it, not how many ratings there are. A score without its raters is a rumor, so the badge refuses to show the number alone. *(The label is pluralised, `1 rater` / `2 raters` — read from `describenet/badge.py`, not reproduced live: no one-rater wallet turned up in the sample I pulled from `/leaderboard` and `/feed` on 2026-08-30.)* |
| `2026-08-30` | The date the index was last refreshed for that wallet. **Inside the image on purpose**: an old badge that states its date is honest; one that does not lie with good typography. |
| the label box | Fixed at `describe.net`, 98 px. Not configurable. |
| colours | `#3a3a44` label box, `#7c4dcc` value box, `#5a5a66` when there is no data **or** a warning, `#ffffff` text. |

Height is always 20 px; width grows with the text. The font is a system stack
(`Verdana, Geneva, DejaVu Sans, sans-serif`) because an SVG inside an `<img>`
cannot load a webfont, and embedding one as base64 would cost tens of kilobytes
per badge.

For accessibility the badge ships `role="img"`, a `<title>` and an `aria-label`
carrying the same sentence, so a screen reader gets the number and the date, not
"image". The `<title>` doubles as the tooltip when the SVG is opened directly.

---

## There are no query parameters. None.

Not "none documented" — none accepted. There is no `theme`, no `style`, no
`size`, no `label`, no `color`. The parameters are not ignored quietly by
accident; the handler declares exactly one input, the wallet in the path, and the
OpenAPI description agrees.

Measured, so nobody wastes an afternoon on it:

```bash
curl -s -o a.svg "https://api.describe.net/badge/0x715dc035ffb97dd7bb4095c6670138ba05bb4e6d.svg"
curl -s -o b.svg "https://api.describe.net/badge/0x715dc035ffb97dd7bb4095c6670138ba05bb4e6d.svg?label=hola&style=flat&theme=dark&size=large"
cmp a.svg b.svg && echo identical      # → identical (2026-08-30)
```

**Why there is no dark theme in particular:** an `<img>` never receives the
visitor's `prefers-color-scheme` — the image is fetched by a context that cannot
tell the badge what the page looks like. So instead of shipping a theme switch
that would be wrong half the time, the badge commits to one appearance that works
on light and dark backgrounds: a dark box with light text. If your page needs a
different look, it is CSS on the `<img>` you already control, not a parameter we
could honour correctly.

---

## Which addresses it accepts

| You pass | Result | Measured |
|---|---|---|
| EVM `0x` + 40 hex | `200` | yes |
| the same address in **UPPERCASE** | `200`, byte-identical SVG | yes — `cmp` against the lowercase one |
| EVM 40 hex **without** the `0x` prefix | `200`, byte-identical SVG | yes |
| Solana base58 (32–44 chars, case-**sensitive**) | `200` | yes |
| anything else | `422` `{"detail":{"error":"not_an_address","wallet":"pepito"}}` | yes |

The `422` renders as a broken image on the page of whoever pasted the snippet
wrong — and that is the right failure. The alternative, guessing at a malformed
string, would mean serving somebody else's badge under your name.

**The configurator at [badge.html](https://describe.net/badge.html) is stricter
than this route, deliberately.** Its input demands `^0x[0-9a-fA-F]{40}$` or a
base58 string and rejects the prefix-less form the API would accept. That is not
a bug in either place: the API is permissive because `normalize_address` repairs
the input; the form is strict because an error you see while typing costs
nothing, and an error you discover as a broken image on a published page costs
you the page.

---

## Caching, and what to do with the ETag

Real headers, 2026-08-30:

```
HTTP/1.1 200 OK
Content-Type: image/svg+xml
Content-Length: 607
Cache-Control: public, max-age=3600, stale-if-error=604800
ETag: "3acdba36870db1e4"
access-control-allow-origin: *
Api-Version: 2.0.0
X-Cache: Hit from cloudfront
```

- **`max-age=3600`** matches the indexer: it runs hourly (`indexer_period_seconds`
  in `GET /health`, which is the authority). A longer TTL would serve stale data;
  a shorter one would pay for origin requests with no new data to show.
- **`stale-if-error=604800`** — a week. If the index is unreachable, the edge
  keeps serving the last good badge instead of a hole in your page: *a dated old
  image beats a broken one*. (Read from the header; not reproduced by taking the
  origin down.)
- **`Access-Control-Allow-Origin: *`** — an `<img>` needs no CORS, but a page that
  legitimately wants to draw the badge into a canvas or fetch it does.
- The `ETag` is a hash of the SVG. If the index did not move, next hour's refetch
  is a `304` with an empty body:

```bash
curl -s -o /dev/null -w '%{http_code} %{size_download}\n' \
  -H 'If-None-Match: "3acdba36870db1e4"' \
  "https://api.describe.net/badge/0x715dc035ffb97dd7bb4095c6670138ba05bb4e6d.svg"
# → 304 0   (2026-08-30)
```

Copy the ETag from **your own** response, not from this page: it changes the
moment the number, the rater count or the date changes.

Every response also carries a `Ratelimit-Policy` header. Read it live rather than
memorising a number — and note that with an hour of edge caching, a page with
real traffic almost never reaches the origin at all.

---

## Solana works exactly the same

No separate route, no `?network=`. Solana ratings are written into the same
tables as every EVM chain, so the same handler answers:

```bash
curl -s "https://api.describe.net/badge/DevFFyNWxZPtYLpEjzUnN1PFc9Po6PH7eZCi9f3tTkTw.svg"
# → 200, aria-label "describe.net: 54.44 · 6 raters · 2026-08-30"
```

Base58 is case-sensitive, so a Solana address must be pasted exactly. The
tolerance for uppercase applies to EVM hex only.

---

## A warning travels inside the pixel

Since **2026-08-30** (commit `a6f49ca`), a wallet that carries a public caveat is
dressed differently: the warning becomes text in the value box and the violet
turns muted grey.

```bash
curl -s "https://api.describe.net/badge/0x000000000000000000000000000000000000dEaD.svg"
```

```
aria-label="describe.net: 60.75 · burn address · 10 raters · 2026-08-30"
value box fill="#5a5a66"   (muted, not the #7c4dcc violet)
```

Today the only such caveat is `burn-address`: three well-known burn addresses
that nobody controls, that anybody can rate, and that nobody can answer for.
**The ratings are still served** — they are real on-chain facts, and deciding
which addresses are "real" is exactly the verdict this index refuses to issue.
What we owe you is the ability to *tell the difference*, so the badge shows the
number and the warning together.

The reason this fix exists is worth stating plainly: the badge is the surface
designed to be pasted on other people's pages — **the one with the least
context** — and it was the last door still serving that score clean. The full,
machine-readable caveat list for a wallet is the `caveats[]` array of the free
`GET /wallets/{wallet}/chains`; branch on `caveats[].code` (`"burn-address"`),
which is stable, rather than on `caveats[].text`, which is prose *(and which
ships in Spanish today — measured 2026-08-30)*.

---

## Known limits

Said out loud, because a reputation service that hides its own limits has no
standing to decompose anybody else's.

**1. The badge cannot tell "never registered" from "registered but never rated."**
Both render the same image:

```bash
curl -s "https://api.describe.net/badge/0x000000000000000000000000000000000000beef.svg"
# → 200, aria-label "describe.net: sin calificaciones · 2026-08-30", fill #5a5a66
```

Twenty pixels of height do not have room for the distinction, and inventing a
shorter word for it would have made the two facts *look* like one. **The route
that does distinguish them is free**:

```bash
curl -s "https://api.describe.net/wallets/0x000000000000000000000000000000000000beef/chains"
# → chains: [], global_score: null      ← no identity anywhere (measured 2026-08-30)
```

A wallet that *is* registered comes back with populated `chains[]` and
`final_score: null` per chain that has no ratings yet. `[]` and `null` are
different answers; the badge collapses them, this route does not.

**2. `null` is never printed as `0`.** The empty state says "no ratings", never
zero — no data is not bad data, and a zero would be a verdict we did not measure.
The mirror image also holds, and it is pinned by a test
(`tests/test_badge.py:75-79`): a **real** zero — a wallet actually rated `0` by
somebody — renders as `0 · 2 raters`, not as the empty state. Absence and zero
never get confused in either direction.

**3. The empty-state string is Spanish today.** It renders literally as
`sin calificaciones` while the label and the rest of this surface are English
(measured 2026-08-30). If you parse the `aria-label` — and a screen reader does —
that is the string you will get.

**4. The score is one number over every chain.** A wallet with a strong score on
one chain and nothing anywhere else shows one figure. The per-chain split is free
at `GET /wallets/{wallet}/chains`, and the click-through profile shows it.

**5. Never decide on the badge.** It is a display surface, not an oracle. It
carries no `policy_version`, no concentration, no self-rating gap, no campaign
signal — the five reading rules in [api.md](https://describe.net/api.md) need
fields the badge has no room for. Use it to *show* reputation; buy the
decomposition to *act* on it.

---

## What we see when someone loads your badge

An ordinary image request, like any logo on any page. It carries no cookies and
no identity — **this service has no accounts**, so there is nobody to attach a
request to. "This is not a tracker" is not a promise we are asking you to trust:
it is how `<img>` works, and you can verify it in devtools in ten seconds. No
script runs, nothing is set, nothing on your page is readable from ours.

Our CDN keeps standard access logs (IP, URL, timestamp) for 30 days for
operations, then deletes them. Most loads never even reach us, because the edge
cache answers them — and on GitHub they never reach us at all, because the proxy
answers first. Nothing is sold, shared or profiled. The rest is at
[privacy.html](https://describe.net/privacy.html).

This architecture is a deliberate rejection, not a simplification. The 2010
Facebook Like button collected data whether or not you clicked it, which ended in
an EFF letter and, eventually, *Fashion ID* at the CJEU: a site embedding a
widget that transmits personal data is a **joint controller** of that
transmission. A badge that transmits nothing creates that problem for nobody. We
copied the Like button's distribution — copy, paste, done — and refused its
architecture. It is also three orders of magnitude cheaper: one cache entry per
wallet against one `fetch` per pageview.

---

## Related

- `https://describe.net/badge.html` — the configurator: type a wallet, see the
  badge, copy the snippet
- `https://api.describe.net/wallets/{wallet}/chains` — free, and the route that
  answers what the badge cannot: per-chain split, `caveats[]`, `[]` vs `null`
- `https://describe.net/api.md` — what the API serves and what it costs
- `https://describe.net/auth.md` — how you pay (you do not authenticate; payment
  is the authentication)
- `https://describe.net/a2a.md` — the agent-to-agent gate, the other free door
- `https://api.describe.net/pricing` — every price and the reason for it
- `https://api.describe.net/health` — the authority on how fresh the index is
