Censor, Never Steal: Why the Service That Decides Is Never the Service That Signs

How FairWins runs a funded gas wallet on the open internet by splitting one server into two — a bouncer that decides, and an engine that pays

The button that spends someone else’s money

In part 1 of this series we covered the friendly half of gasless transactions: on FairWins, a user with no cryptocurrency for network fees can still act. They sign an instruction — “accept this wager,” “claim my winnings” — with their wallet, off to the side, for free. Someone else then pays the small network fee to actually submit it. That post ended on an uncomfortable word: someone.

Someone has to run a server. That server holds a funded wallet, accepts signed instructions from anyone on the internet, and turns them into paid transactions. Every bad outcome you can picture is on the table: the wallet’s key gets stolen; a bot floods the server until the gas budget is gone; a sanctioned wallet slips an action through; one deliberately expensive transaction burns a week’s budget in a single shot; or the whole thing falls over at 2 a.m. and people are stuck mid-wager.

FairWins had an extra constraint. The platform’s standing rule is no backend — just smart contracts, a static web app, and an indexer that reads the blockchain. This gas-paying server is the one deliberate exception, and being the exception raised the bar: if a server has to exist, its potential for damage has to be small enough to describe in a sentence.

The design that shipped does exactly that. One service decides whether a transaction should exist at all. A completely different service decides how it gets paid for and confirmed. Neither can do the other’s job. The goal, in the team’s own words: the hosted system can only ever censor, never steal.

The split: a bouncer in front, an engine behind

Picture two machines wired together.

The first is the bouncer. It faces the internet, receives each signed instruction, and runs it through a checklist before letting anything through. Is the system paused by its emergency switch? Is this a supported network and action? Who actually signed this — worked out mathematically from the signature itself, never taken on the sender’s word? Have we already processed this exact instruction (no double-submitting)? Does the signer pass a sanctions screen and stay under their rate limit? Would this blow the fee budget? Only an instruction that clears every gate gets packaged up and handed onward.

The second is the engine. It is a well-known open-source piece of infrastructure, run as-is, that does one unglamorous job well: take a fully-formed transaction, pay for it, and get it confirmed on the blockchain — managing the fiddly mechanics of ordering, pricing, retries, and switching to a backup connection if one fails. Crucially, the engine holds the gas wallet’s key. That key lives inside a hardware security module — a tamper-resistant vault that can use the key to sign but can never hand the key out.

Between the two machines is a deliberately tiny opening. The engine receives only a finished transaction: where it’s going, and how fast to push it. It never sees the original instruction, never learns who signed it, and holds no opinions. All the judgment lives in the bouncer; all the mechanics live in the engine.

That narrow seam buys three things. The engine is swappable — it’s off-the-shelf, so it could be replaced without touching a line of policy logic. The engine is auditable by its settings rather than its code — a plain configuration file pins a spending cap per network and, most importantly, a list of the only addresses it is ever allowed to pay. Even if hijacked, it could only ever pay into FairWins’ own contracts. And the policy is testable without touching a real blockchain, because it’s cleanly separated from the machinery.

Status flows back honestly: the engine tells the bouncer when a transaction is genuinely confirmed on-chain, and only then does the app report it as done. The system never guesses or reports success early.

Fail closed where money moves

The most telling item on the checklist is the sanctions screen. The smart contracts already screen everyone — so why screen again? Because the gas wallet pays the network fee before the contract ever gets a chance to reject the transaction, and “we paid to submit a sanctioned wallet’s transaction” is a sentence nobody wants to write. So the bouncer re-checks the true signer against the same on-chain screen.

The rule when that screen can’t give a clear answer is strict: if it says no, reject; if it’s unreachable, also reject — never “assume it’s fine and pay anyway.” This is the instinct across the whole checklist: when money is about to move, refuse rather than guess.

What a total compromise actually buys an attacker

Because of the split, the worst case fits in a small table:

PartHoldsCan doCannot do
Bouncertwo shared passwordsaccept or refuse instructions, screen, rate-limitsign anything, move funds, fake a signer
Enginethe ability to use the gas keypay for transactions to approved addresses onlyexceed the spending cap, pay anyone else, touch user funds
Hardware vaultthe actual gas keyproduce signaturesever reveal the key
Gas walleta small balance for feespay network feesanything else — it has no special authority

