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

# App status & offers

> An optional endpoint the Bankroll app reads to decorate your tile — a ring and a headline when you have something on offer for this user.

Bankroll reads `https://<your-origin>/.well-known/bankroll-status` when it
shows your tile. Answer with what you have on offer for the person asking and
the tile gets a ring and your headline; answer nothing and it doesn't. That is
the whole effect: the document is decorative, per user, per moment, and grants
nothing.

## The request

* `GET /.well-known/bankroll-status`, no ambient credentials, at most five
  same-origin redirects, **2 seconds**, `application/json`, body **≤ 1 KiB**.
  Anything else — slow, large, wrong type, unparseable — reads as "no offer",
  never as an error.
* If the user has already connected your app, the request carries
  `Authorization: Bearer <token>` naming the user, scoped to your origin —
  verify it with `verifyToken(token, { audience: origin })` from
  `@joinbankroll/sdk/server` and answer for that wallet. It identifies; don't
  treat it as a login. **No token means a
  user who hasn't connected you yet — respond with your generic new-user
  offer** (a welcome bonus, a first-deposit match), the same answer for
  everyone.
* Per-user answers must not be cached: send `cache-control: no-store`.

## The response

```json theme={null}
{ "offers": [{ "headline": "Double your first deposit", "key": "cmszndqd20000l204ga84lvgq" }] }
```

`offers` is an ordered array; Bankroll shows the **first** entry and ignores
the rest (keep it to a few — the whole body must stay under 1 KiB). The app
asks once each time the Home screen mounts, draws a ring on your tile for the
first offer, and clears it when the tile is tapped. An entry is an object with two optional members, or `true` for an
offer with no details:

| Member     |                                                                                                                                                                                                                              |
| ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `headline` | ≤ 64 characters, drawn as a one-line banner on the tile; aim for \~30 so it fits. Over the limit it is **dropped, not truncated**.                                                                                           |
| `key`      | ≤ 64 characters, opaque to Bankroll. When the user opens your app from the ringed tile it comes back as `?offer=<key>` on your launch URL, so your app knows which offer they tapped. It doesn't change where the app opens. |

Absent, `null`, `[]`, `false`, or anything unrecognized means no offer.

## Example

```ts theme={null}
// app/.well-known/bankroll-status/route.ts
import { verifyToken } from "@joinbankroll/sdk/server";

export const dynamic = "force-dynamic";

export async function GET(request: Request) {
  const auth = request.headers.get("authorization");
  const origin = `https://${request.headers.get("host")}`;
  const session = auth?.startsWith("Bearer ")
    ? await verifyToken(auth.slice(7), { audience: origin })
    : null;

  const offers = session
    ? await offersFor(session.user.wallet)   // this user's open offers, best first
    : await newUserOffer();                  // no token: the generic new-user offer

  return Response.json({ offers }, { headers: { "cache-control": "no-store" } });
}
```

<Note>
  Nothing here is load-bearing: a bad token, a slow query, or an empty answer
  costs the user a ring, not a session. Don't put state your app depends on
  behind it, and don't tell an anonymous caller anything you wouldn't print on a
  poster.
</Note>
