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

# The session token

> One call, one real person: verified identity and geo in a signed token you check on your server.

`bankroll.session()` resolves the **Bankroll session token** — a signed JWT scoped
to your app that identifies the current user. Send it to your server in the
`x-bankroll-token` header and verify it there before trusting anything in it.

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

const token = await bankroll.session();
```

The first call may ask the user to connect your app; later calls resolve without
re-prompting. Tokens are short-lived (\~15 minutes) — the SDK caches the current
one and mints a fresh one before it expires, and concurrent calls share one
mint, so call `session()` as often as you like.

## One call, one real person

Almost everything hard about running a real-money product — multi-accounting,
bonus abuse, limits that actually hold, AML — reduces to one question: *is this
account a unique, real person?* Answering it yourself means building identity
verification and deduplication before you write a line of your actual product.
On Bankroll, the answer is a parameter:

```ts theme={null}
const token = await bankroll.session({ identity: true });
// resolves only for a verified user — one real, deduplicated person.
// session.user.identity is guaranteed truthy.
```

`session({ identity: true })` resolves only once the user has **verified their
identity** — and because a real person can only verify **once**, a verified
session is a **single, Sybil-resistant identity**. That one primitive is the
foundation the hard parts stand on:

* **Multi-accounting** — one verified identity maps to one person; a second
  account for the same human can't produce a second verified session.
* **Responsible gaming** — enforce deposit, loss, and time limits per *person*,
  not per account.
* **AML & eligibility** — the identity is KYC-backed, with verified age and region.

If the user isn't verified yet, Bankroll **walks them through verification** and
resolves once they're done; if they don't complete it, the promise rejects with
`verification_declined`. Plain `session()` still works before verification —
get the handle and run free-to-play, then require `{ identity: true }` at the
moment real money is on the line.

<Note>
  An older Bankroll app that can't run verification rejects `{ identity: true }`
  with `update_required`.
</Note>

## Send it to your server

The token travels in the `x-bankroll-token` header. Decorate `fetch` once and
every request carries it:

```ts theme={null}
import { withBankrollToken } from "@joinbankroll/sdk";

const appFetch = withBankrollToken(fetch);
await appFetch("/api/session", { method: "POST" });
```

In a plain browser (status `'unavailable'`) requests go out bare and your server
responds 401. Any other failure — including the user declining — propagates.

## Verify it on your server

Never trust a session token that hasn't been verified. In Node, `verifyToken` is
the entire route guard:

```ts theme={null}
import { verifyToken } from "@joinbankroll/sdk/server";
import { BANKROLL_TOKEN_HEADER } from "@joinbankroll/sdk";

const session = await verifyToken(req.headers.get(BANKROLL_TOKEN_HEADER), {
  audience: "https://acme.example", // your app's exact origin
});
if (!session) {
  return Response.json({ error: "unauthorized" }, { status: 401 });
}
// session.user.wallet is the user's stable id — bind it to your session.
```

It checks the RS256 signature against Bankroll's public keys, the issuer, and —
critically — that the token was minted for **your** origin. It accepts
`null`/`undefined` and returns `null` on any failure, so one `if (!session)`
covers missing, forged, expired, and wrong-app tokens alike.

On other stacks, verify the JWT directly:

| What               | Value                                               |
| ------------------ | --------------------------------------------------- |
| Algorithm          | `RS256`                                             |
| Public keys (JWKS) | `https://joinbankroll.com/.well-known/jwks.json`    |
| Issuer (`iss`)     | `https://joinbankroll.com`                          |
| Audience (`aud`)   | **Your app's origin** (e.g. `https://acme.example`) |

<Warning>
  Always check `aud` equals your own origin, byte-for-byte — `https`, lowercase
  host, no default port, no trailing slash. The token is scoped to your app, so a
  token minted for a different app must not sign a user into yours. Also fetch the
  keys from the JWKS URL and cache them — never hard-code a key.
</Warning>

See the [Quickstart](/build/quickstart#step-3--identify-the-user) for full
verification snippets in Node.js, Ruby, and Python.

## The session

A verified token gives you the session and the user inside it:

```ts theme={null}
// what verifyToken returns
interface BankrollSession {
  iss: string;         // issuer — always https://joinbankroll.com
  aud: string;         // your app's origin
  iat: number;         // issued-at (seconds)
  exp: number;         // expiry (seconds) — tokens are short-lived (~15 min)
  geo?: string;        // this session's region — 'US-NY' (ISO 3166-2) or 'US'
  user: {
    wallet: string;    // the user's wallet address — their stable id, and payout target
    username: string;  // the user's Bankroll handle — always present
    identity: { age?: number } | false; // verified identity, or false
  };
}
```

The **session** carries the envelope and the request's `geo` (where the user is
*for this session* — current location, not their residence). The **user** carries
who they durably are.

| Field                         | Where   | Meaning                                                                                                                                                                                                                   |
| ----------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `iss` / `aud` / `iat` / `exp` | session | Standard JWT envelope. `iss` is always `https://joinbankroll.com`; `aud` is your origin.                                                                                                                                  |
| `geo`                         | session | The user's region for this session — an ISO 3166-2 code (e.g. `US-NY`), or a bare country code (`US`) when only the country is known. Omitted if unresolvable.                                                            |
| `user.wallet`                 | user    | The user's wallet address. Use it as their primary key on your platform, and as the destination for [payouts](/build/payouts).                                                                                            |
| `user.username`               | user    | The user's Bankroll handle. **Always present** — the host guarantees one, so you never special-case a handle-less user.                                                                                                   |
| `user.identity`               | user    | Verification state, always present. **`{ age }`** — verified (`age` omitted if their date of birth isn't on file). **`false`** — not verified (pending, never started, or rejected). Truthy ⟺ one verified real identity. |

<Note>
  **Wire names:** in the raw JWT, `user.wallet` is the `sub` claim and
  `user.identity` is delivered as `kyc`. The SDK exposes the friendly names; if you
  verify raw JWTs on another stack, read `sub` and `kyc` (treating an absent `kyc`
  as not-verified).
</Note>

## Gating eligibility

Real-money eligibility is a **server-side** decision, computed from the verified
session — never from an unverified client value:

* **`session.user.identity`** (truthy) — the user has cleared verification (one
  real, deduplicated person). `false` means not verified.
* **`session.user.identity.age`** — the user's verified age, for age-restricted play.
* **`session.geo`** — the user's region for this session, for where-you-can-play rules.

```ts theme={null}
const session = await verifyToken(token, { audience: MY_ORIGIN });
const canPlayRealMoney =
  session !== null &&
  session.user.identity !== false &&
  (session.user.identity.age ?? 0) >= 21 &&
  ALLOWED_REGIONS.has(session.geo ?? "");
```

## Handling a decline

A decline is a choice, not a failure. `session()` rejects with a
`BankrollError` whose code is `consent_declined`; `session({ identity: true })`
rejects `verification_declined` if they back out of verification. Catch either
and show your own "connect to continue" state rather than surfacing an error.

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

try {
  const token = await bankroll.session();
  // ...verify server-side, sign the user in
} catch (e) {
  if (e instanceof BankrollError && e.code === "consent_declined") {
    showConnectPrompt(); // let them try again
    return;
  }
  throw e;
}
```