Take over the entire hosted system and here is your whole prize: the small gas balance, plus the power to refuse service for a while. No user funds are reachable — the stakes sit in escrow contracts that independently verify every signature themselves. No administrative power is reachable — the keys that could change the contracts live offline, on physically separate storage, in an entirely different security tier. The blockchain re-checks and re-screens everything regardless of what the server claims. That’s what “censor, never steal” means as an engineering property rather than a slogan.

One checklist, two gasless systems

FairWins actually runs two flavors of gasless transactions, and this is where the split pays off twice.

The first is the one above: signed instructions submitted on a user’s behalf. The second serves the platform’s passkey wallets — accounts you unlock with Face ID or a fingerprint (the same WebAuthn standard your phone already uses for passwordless sign-in). For these, FairWins can sponsor the network fee directly, so the user pays nothing.

Rather than build a second server, the same bouncer simply grew a second door. Sponsoring a fee reuses the same emergency switch, the same sanctions screen, and the same rate limits — plus two extra ceilings that cap what any single sponsored action can cost, so one deliberately expensive request can’t drain the sponsorship fund. One perimeter, one audit trail, one emergency switch, covering two economically different systems. The same checklist has since taken on the platform’s other outside connections too — the collectibles marketplace and the prediction-market trading feature. This bouncer turned out to be the platform’s one reusable piece of backend.

Optional by construction: the never-stranded rule

All of this would still be a liability if people needed it. They don’t. A firm rule holds throughout: every action that can go through the gas-paying server must also complete perfectly well without it, landing exactly the same result on the blockchain — the user just pays their own small network fee.

The app enforces this automatically. Before asking you to sign, it quietly checks whether the server is healthy. If that check fails, the emergency switch is on, the network looks down, or anything goes wrong mid-flow, the app silently routes the very same action through your own wallet as an ordinary paid transaction. Same destination, same result; the only difference is who covered the fee. The sponsored-passkey path behaves the same way — any problem, and the confirmation screen honestly says the user is paying.

Because the worst case is merely “users pay their own fee,” the emergency switch is cheap to pull — which means operators will actually pull it when they should.

Design decisions

Build the judgment, buy the machinery. Transaction ordering, fee pricing, and connection failover are solved problems with sharp edges; a mature open-source engine handles them well. The judgment layer — which contracts, which actions, whose signatures, what limits — encodes FairWins-specific calls no off-the-shelf tool could know. The split puts custom code exactly where the custom decisions are.

Fail closed on money, fail soft on availability. Screening and address-pinning fail closed — the bouncer would rather refuse than guess. Availability fails soft — every refusal degrades to self-pay. The asymmetry is the whole point: the only thing this infrastructure is ever allowed to break is its own usefulness.

Further reading

Sanctions Screening as a Shared Building Block: One Guard, Every Money Path

Why FairWins moved compliance out of the frontend and into a single, fail-closed on-chain gate — and how the same small piece of code protects wagers, pools, memberships, and token issuance

The check that wasn’t there

Picture the code review. Your team ships wallet screening for a peer-to-peer wager platform: when a user connects, the website checks their address against a sanctions list, and if the address is listed, the interface refuses to proceed. The compliance box is ticked. The demo looks great.

A week later, someone on the security review asks the obvious question: what happens if a sanctioned address never opens your website? The contracts live on a public blockchain. Anyone with a script can call them directly, bypassing your site entirely. Your screening layer — the one your Terms of Service describe as a control — turns out to be a polite suggestion.

This is the trap that catches most “compliance-aware” crypto apps. Sanctions exposure under US law is strict liability: it doesn’t matter that you intended to block the address, or that your interface would have blocked it. If your contract accepted money from a listed address, the violation happened. A check that lives only in the website is not a control; it’s theater with good intentions.

FairWins’ answer is to treat sanctions screening the way a careful team treats any core safety check: as a shared building block baked into the contracts themselves. One small, shared piece of code — call it the sanctions guard — is consulted by every entry point on the platform where money moves. The website still checks first, for fast feedback and to avoid wasting anyone’s gas, but the layer that actually enforces is the one nobody can route around.

The guard itself

