This guide builds an app for Bankroll from zero. A Built-for-Bankroll app is an
ordinary web app — any framework, served from your own HTTPS origin — so
scaffold one however you like. Then: install the SDK, serve a manifest,
identify the user, take a payment, and open it with a deep link.
Step 1 — Install the SDK and detect the host
npm install @joinbankroll/sdk
Your app runs in three places: a plain browser, an outdated Bankroll app, and a
current Bankroll host. bankroll.status() tells you which — it’s synchronous
and safe anywhere, including SSR (always 'unavailable' on the server).
import { bankroll } from "@joinbankroll/sdk";
switch (bankroll.status()) {
case "unavailable": // not inside Bankroll — show "Get the Bankroll app"
case "update_required": // Bankroll app too old — show "Update the Bankroll app"
case "ready": // identity() and pay() will work
}
Step 2 — Serve your manifest
Serve a JSON file at /.well-known/bankroll.json on your app’s origin. It
declares your app’s name and icon, the capabilities it uses, and the wallet that
receives payments.
/.well-known/bankroll.json
{
"name": "Acme Games",
"iconUrl": "https://acme.example/icon-256.png",
"merchantWallet": "9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin",
"capabilities": ["identity", "pay"]
}
Bankroll fetches this over HTTPS and fails closed — if it’s missing or
malformed, your app won’t be granted any capability. See
The manifest for every field.
Step 3 — Identify the user
The user’s identity travels as a signed token in the x-bankroll-token header.
The simplest way to send it is to decorate fetch once:
import { withBankrollToken } from "@joinbankroll/sdk";
const appFetch = withBankrollToken(fetch);
// Every request now carries the user's identity token. In a plain
// browser the request goes out bare and your server responds 401.
await appFetch("/api/session", { method: "POST" });
Or attach it manually — bankroll.identity() resolves the token (the SDK
caches it and re-mints before expiry, so call it as often as you like):
import { bankroll, BANKROLL_TOKEN_HEADER } from "@joinbankroll/sdk";
const token = await bankroll.identity();
await fetch("/api/session", {
method: "POST",
headers: { [BANKROLL_TOKEN_HEADER]: token },
});
On your server, verify the token — never trust an unverified token from the
client. The sub claim is the user’s stable id.
import { verifyToken } from "@joinbankroll/sdk/server";
import { BANKROLL_TOKEN_HEADER } from "@joinbankroll/sdk";
// Your app's exact origin — the token is scoped to it.
const MY_ORIGIN = "https://acme.example";
export async function requireBankrollUser(req) {
const verified = await verifyToken(req.headers.get(BANKROLL_TOKEN_HEADER), {
audience: MY_ORIGIN,
});
// null covers everything: missing header, bad signature, expired,
// or a token minted for a different app.
if (!verified) return null;
return verified; // { sub, username?, geo?, identity?, ... }
}
require "jwt"
require "net/http"
require "json"
JWKS_URI = "https://joinbankroll.com/.well-known/jwks.json"
MY_ORIGIN = "https://acme.example"
# Cache the key set — don't refetch on every request.
def bankroll_jwks
@bankroll_jwks ||= JWT::JWK::Set.new(JSON.parse(Net::HTTP.get(URI(JWKS_URI))))
end
# token = request.headers["x-bankroll-token"]
def verify_bankroll_token(token)
payload, = JWT.decode(
token, nil, true,
algorithms: ["RS256"],
iss: "https://joinbankroll.com", verify_iss: true,
aud: MY_ORIGIN, verify_aud: true,
jwks: bankroll_jwks
)
payload # { "sub" => ..., "username" => ..., ... }
end
import jwt
from jwt import PyJWKClient
JWKS_URI = "https://joinbankroll.com/.well-known/jwks.json"
MY_ORIGIN = "https://acme.example"
_jwks = PyJWKClient(JWKS_URI)
# token = request.headers["x-bankroll-token"]
def verify_bankroll_token(token: str) -> dict:
key = _jwks.get_signing_key_from_jwt(token).key
return jwt.decode(
token, key,
algorithms=["RS256"],
issuer="https://joinbankroll.com",
audience=MY_ORIGIN,
)
Verify the audience equals your origin, byte-for-byte: https, lowercase
host, no default port, no trailing slash (e.g. https://acme.example). The
token is scoped to your app; a token minted for someone else’s app must not
authenticate a user on yours.
See Identity for every claim and what it means.
Step 4 — Charge the user
Call bankroll.pay() with an amount in whole US cents. Bankroll shows the user
the charge, moves the funds to the merchantWallet from your manifest, and
resolves the settled payment’s signature. The recipient is fixed by your
manifest — the call cannot name an address.
import { bankroll, BankrollError } from "@joinbankroll/sdk";
async function buy(orderId: string, amountCents: number) {
try {
const signature = await bankroll.pay({
amountCents,
memo: `order:${orderId}`,
});
// Send the signature to your server to confirm before granting the item.
await fetch("/api/orders", {
method: "POST",
body: JSON.stringify({ orderId, signature }),
});
} catch (e) {
if (e instanceof BankrollError) {
if (e.code === "insufficient_funds") return; // host already showed add-cash
if (e.code === "consent_declined") return; // user backed out
}
throw e;
}
}
Every failure is a BankrollError with a stable snake_case code.
insufficient_funds (the host already funnels the user to add cash) and
consent_declined (the user declined) are expected outcomes — treat them as
handled, not errors. On your server, confirm the returned signature actually
settled the expected amount to your wallet before granting value. See
Payments.
Step 5 — Launch your app
Users open your app with a Bankroll deep link — your app’s URL, URL-encoded:
https://joinbankroll.com/play?url=https%3A%2F%2Facme.example%2F
The SDK builds it for you:
import { playLink } from "@joinbankroll/sdk";
playLink("https://acme.example/");
// → https://joinbankroll.com/play?url=https%3A%2F%2Facme.example%2F
Opening that link on a device with Bankroll installed launches the app and loads
your site in the host webview. The URL must be HTTPS, and its origin must
resolve to a valid manifest (Step 2).
Step 6 — Test the round trip
- Open your
/play deep link on a device signed into Bankroll.
- Your site loads; the first
identity()/pay() call shows the consent sheet.
identity() returns a token your server verifies (Step 3).
pay() returns a signature your server confirms (Step 4).
Launch your app
Opening a third-party app in Bankroll is in early access. When your manifest,
identity verification, and payment confirmation all work end-to-end, reach out to
the Bankroll team to have your app enabled for launch.