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

# Identity

> Get a signed token that identifies the Bankroll user, and verify it on your server.

`bankroll.identity()` resolves a **signed identity token** — a JWT scoped to
your app. Use it to know who the user is: their stable id, their Bankroll
handle, region, and verified age. Send it to your server and verify it there
before trusting anything in it.

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

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

The first call for a new app shows the user exactly what they're sharing; once
granted, later calls resolve without re-prompting. Tokens are short-lived
(\~15 minutes) — the SDK caches the current token and mints a fresh one before it
expires, and concurrent calls share one mint, so call `identity()` as often as
you like.

The promise rejects with a [`BankrollError`](/build/payments#error-codes) —
`consent_declined` if the user declines the consent prompt, `unavailable` /
`update_required` outside a current Bankroll host.

## 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 a consent decline — propagates; it
is never silently downgraded to a bare request.

## Verify it on your server

Never trust an identity 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 verified = await verifyToken(req.headers.get(BANKROLL_TOKEN_HEADER), {
  audience: "https://acme.example", // your app's exact origin
});
if (!verified) {
  return Response.json({ error: "unauthorized" }, { status: 401 });
}
// verified.sub 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 (!verified)`
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.

## Claims

A verified token gives you:

```ts theme={null}
// what verifyToken returns
interface VerifiedToken {
  sub: string;         // stable user id — bind to your session, store as-is
  username?: string;   // Bankroll handle; omitted when unset
  geo?: string;        // 'US-NY' (ISO 3166-2) or 'US'; omitted when unresolvable
  identity?: { age?: number } | false; // verification state — see below
  aud: string; iss: string; iat: number; exp: number;
}
```

| Claim         | Always present | Meaning                                                                                                                                                                                                                                               |
| ------------- | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `sub`         | Yes            | The user's stable id. Use it as the primary key for the user on your platform.                                                                                                                                                                        |
| `iss`         | Yes            | Always `https://joinbankroll.com`.                                                                                                                                                                                                                    |
| `aud`         | Yes            | Your app's origin.                                                                                                                                                                                                                                    |
| `iat` / `exp` | Yes            | Issued-at and expiry. Tokens are short-lived (\~15 minutes); the SDK re-mints automatically.                                                                                                                                                          |
| `username`    | No             | The user's Bankroll handle. Omitted if they haven't set one.                                                                                                                                                                                          |
| `geo`         | No             | The user's region as an ISO 3166-2 code (e.g. `US-NY`), or a bare country code (`US`) when only the country is known. Omitted if unresolvable.                                                                                                        |
| `identity`    | No             | Identity-verification result — three states. **Absent**: the user has never been through verification. **`false`**: verification was rejected. **Object**: verified — `{ "age": <number> }`, with `age` omitted if their date of birth isn't on file. |

<Note>
  **Wire name:** in the raw JWT this claim is delivered as `kyc`. The SDK's
  `verifyToken` reads either name and always exposes it as `identity`; if you
  verify raw JWTs on another stack, read `identity ?? kyc`.
</Note>

<Note>
  Optional claims are **omitted**, never set to `null` — test for presence.
</Note>

## Gating eligibility

For real-money play, compute eligibility on your **server** from the verified
token — never from an unverified client value:

* **`typeof verified.identity === "object"`** — the user has cleared identity
  verification. Don't gate on mere *presence*: a rejected user's value is
  `false`, which is present too. Absent means they haven't verified yet.
* **`verified.identity.age`** — the user's verified age, for age-restricted play.
* **`verified.geo`** — the user's region, for where-you-can-play rules.

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

## Handling a decline

A user declining the consent prompt is a normal choice, not an error.
`identity()` rejects with a `BankrollError` whose code is `consent_declined` —
catch it 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.identity();
  // ...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;
}
```