The guard is deliberately tiny: about a hundred lines, holds no funds, and can’t be upgraded. It combines two lists into a single yes-or-no verdict:

  1. A public sanctions oracle maintained by Chainalysis — an on-chain service that answers, for any address, whether it appears on the US Treasury’s sanctions list.
  2. A discretionary block-list the operator maintains — a simple list for addresses tied to illicit finance beyond the official set, editable only by the holder of the narrow compliance permission described in part 1 of this series.

Consumers get two ways to ask the guard a question. One returns a plain yes-or-no, for callers that want to branch on it — that’s what the website’s advisory check uses. The other simply stops the transaction cold if the address is blocked, which is the form the contracts use, because refusing to let a transaction complete is the cheapest and safest way to make sure a forbidden action never happens.

The block-list editing has one nice property worth calling out: every change records who made it, which address was affected, in which direction, and a human-readable reason — all written permanently to the blockchain. So the block-list’s entire history is a built-in audit trail. There’s no separate off-chain compliance database to subpoena or lose; the on-chain event log is the record. And the keys that can edit those lists follow the platform’s air-gapped, offline signing process, so no change happens casually.

Fail-closed, for real

The interesting engineering is in how the guard talks to the outside sanctions oracle. The naive version — just call the oracle and wrap it in a try/catch — has a sharp edge. If the oracle address is misconfigured, pointing at nothing or at the wrong network, that kind of failure isn’t reliably caught, and the system can silently start treating everyone as clean.

So the guard makes the query in a careful, low-level way that gives it full control over every possible failure: the oracle reverts, runs out of gas, returns nothing, or returns garbage. Anything short of a clean, well-formed “yes” or “no” is treated as the oracle gave no usable answer — and in that case the guard blocks every address. The rule is stated plainly in the platform’s requirements: if the screening source is unavailable, refuse the action rather than allow it unscreened. This is what “fail-closed” means — when in doubt, deny.

There’s exactly one deliberate exception, and it’s a configuration, not a failure. Setting the oracle to “none” means block-list-only enforcement — the intended posture for networks where Chainalysis simply doesn’t operate. Test networks get a stand-in; production gets the real oracle address injected at deployment, never hardcoded. The distinction is precise: a configured but broken oracle blocks everyone, while an intentionally unset oracle blocks only the block-list. Confusing those two states is exactly how fail-closed systems quietly turn fail-open.

One guard, four subsystems

What makes this a reusable building block rather than a one-off feature is that the same guard protects four independent parts of the platform in the same way:

Wagers. Every escrow entry point screens first. Creating a wager screens the creator. Accepting one screens both sides — the person accepting and the original creator — because acceptance is the moment the second stake enters escrow, and the creator might have been added to a sanctions list since they first posted the wager. Screen at every entry, every time.

Memberships. Buying, upgrading, extending, or redeeming a voucher into a membership all screen the person before any USDC moves. Even the admin-only path that grants a membership directly screens the recipient — the guard can’t be bypassed even by operators, so a permission-holder can’t accidentally hand access to a listed address.

Wager pools. Group pools screen creators and joiners through the same guard, with one extra safeguard worth stealing: on production networks the pool factory refuses to run at all if screening is supposed to be on but no guard is configured. The “unset means off” convenience that’s fine on a laptop becomes an impossible, boot-blocking state in production.

Token issuance and naming. Issuing a token screens the issuer and passes the same guard into every token it creates. The naming registry checks the guard before letting anyone claim a name.

Each of these holds its own pointer to the guard, so the guard can be swapped out without touching any of them — and a single block-list update propagates instantly to all four. One list, one place to edit it, one event stream, four enforcement points.

What is deliberately not screened

Here’s the design decision most teams get backwards: the exit paths — claiming a refund, claiming a payout, sweeping up expired wagers — are not screened.

The reasoning matters. If an address is added to the block-list after their stake is already sitting in escrow, screening the exit would permanently trap their funds inside your contract. That turns a screening control into an asset freeze — a much heavier and legally distinct act than simply refusing new business, and one that effectively makes your escrow contract a custodian of blocked property. FairWins draws the line cleanly: a listed address can take no new action that moves value in, but can always recover what’s already theirs. The guard gates entry, never exit.

Trade-offs

A little gas on every entry. Each screened action pays for an extra cross-contract call, plus a second one into the oracle when it’s set. That’s real overhead on the busy path. The team judged it worth it, because the alternative — screening only once, at membership time — leaves a gap: an address listed mid-membership could keep wagering until renewal. Re-screening at every entry closes it.

Trusting an outside oracle. The Chainalysis oracle is a centralized, permissioned data source, and FairWins takes its answers as ground truth. The safeguards are structural rather than trustless: the guard only ever reads from it, it can be swapped out if it’s ever compromised or retired, and the discretionary block-list keeps working even with the oracle unset. This is an honest trade — no decentralized sanctions feed exists, and pretending otherwise helps no one.

Fail-closed can mean downtime. If the oracle ever broke for everyone, every screened entry point would halt until an operator re-pointed or unset it. That’s the accepted cost of failing closed: a brief outage is recoverable; a strict-liability violation is not.

Defense in depth, not defense in one place. The on-chain guard is one layer of several: an edge network geo-gate that blocks restricted regions before a request even reaches the app, the website’s fast advisory check, versioned legal documents, and on-chain records of user consent. The guard is the layer that holds when every other layer is skipped — because on a public blockchain, one of them always can be.

Further reading

What “Be Your Own Bank” Actually Means

Self-custody in plain English: what a key is, why “not your keys, not your coins” caught on, and the responsibility that comes with the freedom

SeriesKnowledge Base
TrackWallets & Keys
LevelBeginner
AudienceCurious newcomers who use a bank app but have never held crypto
Tagsself-custody, wallets, keys, security, basics
Reading time~5 minutes

Who holds the vault key?

Think about the money in your bank account. You can see the balance in an app, tap to send some, and trust that it will be there tomorrow. But you are not the one actually holding it. The bank holds it. If you forget your password, the bank can reset it. If someone drains your account, the bank can often claw the money back. That safety net exists because a company sits in the middle, holding your money on your behalf.

That arrangement is called custody — someone else has custody of your funds. It is comfortable and familiar. It also means the bank can freeze your account, decline a payment, or close your access if it decides to.

Crypto offers a different arrangement, and it has a name that sounds bold: self-custody. It means you hold your own money directly, with no company standing in the middle. People sometimes call this “being your own bank.” This primer is about what that really involves — the freedom and the responsibility, honestly.

What self-custody actually is

In crypto, your money lives on a shared public ledger — a giant, tamper-resistant record that thousands of computers keep in sync. Nobody “has” your coins in a drawer. Instead, the ledger says a certain balance belongs to a certain account, and the only way to move that balance is to prove you control the account.

You prove it with a key — think of it as a secret password that also acts as a signature. Whoever holds the key can move the money. There is no manager to appeal to, no “forgot password” button that a company can press for you. The key is the ownership.

That is the whole idea. Self-custody means the key lives with you, not with a company. In older wallets, that key was often shown to you as a list of twelve or twenty-four words — a seed phrase — that you were told to write on paper and never lose. (Newer wallets, including FairWins, replace that with your phone’s built-in security — more on that in the next primer.)

Why people care: “not your keys, not your coins”

You will hear this phrase everywhere in crypto: “not your keys, not your coins.” It is a warning, and it comes from hard experience.

When you leave your crypto on an exchange or app that holds the keys for you, you are trusting that company the same way you trust a bank — except most crypto companies are not banks and have none of a bank’s protections. Several large ones have collapsed or been hacked, and people who thought they “owned” coins there discovered they only owned an IOU that the company could no longer honor.

The phrase means: if you do not personally hold the keys, you do not truly own the coins — you own a promise. Self-custody removes the middleman and the promise. Your money cannot be frozen by a company, cannot vanish in someone else’s bankruptcy, and does not require anyone’s permission to move.

The trade-off, stated honestly

Freedom has a price, and it would be dishonest to skip it: with self-custody, you are responsible for your own keys. There is no support line that can undo a mistake.

  • If you lose the only key and have no backup, the money is gone. Not “frozen pending review” — gone.
  • If someone tricks you into revealing your key or signing something you did not understand, they can take everything, and no one can reverse it.
  • Nobody can help you recover access the way a bank can, precisely because nobody else has your key.

