Skip to main content
Every app declares itself with a manifest served at /.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:
On Next.js, pass it to 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.jwt on the same origin your app loads from. Bankroll matches by origin (scheme + host + port), so https://acme.example and https://app.acme.example are different apps with different manifests — and each manifest’s sub must equal its own origin.
  • application/jwt. Serve it with Content-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 is header.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 / manifestVersion are wrong, session() and charge() reject with manifest_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 name and supportUrl — 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/support can 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.

Any other stack

A manifest is two Base64URL-encoded JSON segments joined by dots, with an empty signature. There is nothing to sign and no key to hold, so building it by hand is a few lines:
Before testing your app, make sure the manifest is live and well-formed — fetch it and decode the middle segment to check your claims: