relayshield-snap
v0.1.0
Published
Screens the counterparty address of every MetaMask transaction against RelayShield before you sign.
Readme
RelayShield Counterparty Screening (MetaMask Snap)
Screens the counterparty address of every transaction against RelayShield's criminal intelligence corpus, in the MetaMask confirmation window, before the user signs.
What it does
onTransaction takes the to address, calls POST /v1/metered/wallet-risk, and renders one of
three things: a flagged verdict, a no-known-risk verdict, or an explicit not screened panel.
The rule this snap is built around
A failed check is never rendered as a clean result.
Every path that cannot produce a verdict returns the "Not screened" panel, which says in as many
words that this is the absence of a result rather than a clean one. That covers a network failure,
a rejected key, an empty balance, a non-200, an unparseable body, and an ok: false.
It also covers degraded: true. The API sets that when it assembled an answer from an upstream that
did not respond, which makes an empty risk_flags list an absence of data rather than an absence of
risk. Rendering that as "no known risk" would reproduce, inside a wallet at the moment of signing,
the same false-clean defect that shipped in three separate live billing endpoints in August 2026.
That is the single most important behaviour in src/index.tsx, and it is why Screening has
screened: false as a first-class variant rather than an error thrown away in a catch.
Funding: the user pays for their own screens
Founder decision, and the reason for the endpoint choice. The snap holds the user's own RelayShield
API key in snap_manageState and every call is billed to them at $0.05. We bundle no allowance.
The originally planned endpoint was /v1/payg/wallet-risk, which does not work here:
- Bare
/v1/wallet-riskis keyless and unbilled, so a key sent to it funds nothing. /v1/payg/wallet-riskis x402, and paying it from inside a snap requires signing a payment, which needs a key-management permission, which triggers the mandatory third-party audit this snap is deliberately scoped to avoid.
So /v1/metered/wallet-risk was added to relayshield_api.py on 2026-08-09: same handler, same
$0.05, reachable with the caller's own API key.
Permissions, and what is deliberately absent
endowment:transaction-insight read the pending transaction
endowment:network-access call api.relayshield.net
endowment:page-home configuration screen
snap_dialog key entry
snap_manageState store the user's keyallowTransactionOrigin is not requested: the snap does not need to know which dapp initiated
the transaction, and asking for less is both better for the user and better at review time.
There is no endowment:keyring, no snap_getBip44Entropy and no snap_getEntropy. The snap never
touches a key, a seed or a signature. That is what keeps it outside the mandatory audit requirement.
Build
npm install
npm run buildThe build must run from a path with no spaces in it. mm-snap build URL-encodes the project
path when it launches its eval worker and then fails to resolve it, so building from
/Users/.../Side SaaS Hustle/relayshield-snap dies with
Cannot find module '.../Side%20SaaS%20Hustle/.../eval-worker.cjs'. The bundle itself is fine; it
is only the post-build evaluation that breaks. Copy the package to a space-free directory, build
there, and copy dist/ and snap.manifest.json back.
Key entry
onHomePage renders a form when no key is set and a "Remove key" button when one is. onUserInput
handles both and re-renders the page in place with a success or failure banner.
The key is validated locally against rs_live_ plus 32 hex characters before it is stored. That
format comes from f"rs_live_{uuid.uuid4().hex}" in relayshield_developer_signup.py. Catching a
paste error here matters: without it the first sign the user hears about it is a 401 rendered over a
pending transaction, which is the worst possible moment.
Removing a key uses snap_manageState clear rather than writing an empty string, so nothing is
left behind.
Where users find us
Three links to api.relayshield.net/developers?source=metamask-snap: the unconfigured transaction
panel, the home page when no key is set, and the home page footer. The metamask-snap source key is
registered in _SOURCE_BANNERS so those arrivals get an answer to the question they arrived with.
That registration went in at the same time as the link on purpose, because rsscan shipped its link
first, was silently aliased to the generic GitHub banner, and spent months sending arrivals to a
paragraph about client libraries.
Not done yet
- Not published to npm, so the manifest
packageNameandrepositorypoint at a repo that does not exist yet. - EVM only, which matches MetaMask. The underlying endpoint also handles Solana, TON and Bitcoin.
- No local caching, so one screen per transaction. Worth revisiting alongside the freshness work.