This is not meant to scare you off. Millions of people self-custody safely by doing two simple things: keeping a backup so a single lost device is not the end, and slowing down before approving anything involving money. The rest of this track is about making those two things easy.

How this shows up in FairWins

FairWins is a self-custody app. When you join, an account is created that only you control — the keys never touch a FairWins server, and the company holds no master switch over your funds. That is a deliberate design choice: it is what lets FairWins honestly say your money is yours.

FairWins tries to keep the good part of self-custody (you are in control) while softening the scary part (one mistake ends everything). It does this two ways worth knowing now. First, it uses your phone’s own security hardware instead of a seed phrase you have to write down. Second, it strongly encourages you to add a backup controller — a second way to get into your account — before anything goes wrong, so a lost or broken phone is an inconvenience, not a catastrophe. The app will nudge you until you have one, on purpose.

What to watch out for

  • A backup is not optional. The single most common way people lose self-custodied money is having exactly one way in, then losing it. Set up a backup early, while everything is working.
  • No one legitimate ever needs your key or recovery words. Anyone who asks — “support,” a giveaway, a friend in a hurry — is trying to rob you. Real apps never ask.
  • Read before you approve. Signing a transaction is like signing a check that cannot bounce or be cancelled. FairWins shows you exactly what you are approving before you approve it; take the extra second to look.
  • Self-custody is a responsibility, not a personality test. If you are not ready for it on day one, that is fine — just do not keep more on any app than you would be comfortable managing.

Related deep-dive

Want the engineering details? Read Losing Every Passkey Shouldn’t Mean Losing the Account — how FairWins makes a self-custody account recoverable without bringing back the seed phrase.

Learn more

One Action, One Role: RBAC and the Operations Control Plane

How FairWins maps every privileged action to exactly one permission — and makes the operator dashboard prove it

The compliance officer who couldn’t reach her own tool

Picture a compliance officer at a wagering platform. Her job is narrow and serious: when an address needs to be blocked from the protocol, she adds it to a block-list, and the reason is recorded permanently on the blockchain. The system was built for exactly this. She holds one specific permission — the one that lets her edit the block-list — and nothing more. She cannot pause the platform, cannot touch the treasury, cannot freeze anyone’s account.

Then she opens the admin panel, and the block-list tab isn’t there.

This was a real gap FairWins found while auditing its own controls. The permission existed on the blockchain, and her account held it. But the admin dashboard had never been taught that this permission existed, so it hid the block-list behind the top-level “full administrator” permission instead. The underlying security was correct — and completely useless in practice. To do her narrow job, she would have needed to be handed the keys to everything, which is precisely the over-reach the narrow permission was designed to prevent.

The lesson generalizes. Access control isn’t only a smart-contract problem. A permission that exists on-chain but not in the operator’s screen creates quiet pressure to over-grant, and over-granting is how “least privilege” — the principle that every account should hold the minimum power it needs — dies in real life. This post walks through both halves of FairWins’ answer: the on-chain discipline of “one action, one role,” and the admin console built to mirror it, screen by screen.

The permission inventory: one paid role, six operator roles

FairWins is a peer-to-peer wager platform. Smart contracts hold each side’s stake in escrow and settle the bet against a trusted outside source of truth. Its permission model is deliberately small: one permission that members buy, and six that operators hold. All of them use a plain, well-audited access-control library from OpenZeppelin, an industry-standard toolkit for smart contracts — nothing exotic, nothing homegrown.

The paid one lets a member create and accept wagers; members buy it as a time-limited membership tier, and it’s the subject of part 2 in this series. The other six are the operator permissions, and each maps to a single, clearly bounded job:

  • Full administrator — protocol wiring, tier pricing, treasury withdrawals, and handing out or revoking the other permissions.
  • Guardian — can pause and un-pause the whole platform in an emergency. Nothing else.
  • Account moderator — can freeze and unfreeze an individual account. Cannot pause the platform.
  • Membership manager — can grant or revoke memberships directly. Cannot touch admin permissions.
  • Sanctions admin — can edit the compliance block-list. This is the compliance officer’s role.
  • Upgrader — can ship new versions of the upgradeable contracts. Cannot grant itself anything.

