> ## Documentation Index
> Fetch the complete documentation index at: https://docs.joinbankroll.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Quickstart

> Build an app for Bankroll from zero — SDK, manifest, identity, payments, and a deep link.

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

```bash theme={null}
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).

```ts theme={null}
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.

```json /.well-known/bankroll.json theme={null}
{
  "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](/build/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:

```ts Client theme={null}
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):

```ts Client (manual) theme={null}
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.

<CodeGroup>
  ```javascript Node.js theme={null}
  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?, ... }
  }
  ```

  ```ruby Ruby theme={null}
  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
  ```

  ```python Python theme={null}
  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,
      )
  ```
</CodeGroup>

<Warning>
  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.
</Warning>

See [Identity](/build/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.

```ts Client theme={null}
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](/build/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:

```ts theme={null}
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.
