> ## 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.

# On your own origin

> The self-hosted path — install the SDK, serve a manifest, verify a session, take a payment, and open the app with a deep link.

<Note>
  The main path is the Bankroll CLI: `bankroll apps create` makes an app that
  Bankroll hosts, and a push deploys it. See [Build with an agent](/build/agents).
  This page is for an app you host yourself, on your own origin.
</Note>

A Built-for-Bankroll app is a web app served from your own HTTPS origin — any
framework.

## Step 1 — Install the SDK and detect the host

```bash theme={null}
npm install @joinbankroll/sdk
```

Your app will run in three places: a plain browser, an outdated Bankroll app,
and a current Bankroll host. `bankroll.status()` tells you which — synchronous,
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":           // session() and charge() will work
}
```

## Step 2 — Serve your manifest

Serve a manifest at **`/.well-known/bankroll.jwt`** on your app's origin. It
declares your app's name and icon, the capabilities it uses, and the address that
receives payments. It's an **Unsecured JWT** (`alg: none`, empty signature) —
Bankroll binds it to your app by fetching it from your origin, so there's no key
to manage.

On Next.js, `manifestRoute()` is the whole route — you declare what's yours and
the SDK owns the format:

```ts app/.well-known/bankroll.jwt/route.ts (Next.js) theme={null}
import { manifestRoute } from "@joinbankroll/sdk/next";

export const dynamic = "force-dynamic"; // built from the request's own host

export const GET = manifestRoute({
  name: () => "Acme Games",
  launch: "/app",
  payments: () => process.env.PAYMENT_ADDRESS ?? null,
});
```

On any other stack, build the two Base64URL segments yourself — see
[The manifest](/build/manifest#any-other-stack) for Node, Ruby, and Python.

Bankroll fetches this over HTTPS — if it's missing or malformed, your app won't
be granted any capability. To show an icon, also serve a square PNG at
`/.well-known/bankroll-icon.png`. See [The manifest](/build/manifest) for every
claim and rule.

## Step 3 — Identify the user

The user's verified session 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 session 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.session()` resolves the token (the SDK caches
it and re-mints before expiry, so repeated calls are cheap):

```ts Client (manual) theme={null}
import { bankroll, BANKROLL_TOKEN_HEADER } from "@joinbankroll/sdk";

const token = await bankroll.session();
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. `session.user.wallet` is the user's stable id.

On Next.js this is one call, with the audience taken from the request's own host
so there's no origin constant to keep in step — see
[Next.js helpers](/build/next):

```ts app/api/session/route.ts (Next.js) theme={null}
import { requireSession } from "@joinbankroll/sdk/next";

export const dynamic = "force-dynamic";

export async function POST(request: Request) {
  const session = await requireSession(request); // throws Unauthorized
  return Response.json({ wallet: session.user.wallet });
}
```

On any other stack, verify it directly:

<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 requireBankrollSession(req) {
    const session = 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 (!session) return null;
    return session; // BankrollSession — { ..., geo?, user: { wallet, username, 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 [The session token](/build/session) for every claim and what it means.

## Step 4 — Charge the user

Call `bankroll.charge()` with an amount in whole US cents. Bankroll shows the user
the charge, moves the funds to the address your manifest fixes in
`capabilities.payments`, and resolves with the settled payment's signature.
The call cannot name a recipient.

```ts Client theme={null}
import { bankroll, BankrollError } from "@joinbankroll/sdk";

async function buy(orderId: string, amountCents: number) {
  try {
    const signature = await bankroll.charge({
      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; // Bankroll already prompts to add funds
      if (e.code === "payment_denied") return;     // user declined the charge
      if (e.code === "consent_declined") return;   // user declined connecting the app
    }
    throw e;
  }
}
```

Every failure is a `BankrollError` with a stable snake\_case `code`. Handle
`insufficient_funds`, `payment_denied`, and `consent_declined` as shown above
without surfacing an error; propagate anything else. On your server, confirm the returned signature
settled the expected amount to your payment address 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
```

When a *user* shares that link rather than you, pass their wallet and Bankroll
credits them for anyone new who joins through it — see
[Share links](/build/share-links):

```ts theme={null}
playLink("https://acme.example/", { referrer: session.user.wallet });
```

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; your first `session()`/`charge()` call runs.
3. `session()` returns a token your server verifies (Step 3).
4. `charge()` returns a signature your server confirms (Step 4).

## Going live

No registration or approval is required: once your manifest is served, your
`/play` link opens your app for any Bankroll user.

To pay users back — winnings, refunds — see [Paying a user](/build/payouts).
Being **bundled** in the Bankroll app itself (featured, first-party placement)
is a separate step that requires Bankroll approval — ask in the
[Built for Bankroll Discord](https://discord.gg/FH3BbAM7t6).
