The full upstream README, mirrored here for reference. Install config, tool schemas, adoption signals, and an original overview live on the Presign Guard listing page.
A pre-sign risk check for AI agents. Before an agent signs a transaction, approval, or EIP-712 signature, it pays a few cents per call over x402 and gets back a green / orange / red verdict with machine-readable reason codes. Optionally, it also gets a plain-language explanation in Dutch or English.
Watch the 1-minute explainer: presign-guard.fizzl.eu/media/explainer.mp4
New: the token verdict in 45 seconds: presign-guard.fizzl.eu/media/token.mp4
Part of Klaartaal by FIZZL AI.
| Route | Price | Returns |
|---|---|---|
POST /v1/check | $0.01 USDC | Verdict, reason codes, decoded subject |
POST /v1/check/explain | $0.03 USDC | The same, plus a plain-language explanation (lang: "nl" or "en") |
GET /v1/token?chain=…&address=… | $0.01 USDC, Base or Solana | Token verdict: grade, reason codes, one-line summary, market data (see below) |
GET /v1/approvals?chain=…&address=… | $0.02 USDC, Base or Solana | Wallet approval audit: every open token approval, its spender, and which to revoke (see below) |
POST /mcp | free / paid | MCP server (Streamable HTTP): see below |
POST /feedback | free | Report a bug or a missing feature: see Feedback |
GET /health | free | Liveness |
GET /openapi.json | free | OpenAPI 3.1 spec with prices (x-payment-info) |
GET /.well-known/x402 | free | x402 discovery manifest |
Payment is x402 v2 with the exact scheme, in USDC on Base (the token verdict and the approval audit also on Solana). The 402 carries Bazaar discovery metadata (input example, input and output schema), and the challenge is mirrored into the JSON body for clients that don't read the PAYMENT-REQUIRED header. You are never charged for an error. Invalid requests (400) and upstream outages (503) cancel settlement, and they always return verdict: null, never a guessed verdict.
https://presign-guard.fizzl.eu/mcp is an MCP server (Streamable HTTP, stateless) for Claude, Cursor and agent frameworks, listed in the official MCP registry as io.github.Fizzl13/presign-guard.
| Tool | Price | Returns |
|---|---|---|
presign_quick_check | free, 10 calls/hour | The verdict only (green, orange or red) |
presign_check | $0.01 USDC via x402 | The full verdict and reason codes, as POST /v1/check |
presign_check_explain | $0.03 USDC via x402 | The same plus a plain-language explanation, as POST /v1/check/explain |
token_quick_verdict | free, shares the 10 calls/hour | The token verdict and grade only |
token_verdict | $0.01 USDC via x402 | The full token verdict, as GET /v1/token |
wallet_approvals | $0.02 USDC via x402 | The wallet approval audit, as GET /v1/approvals |
feedback | free | Report a bug or a missing feature, as POST /feedback |
The paid tools are paid inside the MCP call with the x402 MCP transport (_meta["x402/payment"]), on Base (token_verdict and wallet_approvals also on Solana), to the same payout wallets as the HTTP routes. Invalid input is refused before payment, and a failed check is not charged.
GET /v1/token?chain=solana&address=<mint> (or chain=base|ethereum|arbitrum|optimism|polygon|bsc with a 0x token contract) answers one question before an agent buys, holds or accepts a token: is the token itself a trap?
Grades: SAFE (green), CAUTION (one orange reason), RISKY (two or more), AVOID (red). The one-liner states facts only.
| Severity | Codes |
|---|---|
| red | RUGGED, NON_TRANSFERABLE, MALICIOUS_AUTHORITY; EVM: TOKEN_HONEYPOT, TOKEN_AIRDROP_SCAM, TOKEN_IMPERSONATION |
| orange | MINT_AUTHORITY_ACTIVE, FREEZE_AUTHORITY_ACTIVE, BALANCE_MUTABLE, CLOSABLE, TRANSFER_HOOK, TRANSFER_FEE, HIGH_TRANSFER_FEE (≥10%), TRANSFER_FEE_UPGRADABLE, LP_NOT_LOCKED (<50% locked, token younger than 30 days), LOW_LIQUIDITY (<$50k), NO_DEX_MARKET, NEW_TOKEN (<24 h), TOP_HOLDERS_CONCENTRATED (top holder >20% or top 10 >50%, pools and locked accounts excluded; on EVM only wallets count, not contracts); EVM: the GoPlus token codes of /v1/check (TOKEN_HIGH_TAX, TOKEN_UNVERIFIED, …), TOKEN_CANNOT_BUY and TOKEN_PAUSED (transfers are paused right now) |
| info | MUTABLE_METADATA, NO_SOCIALS, TOKEN_ON_TRUST_LIST, NO_SECURITY_DATA, RUGCHECK_DANGER, RUGCHECK_UNAVAILABLE, LP_NOT_LOCKED on older tokens, on trust-list tokens (USDC, USDT, WETH): the issuer's powers, LP_NOT_LOCKED, LOW_LIQUIDITY and NO_DEX_MARKET (DexScreener undercounts quote assets); EVM: TOKEN_PAUSABLE and TOKEN_BLACKLIST (the issuer can pause transfers or blacklist holders; a green one-liner says so) |
For a proxy token (USDC, cbBTC, EURC on Base) GoPlus reports no pause or blacklist fields, so the verdict asks the token itself with eth_call on the standard getters (paused, pauser, PAUSER_ROLE, blacklister, isBlacklisted, isBlackListed); those reasons carry source: "onchain" and the role address, and sources includes chain. If the chain RPC is down they are left out and the verdict still answers.
On a token on the GoPlus trust list (USDC, USDT), the issuer's powers (mint, freeze, change balances) are info: the issuer keeps them on purpose. Missing data never makes a token red. If GoPlus or DexScreener is down there is no verdict (503, not charged); if RugCheck is down the verdict comes without it and says so.
GET /v1/approvals?chain=base&address=<wallet> (or chain=ethereum|arbitrum|optimism|polygon|bsc) is the follow-up to /v1/check: that one asks "should I sign this approval?", this one asks "which approvals did I already give, and which should I revoke?". Agents with their own wallet can run it as a periodic check.
| Severity | Codes (per approval; the wallet-level reasons count them) |
|---|---|
| red | SPENDER_MALICIOUS (the spender is flagged by GoPlus) |
| orange | APPROVAL_TO_WALLET (the spender is a plain wallet, not a contract), SPENDER_SUSPICIOUS (GoPlus doubt list), SPENDER_UNVERIFIED (contract source not verified), UNLIMITED_APPROVAL (to a spender not on the GoPlus trust list) |
| info | UNLIMITED_APPROVAL_TRUSTED (e.g. Permit2), STALE_APPROVAL (older than a year), TOKEN_FLAGGED (the approved token itself), NO_APPROVALS |
Every approval with an orange or red code has revoke: true. The grades are the same as the token verdict. Source: GoPlus token_approval_security (ERC-20 allowances); NFT approvals are not covered. If GoPlus is down there is no verdict (503, not charged).
Supported chains: 1, 10, 56, 137, 8453, 42161.
Optional on every type: "origin": "https://…", the site asking for the signature or transaction (a URL or a hostname). Its domain age is looked up: a domain registered less than 30 days ago is orange (NEW_DOMAIN), which catches the fresh phishing sites wallet drainers run on. It is also checked against phishing lists and lookalikes: on MetaMask's eth-phishing-detect blocklist (via PG1) or GoPlus's phishing list is red (PHISHING_SITE, details.flaggedBy names the lists); a lookalike of a known brand (PG1's typosquat detection, e.g. metamask-login.com → metamask.io) is orange (LOOKALIKE_SITE); with METAMASK_SCAN=on (off by default: MetaMask's site-scanner endpoint has no published API or licence), a site MetaMask's own site scanner (Blockaid) blocks is orange (WALLET_BLOCKS_SITE), not red, because that scanner also blocks legitimate browser-payment pages. A source that can't be reached is SITE_REPUTATION_UNAVAILABLE (info), never a clean result. ORIGIN_REPUTATION=off turns these checks off. Local and IP origins are not looked up.
Recognised signatures: EIP-2612 Permit, DAI-style permit, Permit2 (PermitSingle, PermitBatch, PermitTransferFrom, batch and witness variants), EIP-3009 TransferWithAuthorization / ReceiveWithAuthorization (what x402 asks an agent to sign to pay), and Seaport OrderComponents.
An x402 payment moves one fixed amount to one recipient and grants no allowance, so paying a plain wallet is green (PAYMENT_AUTHORIZATION, info). It turns red if the recipient is flagged, and orange if the amount is effectively unlimited or the authorization stays valid for more than a month. Anything else comes back at least orange (UNRECOGNIZED_SIGNATURE).
Every token that is approved, permitted or paid is also checked with GoPlus token security (honeypot, impersonation, owner powers, taxes). The verdict is the most severe reason: any red makes it red, otherwise any orange makes it orange. info reasons never change the verdict.
| Severity | Codes |
|---|---|
| red | PHISHING_SITE (the origin is on a phishing list; details.flaggedBy), PHISHING_ACTIVITIES, STEALING_ATTACK, SANCTIONED and other GoPlus address flags, MALICIOUS_DELEGATE (an EIP-7702 wallet delegates to a flagged contract; details name the delegate and the flags), SANCTIONED_ADDRESS (on the OFAC SDN list; details name the SDN entry and program, and alsoFlaggedBy: ["goplus"] replaces GoPlus's own SANCTIONED for the same address), CREATOR_OF_MALICIOUS_CONTRACTS, MALICIOUS_CONTRACT_BEHAVIOR, ON_DOUBT_LIST, UNLIMITED_APPROVAL_TO_EOA, SIGNATURE_GRANT_TO_EOA, ORDER_PAYS_YOU_NOTHING, and for the token itself TOKEN_HONEYPOT, TOKEN_IMPERSONATION (details name the real token), TOKEN_AIRDROP_SCAM |
| orange | LOOKALIKE_SITE (origin imitates a known brand), WALLET_BLOCKS_SITE (MetaMask's site scanner blocks the origin), UNVERIFIED_DELEGATE (an EIP-7702 wallet delegates to unverified code), UNLIMITED_APPROVAL, UNLIMITED_TRANSFER, APPROVAL_FOR_ALL, APPROVAL_TO_EOA, SIGNATURE_TRANSFER, LONG_LIVED_PERMISSION, NONCANONICAL_PERMIT2, UNVERIFIED_CONTRACT, RECENTLY_DEPLOYED, MARKETPLACE_ORDER, UNRECOGNIZED_SIGNATURE, BLACKLIST_DOUBT, MIXER, NEW_DOMAIN (origin registered under 30 days ago), DOMAIN_NOT_REGISTERED, and for the token TOKEN_PAUSED (transfers are paused right now), TOKEN_OWNER_CAN_CHANGE_BALANCES, TOKEN_OWNERSHIP_RECLAIMABLE, TOKEN_HIDDEN_OWNER, TOKEN_SELFDESTRUCT, TOKEN_CANNOT_SELL_ALL, TOKEN_CREATOR_MADE_HONEYPOTS, TOKEN_HIGH_TAX (buy or sell tax of 10% or more), TOKEN_UNVERIFIED |
| info | EIP7702_DELEGATED_WALLET (a plain wallet with EIP-7702 code, treated as a wallet; details name the delegate contract, which is screened like a spender), PARTIAL_SOURCE_DATA (GoPlus returned partial data for this address), PAYMENT_AUTHORIZATION, REVOKES_APPROVAL, OFFCHAIN_SIGNATURE, SIGNATURE_EXPIRED, UPGRADEABLE_PROXY (only when the chain confirms a standard proxy layout: EIP-1967 or its beacon, EIP-1822, the older OpenZeppelin slot or an EIP-1167 clone; details name the kind and what it points to, or confirmed: false when the chain RPC can't be asked), ON_TRUST_LIST, UNDECODED_CALL, and issuer controls on the token (USDC has several): TOKEN_MINTABLE, TOKEN_PAUSABLE, TOKEN_BLACKLIST (for a proxy token GoPlus has no answer, so the chain is asked, as in the token verdict: source: "onchain" and the role address), TOKEN_UPGRADEABLE, TOKEN_TAX_MODIFIABLE, TOKEN_TRADING_COOLDOWN, TOKEN_TAX, TOKEN_ON_TRUST_LIST, TOKEN_NO_SECURITY_DATA (GoPlus has no record of the token), DOMAIN_AGE, DOMAIN_AGE_UNKNOWN (no RDAP data for that TLD), SITE_ALLOWLISTED (origin on MetaMask's allowlist), SITE_REPUTATION_UNAVAILABLE (a phishing-list source could not be reached; details.sources), and when PG1 can't be reached SANCTIONS_SCREEN_UNAVAILABLE / DOMAIN_AGE_UNAVAILABLE (the check goes on; GoPlus still carries a sanctions flag) |
Every paid answer (HTTP and MCP) carries a receipt signed by presign-guard, so you can later prove which verdict was delivered for which request, not only that you paid:
receipt.signature left out, as canonical JSON (profile js-json-stringify-sorted-utf16-ascii-v1: keys sorted by UTF-16 code units at every level, no whitespace, every code unit from U+007F up escaped as lowercase \uXXXX, numbers spelled as JavaScript's JSON.stringify writes them — 1.0 → 1, 0.000001 → 0.000001, 1e21 → 1e+21 — then UTF-8 bytes). Python's json.dumps matches only for ASCII keys and integers (it writes 1.0 and 1e-06); use examples/canonical.py, with EIP-191 personal_sign. Flipping the verdict, or moving it to another request id, breaks the signature.input_sha256 is the SHA-256 of the canonical JSON of {"route": …, "input": …}, where input is your JSON body (POST), your query parameters as strings (GET), or the tool arguments (MCP, route mcp <tool>). Recompute it to prove the verdict answers your request.payment ties the verdict to the payment that bought it, from your x402 payment payload. On Base: the payer and the EIP-3009 nonce, so anyone can find the settlement on-chain as the USDC contract's AuthorizationUsed(payer, nonce) event without trusting us. On Solana: the payer and a SHA-256 of the signed transaction you sent (the facilitator adds its fee-payer signature at settlement, so the final transaction id is not known when we sign)./.well-known/presign-guard-signer.json. Retired signers stay listed with their dates, so old receipts keep verifying.POST /v1/verify with {"response": <the signed answer>, "route": "POST /v1/check", "input": <what you sent>} returns valid, signer, known_signer and input_matches.Verify it yourself (Node, viem):
Python (eth-account), with examples/canonical.py (not json.dumps, which spells numbers differently):
Key rotation without client updates: the payout wallet (0x6B0F4651eD42893ab58139938175E4a69f175F25, the payTo of every payment) authorises each signing key with a certificate: a personal_sign over
Set it as RECEIPT_SIGNER_CERT (YYYY-MM-DD:0x<signature>; the page /sign-receipt-key produces it from the wallet's browser; /sign-receipt-key?service=x402-doctor does the same for x402 Doctor, so the payout wallet signs on one site only). It is checked at startup and carried inside every receipt as receipt.cert, so a client that pins only the payout wallet verifies receipts from a new key offline. To rotate: generate a new RECEIPT_SIGNER_SECRET, sign a new certificate, move the old address to RECEIPT_RETIRED_SIGNERS.
The signing key comes from RECEIPT_SIGNER_SECRET (any long random string; it holds no funds and signs nothing but receipts). Without it, answers are unsigned.
eth_sign and personal_sign messages, and transaction simulation. Treat a green verdict as "no known risk signals", not as a guarantee.
Found a bug, or missing something? Send it with POST /feedback (free, no payment) or the MCP tool feedback:
type is bug, feature or other; message is required (up to 2000 characters); endpoint and contact are optional. The answer is 202 with an id. At most 10 reports per hour per caller. Reports go to the usage log, and a person reads every one; nothing in a report is run or changed automatically. GET /feedback shows the schema.
Fund a throwaway wallet with Base Sepolia test USDC, set AGENT_PRIVATE_KEY and CHECK_URL in .env, then run:
The script makes a valid call, which should return 200 with a settlement receipt, and an invalid call, which should return 400 with no charge.
The live service runs on Base mainnet: render.yaml sets X402_NETWORK=eip155:8453, which needs CDP_API_KEY_ID and CDP_API_KEY_SECRET (Coinbase CDP facilitator; the service refuses to start on mainnet without them). After the first paid call settles through CDP, the routes are listed in the CDP Bazaar. To test without real money, set X402_NETWORK=eip155:84532 (Base Sepolia, public x402.org facilitator); without X402_NETWORK the code also defaults to Base Sepolia.
PAY_TO_SOLANA (optional) is a Solana address for USDC payments on Solana, offered for the token verdict only; those payments settle through the PayAI facilitator (SOLANA_FACILITATOR_URL to override). Without it, the token verdict is paid on Base only.
PAY_TO must be an EVM address (0x + 40 hex characters). Surrounding spaces are trimmed; anything else stops the server at startup with a clear error, so a typo can't publish an unpayable 402.
Risk data comes from the GoPlus Security API. OFAC SDN sanctions screening and domain age come from PG1 (public OFAC and RDAP data, credited as pg1 in sources; set PG1_API_KEY to a PG1 membership key to be exempt from PG1's anonymous rate limit); the token verdict adds RugCheck (Solana) and DexScreener (market data). Plus eth_getCode on a public RPC to recognise EIP-7702 wallets (override with RPC_URL_<chainId>). Explanations come from Claude (Anthropic).