Skip to main content
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:
Client
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):
Client (manual)
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.
Client
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

  1. Open your /play deep link on a device signed into Bankroll.
  2. Your site loads; the first identity()/pay() call shows the consent sheet.
  3. identity() returns a token your server verifies (Step 3).
  4. 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.