Embed
One line, and their reputation is on your page
Paste one script tag and the wallet's score appears where you pasted it — with the number of distinct raters behind it and the date of the data, because a score without its raters is a rumor. No cookies, no calls to our API from your page, and nothing that tells us which page it is on.
Make yours
That is not an EVM address (0x + 40 hex) nor a Solana id (32–44 base58 characters).
The four things it can say — and none of them is a zero
Every badge below is real, mounted right now by this same file. They look different on purpose: telling them apart is the whole job.
Rated
The score, how many different counterparties wrote it, and the date the index was last refreshed. Two decimals, trailing zeros trimmed — the same rule every surface of this ecosystem uses.
Rated, with a warning
The ratings are real and still served; what changes is the dress. A burn address nobody controls carries its warning inside the pixel and loses the violet. The badge is the surface with the least context, so the caveat travels with it.
Nobody rated it yet
"sin calificaciones", never a 0. No data is not bad data, and a zero would be a verdict we never measured. This is the state most subjects start in, and it is an invitation, not a shame.
We are down
Different words, different shape: an empty dashed box, never the grey filled one. Turning "describe is down" into "this wallet has no reputation" is exactly the confusion this index exists to prevent — so the failure refuses to wear the empty state's clothes, and it still links to the profile.
The failure is observable from your page, and it never phones home
A silent fail-open is a slow lie. Three channels, all local to your page:
the attribute data-describe-state on the element
(loading · ready ·
error · invalid), a bubbling
describe:badge event carrying the reason, and one
console.warn. Nothing is sent to us. Ever.
What it sends us — measured, not promised
A widget that gets pasted everywhere is exactly how an accidental tracker gets built: every load could tell the API which page it is on. That is the 2010 Facebook Like button, and it ended in an EFF letter and, at the CJEU, in a site that embeds a data-transmitting widget being a joint controller of the transmission. We copied the Like button's distribution and refused its architecture.
| Does the widget… | Answer | How you can tell |
|---|---|---|
| …call our JSON API from your page? | No | There is not one fetch in the file. The only request is the image, and the browser makes it in non-scriptable mode. One URL per wallet, cached an hour at the edge: a page with a million views an hour costs this origin about one request per CDN location per hour. While your page stays open, the badge asks for that same image again at most once an hour, under a URL every viewer shares that hour — so keeping it fresh costs this origin at most one more request per wallet per CDN location per hour, never one per viewer. |
| …send the page you are on? | No | The image carries referrerpolicy="no-referrer" and the link rel="noreferrer". Measured 2026-08-30 against a local origin that logs the header: the hand-written <img> snippet delivered the host's origin; the widget delivered nothing at all. |
| …set cookies or touch storage? | No | No document.cookie, no localStorage, no beacons. There is nothing to attach a request to anyway: this service has no accounts. |
| …read anything on your page? | No | It reads the attributes you wrote and your <html lang>, to pick the language of its own fallback text. Nothing leaves the page. |
| …know who clicked? | No | Deliberately: rel="noreferrer" costs us the referral metric and we pay it. Analytics is not worth contradicting the promise on somebody else's page. |
| …does our CDN log the image request? | Standard access logs | IP, URL and timestamp, 30 days, then deleted — same as any image on any CDN. Most loads never reach us because the edge answers first. Nothing sold, shared or profiled. The rest is on Privacy. |
One honest caveat about the "reads without JavaScript" variant. If the
fallback you put inside the element is the badge <img>
itself, the browser already requested it before our file existed, and it did
so without referrerpolicy — so your origin reaches us
anyway. Measured 2026-08-30: one single request, but carrying
Referer. If the no-referrer property is what you want,
make that inner fallback a text link. The configurator's variant does exactly
that.
When you should not use this
For one badge in a README, a Markdown doc or a static page, the plain image is better and this file is a waste. It is one tag, it runs no code, it cannot break the page it is on, and it needs nothing from us but the pixel. Take it from the badge configurator and skip this page. A widget that sells itself as a replacement for something cheaper than itself is lying.
The script earns its place when there are many badges, when your page paints its list with JavaScript and the badges do not exist at parse time, or when you need the reader to see something honest if we are down. Everything else the image already does.
And it changes the authoring, never the transport: what lands on
your page is the same <img> pointing at the same
free GET /badge/{wallet}.svg the image snippet uses.
A widget that fetched JSON per pageview would be three orders of magnitude
more expensive for the same pixel, and could not stay free.
As an iframe: the same badge, keeping itself fresh
embed-frame.html is the one page of this site that any
other site may put in an <iframe>: it is served with
Content-Security-Policy: frame-ancestors *, while every
other page of describe.net keeps answering X-Frame-Options: DENY.
Inside it is the same free badge the script mounts — the score, how many distinct
raters wrote it and the date of the last description, drawn inside the pixel.
Nothing from the paid routes: the frame never asks for anything a visitor could
not ask for free.
While the page stays open it asks for the badge again, never more often than the
route's own max-age — an hour, which is also how often the
index is recomputed — and further apart after an error or a 429. If we are down,
the dated badge it already has stays where it is, because an old badge that states
its date beats "unavailable".
<iframe src="https://describe.net/embed-frame.html?wallet=0xa3279f744438f83bc75ce9f8a8282c448f97cc8a"
title="reputation on describe.net"
width="440" height="20" loading="lazy"
referrerpolicy="no-referrer"
sandbox="allow-scripts allow-popups allow-popups-to-escape-sandbox"
style="border:0;overflow:hidden"></iframe>
There is nothing in that frame to act with: no session, no cookies, no form (its
own policy says form-action 'none') and no call to our API
but the image. The sandbox above, without
allow-same-origin, gives it an opaque origin, so it could
not read or write anything of describe.net even if it tried;
allow-popups is what lets the profile link open in a new
tab. And referrerpolicy="no-referrer" keeps the address of
your page from reaching us at all.
Three parameters and no more: wallet (required),
height in pixels (8 to 200, default 20) and
lang (es or en,
for the frame's own text). A frame does not shrink to its content, so the width is
yours to set: about 22 times the height fits even a badge carrying a warning
(427 px at height 20).
It runs on your page and it cannot break it
| Your CSS cannot disfigure it | Each badge lives in an open shadow root, so no selector of yours reaches inside. Measured 2026-08-30 against a hostile sheet (img{width:100%!important;filter:invert(1)!important;border:4px solid red!important} — three rules that exist in real themes): the hand-written badge came out 908×28, inverted and red-bordered; the widget came out 315×20, untouched. Open, not closed: you have every right to inspect what we put in your page. |
| It cannot throw at you | Every entry point is wrapped. The worst case is a console.warn and a badge that does not appear. |
| It claims one global | window.describeBadge, and if it already exists the second copy reuses the first instead of overwriting it. |
| It writes no HTML | No innerHTML, no document.write, no eval. The DOM is built node by node, and the wallet is validated with the same regular expression this page uses before it ever reaches a URL. That matters when the wallet in the attribute comes from the rated subject rather than from you. |
| It installs no observers on your document | No MutationObserver watching a page we do not own. If your list renders later, call describeBadge.scan() yourself — explicit and cheap. |
| It does not steal your layout | The height is reserved before the image lands, so the badge does not shove your paragraph down. The width is not fixed, because a badge grows with what it has to say (315 px with a score, 427 px when it also carries a warning). |
Every attribute it reads
Put them on the element, or on the script tag to set a default for the whole page. A value that does not parse falls back to the default with a warning — never an exception.
| Attribute | Default | What it does |
|---|---|---|
data-wallet | — | On the <script> tag only: mounts one badge exactly where the tag is. |
data-describe-wallet | — | On any element: mounts the badge inside it, replacing what it paints. This is the one to use for many badges. |
data-height | 20 | Badge height in pixels. The width follows the content. |
data-link | true | false renders the badge with no anchor, for pages that handle their own clicks. |
data-target | _blank | Where the profile opens. _self navigates away from your page, which is why it is not the default. |
data-timeout | 30000 | Milliseconds before a hanging request counts as down. 30 s is the same clock the three live consumers use, and it starts when the badge reaches the screen, not when it mounts. |
data-refresh | true | Asks for the badge again while the page stays open — at most once an hour, which is the route's own max-age, and further apart after an error or a 429. false mounts it once and never looks again. |
data-api | https://api.describe.net | For testing against a local backend, or self-hosting. |
data-site | https://describe.net | Where the profile link points. Same reason. |
For pages that paint their lists with JavaScript
The script scans once when the DOM is ready. Badges that appear later are yours to mount — that is deliberate, because watching a whole document we do not own is a cost we cannot measure from here.
describeBadge.scan() // mount every [data-describe-wallet] not mounted yet
describeBadge.scan(container) // same, limited to a subtree
describeBadge.mount(el, { wallet }) // mount one element explicitly
describeBadge.badgeUrl(wallet) // the SVG URL, or null. Touches no network.
describeBadge.profileUrl(wallet) // the profile URL, or null
describeBadge.isWallet(s) // the same validation the badge route uses
el.addEventListener('describe:badge', (e) => {
// e.detail = { state, wallet, reason, version }
// state: 'ready' | 'error' | 'invalid'
});
There is no method that returns a bare number, and there never will be: the score always arrives with its rater count and its date, inside the pixel. A score without its raters is a rumor, and a helper that handed you the float alone would erase the only thing this index is for.
Related
- The badge configurator — the plain image, no JavaScript, the right answer for a README
- badge.md — what is inside the pixel, the cache headers, and the known limits
- The profile of a wallet — where the click lands: every rating with its transaction
- API and MCP — the free route that tells "never registered" from "registered but never rated", which the badge cannot
- Privacy — the whole policy, of which this page is one corner