Token Gating
Using a token balance instead of a subscription, and the two caveats that come with it.
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:
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.