badge.md — the embeddable reputation badge
What the API serves and what it costs: api.md · api.html · how you pay: auth.md · the configurator with a live preview: 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). 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)
[](https://describe.net/agent.html?wallet=0x715dc035ffb97dd7bb4095c6670138ba05bb4e6d)
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-Controlsays 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 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, 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:
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 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=3600matches the indexer: it runs hourly (indexer_period_secondsinGET /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
ETagis a hash of the SVG. If the index did not move, next hour's refetch is a304with an empty body:
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:
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.
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:
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:
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 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.
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 snippethttps://api.describe.net/wallets/{wallet}/chains— free, and the route that answers what the badge cannot: per-chain split,caveats[],[]vsnullhttps://describe.net/api.md— what the API serves and what it costshttps://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 doorhttps://api.describe.net/pricing— every price and the reason for ithttps://api.describe.net/health— the authority on how fresh the index is
Verbatim source: badge.md · from site/badge.md at git:b5eb83903e36 · back to the documentation portal