/.well-known/bankroll.jwt on its own origin. Bankroll fetches it over
HTTPS to learn three things: what your app is called, which capabilities it
uses, and the address that receives its payments. Because Bankroll reads it
from your origin, the manifest is bound to your app by where it’s served — not
by a signature. It’s an Unsecured JWT — a JWT with alg: none and an
empty signature — so there is no key to manage. Verified apps serve a
Bankroll-signed version of the same manifest instead.
Claims
Decoded, the manifest payload is a small JSON object:Your icon
Your icon is not a manifest claim. Serve a square PNG (256×256 or larger) at the fixed path/.well-known/bankroll-icon.png on your origin — a
sibling of the manifest. Bankroll fetches it from there and shows it on your
app’s tile and in Connected Sites; until you serve one, it shows a monogram of
your app’s name. A manifest that still includes an iconUrl claim remains
valid — the claim is simply ignored.
Updating your icon — iconDigest
If you replace the PNG at the same path, users who have already connected your
app can keep seeing the old one. To make an update reach them, add the OPTIONAL
iconDigest claim: a hash of the icon bytes as a Subresource Integrity
hash-expression (sha256-<base64>). Bankroll treats a changed iconDigest as a
manifest change, so your new icon propagates to connected users. Compute it from
the exact bytes you serve, and omit the claim if you don’t serve an icon:
manifestRoute’s
optional iconDigest and the SDK handles the omission rules.
The recipient is read from the fetched manifest, never supplied by your page.
For a session-only app, omit
payments and set session: true alone.Rules
- Served on your origin. The manifest must live at the exact path
/.well-known/bankroll.jwton the same origin your app loads from. Bankroll matches by origin (scheme + host + port), sohttps://acme.exampleandhttps://app.acme.exampleare different apps with different manifests — and each manifest’ssubmust equal its own origin. application/jwt. Serve it withContent-Type: application/jwt, no larger than 8 KiB, over HTTPS. Your app URL must be HTTPS too.alg: none. The header is exactly{ "alg": "none", "typ": "bankroll-app-manifest+jwt" }, and the signature segment is empty — the token isheader.payload., with a trailing dot and nothing after it.- Must be valid. If the manifest is missing, unreachable, malformed, the
wrong media type, or its
sub/aud/manifestVersionare wrong,session()andcharge()reject withmanifest_error. - Declare what you use. Any capability you call must be enabled here. Calling
one you didn’t declare rejects with
capability_not_registered. See error codes. - Changing it re-asks your users. A user’s grant is bound to the exact
manifest they approved, so editing any claim — including
nameandsupportUrl— means everyone who already connected your app is asked to approve it again on their next visit. Treat these as settings you pick once. Where a value may need to move later, point it somewhere you control and redirect:https://acme.example/supportcan change destination freely, but editing the claim itself cannot.
Serving it
On Next.js,manifestRoute() from @joinbankroll/sdk/next is
the whole route. You declare what is genuinely your app’s — its name, where it
launches, where payments settle — and the SDK owns the format, so a change to
the format arrives with an npm upgrade rather than a diff you have to write:
sub is taken from the request’s own host, so preview deployments, custom
domains, and local tunnels each declare themselves correctly with nothing to
configure. payments returning null omits the capability rather than
advertising an address the app cannot honor — which is what
treasuryAddress() does before a treasury key is
set. supportUrl and appTokens are omitted from the payload entirely when
they resolve to nothing, because an empty claim is still a claim and would
re-ask every connected user for consent once it gained a value.