## The entitlement seam

`RequireEntitlement` asks one question: may this workspace use the gated surface at all? **What that
question means** lives in `app/models/entitlement.rb`, in one method:

```ruby
def for(account) = Subscription.new(account)
```

Four strategies ship: `Subscription` (Stripe, the default), `TokenBalance`, `Nft`, and `Open` (no
gate). `Any` composes two, for "subscribers or holders".

This is a file you **edit**, not a framework to extend. Point `for` at the strategy your app uses and
delete the rest. That indirection exists for exactly one reason: it lets a gated controller stay
gated while what the gate means changes, so removing billing does not mean rewriting every gated
controller.

A grep-based spec keeps `RequireEntitlement` the only caller. If a controller starts asking `Billing`
directly again, removing billing breaks that controller rather than one file.

## Never read the chain in a request

`Entitlement::TokenBalance` reads a **cached** result, and this is the design decision that decides
whether token gating is usable at all.

The kit's billing design is deliberate: `Billing::Stripe` inherits `entitled?` unchanged, so
entitlement reads never touch Stripe. They read the local row that the webhook keeps in sync. A gate
that called `eth_call` inside a `before_action` would be **strictly worse than the thing the kit
already refuses to do**: an RPC round trip on every gated request, and a page that hangs whenever the
provider is degraded.

So the balance is checked at sign-in and by a recurring job, and written to `wallets.gate_ok` with a
`gate_checked_at` timestamp. `Wallet#gate_ok?` **fails closed** once that answer is older than
`Wallet::GATE_TTL`: an expired answer is not an answer.

Chains have no webhooks, so a staleness window is unavoidable. Making it an explicit constant rather
than an accident is the whole point.

## Two caveats, documented rather than hidden

**A wallet is transferable and a workspace is not.** Re-pointing one has to revoke the previous
holder. The kit already worked this out once, in `License#assign_github_username!`, and it is the
same bug.

**A balance gate is a soft gate.** Tokens can be borrowed for the duration of a check. If the gated
surface is worth real money, the answer is a snapshot at a fixed block, not a live balance. Use this
for app access, not for anything you would be upset to give away.

**And it is not billing.** Token gating produces no revenue, and the credential is transferable.
Framing it to your users as "the crypto version of Stripe" sets a false expectation. It is an access
gate keyed to a wallet, with a stated staleness window.

One more thing worth knowing: utility gating can *increase* securities risk if you promote it
alongside price appreciation. Frame it as a product feature, never as an investment thesis.