A handful of narrower, single-purpose permissions exist for individual subsystems — issuing tokens, setting fees, curating the naming registry — but they follow the exact same pattern, each scoped to one job.

One action, one role

The discipline that holds the whole model together is simple to state: every privileged action is guarded by exactly one permission. Not “admin or guardian.” Not a points system where enough small permissions add up to a big one. One gate per door.

Pausing the platform requires the guardian permission, full stop. Freezing an account requires the account-moderator permission, full stop. Changing pricing or protocol wiring requires the full-administrator permission. There’s one instructive exception that actually proves the rule: swapping out a piece of the wager engine’s code is treated as an upgrade — because it changes what code runs — so it requires the upgrader permission rather than the administrator one. The gate matches the true weight of the action.

The upgrader permission is kept separate from the top administrator permission on purpose. It can later be reassigned to a time-locked multi-signature wallet without changing any code, and the ability to perform future upgrades is wired in permanently, so an upgrade can never accidentally strip away the platform’s own ability to be upgraded again.

The negative space matters as much

FairWins keeps a table of what each role explicitly cannot do, and that table earns its keep. A guardian can stop the whole platform but cannot seize an account. A moderator can freeze an account but cannot pause the platform or move money. A membership manager can hand out memberships but cannot revoke anyone’s admin powers. The compliance officer can block an address and do nothing else. Even the full administrator has hard limits baked into the code: it cannot create wagers on someone’s behalf, cannot decide who wins, and cannot move staked funds — there is simply no button for any of those, because no such function was ever written.

When you design a permission, write its “cannot” list first. It tells you whether the role is genuinely narrow or just labeled that way.

The control plane: permissions flow from the contract to the screen

The operator side is a single admin console, reorganized after that audit into clear groups: a control room, incident response, compliance, membership and revenue, protocol config, identity, access control, and infrastructure.

The core rule of the interface mirrors the core rule of the contracts: each screen is shown only to operators who hold the on-chain permission its actions require, and a group of screens appears only if the operator can actually use at least one screen inside it. The dashboard calculates permissions the exact same way the contracts do and checks them against the live blockchain, so what an operator sees is a faithful picture of what they can actually do.

A guardian signing in sees the control room, incident response, and infrastructure — nothing else. The compliance officer from the opening now sees the control room and compliance. Crucially, the dashboard isn’t enforcing security — the contracts do that, and they can’t be fooled by a hidden or shown button. The dashboard’s job is to make the permission set legible, so nobody is ever tempted to over-grant a big role just to make a screen appear.

One subtlety the console gets right: different permissions live on different contracts, so when an operator grants a permission, the request has to be routed to the specific contract that defines it — not blanket-sent to one place and hoped for the best.

Design decisions

Why a plain, boring access-control library? Standard, audited permission checks are understood by every reviewer and every tool in the ecosystem. The “one action, one role” discipline delivers most of what elaborate custom permission systems promise, without introducing a new thing that can break or be attacked. The cost is granularity: withdrawing fees currently requires the full administrator permission, and splitting out a dedicated “treasurer” would take a contract upgrade — a noted future option, not a quick setting.

Why gate the UI on permissions at all, if the contracts already enforce them? Because the failure mode of a permission-blind dashboard isn’t a break-in — it’s privilege creep. The gap that started this story showed that when the interface doesn’t know about a permission, operators get handed a bigger one to compensate. Modeling every permission in the console is what keeps the on-chain least-privilege design honest in day-to-day operations.

What deliberately stays off the console. Two things are excluded by policy, not by accident. Anything touching the most sensitive keys — the ones that authorize upgrades — stays on offline, air-gapped, scripted paths that never touch a web form. And the optional relay service that can sponsor gasless transactions has no remote admin controls at all, on purpose: an internet-facing kill switch would be a brand-new attack surface, and the relay’s worst case is designed to be “refuses to help,” never “loses funds.” The console shows the state of both, read-only, and links to the written procedures.

Not everything needs a permission. Routine housekeeping — sweeping up expired wagers, settling ones whose outcome is already known — is open to anyone by design, so those screens are open to any operator. And some contracts have no admin controls whatsoever: no permission can drain or redirect their funds because no function to do so exists. The strongest access control is the door you never build.

Further reading