describe.net docs

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)

[![reputation on describe.net](https://api.describe.net/badge/0x715dc035ffb97dd7bb4095c6670138ba05bb4e6d.svg)](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-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 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_ratershow 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=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:
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.


  • 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

Verbatim source: badge.md · from site/badge.md at git:b5eb83903e36 · back to the documentation portal