scamgate
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@scamgatecheck https://paypa1.com/login for scam risk before I buy a ticket"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.

scamgate
A self-hosted scam check for AI agents: verify links and payments before the agent acts.
Website and docs: https://odaiameera.github.io/scamgate/
AI agents now click links and make payments for people, and scammers follow the money. Text planted in pages, listings or messages can steer an agent to a fake shop, a credential prompt or a "pay by bank transfer" request. Agent payment rails secure the payment (tokens, limits, authentication). They don't judge whether the payee or the site is a scam. scamgate does that check, runs locally as an MCP server, and makes no network calls while checking.
How it decides
Deterministic rules, no LLM judging. The same input always gets the same verdict.
Each rule that fires adds points and a plain-English reason. The total (capped at 100) decides:
allow: under 25
ask (stop and check with the human): 25 to 69
deny: 70 or more
The agent gets the decision, the score, every reason, and any warnings (for example a stale feed cache).
Link rules: known-bad-link feeds, your own blocklist, brand look-alikes (typosquats,
homoglyphs, brand names on unofficial domains, real domains used as a prefix like
paypal.com.secure-login.xyz), addresses hidden before an @ (paypal.com@evil.example), raw
IP hosts, punycode, cheap abused TLDs, and pages asking for a seed phrase or a login the task
doesn't need. A link it can't read (an invalid site name, a javascript: or data: link, an
oversized address) asks: it never gets an allow.
Payment rules: new payee plus urgency, changed bank details, irreversible methods (crypto, gift cards, transfer to a new payee), sellers insisting on a method, over budget, pressure or secrecy phrases, and Verification of Payee results.
Related MCP server: attest-mcp
Quickstart
Requires Python 3.11+.
# with uv
git clone https://github.com/odaiameera/scamgate.git
cd scamgate
uv sync
uv run scamgate update-feeds # download the known-bad-link feeds into ~/.cache/scamgate
# or from PyPI, pinned to a release
uv tool install scamgate==0.3.3 # or: pip install scamgate==0.3.3
scamgate update-feedsUsing Claude Code? Install the Claude Code plugin instead: it brings the hook, the MCP tools and the feed refresh in one step.
Check a link (real output):
$ uv run scamgate check-url "https://paypa1.com/login" --intent "buy a ticket"
{
"decision": "deny",
"risk_score": 100,
"reasons": [
{
"rule_id": "url.lookalike_brand",
"explanation": "'paypa1.com' disguises 'paypal' with look-alike characters ('paypa1'). The real site is paypal.com.",
"weight": 60
},
{
"rule_id": "url.intent_mismatch",
"explanation": "The task is 'buy a ticket', but the link goes to a 'login' page. A credential prompt the task doesn't need is a classic phishing step.",
"weight": 45
}
],
"summary": "Do not proceed with this link. Main concern: 'paypa1.com' disguises 'paypal' with look-alike characters ('paypa1'). The real site is paypal.com.",
"checked_host": "paypa1.com",
"warnings": [],
"advisory": "scamgate is advisory. It can miss scams and flag legitimate actions; a human should make the final call on anything flagged."
}(warnings is empty once feeds are cached. Before update-feeds it says no feed is cached.)
A link listed in a feed comes back with the feed named in the reason:
{
"rule_id": "url.known_bad_feed",
"explanation": "This exact link is listed as phishing/malware by OpenPhish community feed.",
"weight": 100
}Other commands: scamgate feeds-status, scamgate bench, scamgate serve (MCP over stdio),
plus hook, review, promote and record-payment (see Enforce it with a hook).
Add it to your agent
Claude Code plugin
The easiest way for Claude Code. One install adds the enforcing hook, the check_url, check_payment and
update_feeds tools, and a refresh of the phishing lists every 6 hours. It runs scamgate from PyPI with
uvx scamgate==0.3.3, so it needs uv, and Claude Code v2.1.295 or later so
that a hook that can't start blocks instead of letting calls through.
/plugin install scamgate --marketplace odaiameera/scamgateOn Claude Code before 2.1.275, run /plugin marketplace add odaiameera/scamgate, then
/plugin install scamgate@scamgate. What the plugin runs, downloads and stores:
claude-plugin/README.md.
MCP server only
Replace /path/to/scamgate with your checkout, or run the PyPI release with uvx scamgate==0.3.3 serve.
Claude Desktop (claude_desktop_config.json):
{
"mcpServers": {
"scamgate": {
"command": "uv",
"args": ["--directory", "/path/to/scamgate", "run", "scamgate", "serve"]
}
}
}Claude Code:
claude mcp add scamgate -- uv --directory /path/to/scamgate run scamgate serveHermes (~/.hermes/config.yaml):
mcp_servers:
scamgate:
command: "uv"
args: ["--directory", "/path/to/scamgate", "run", "scamgate", "serve"]Then tell the agent: "Call check_url before opening any link and check_payment before any
payment. On ask, stop and show me the reasons. On deny, don't proceed."
MCP tools
check_url(url: str, intent: str | None = None) -> Verdict
update_feeds() -> {updated, failed, not_configured, skipped, summary}
check_payment(payee_name: str, payee_account: str, amount: float, currency: str,
method: "card" | "bank_transfer" | "crypto" | "gift_card" | "other",
context_text: str | None = None, is_new_payee: bool | None = None,
bank_details_changed: bool | None = None, budget: float | None = None,
vop_result: "match" | "close_match" | "no_match" | "unavailable" | None = None) -> VerdictVerdict = decision, risk_score, reasons[] (rule_id, explanation, weight),
summary, checked_host (for links: the host a browser would open), warnings[], advisory.
Both check tools are marked read-only and publish this as their output schema, so MCP clients get it as
structured content. update_feeds takes no arguments: it downloads the default lists from their fixed
sources, at most once an hour, so an agent can ask for a refresh but can't choose what lands in the cache.
Enforce it with a hook
The MCP tools only work when the agent chooses to call them, and a prompt injection can tell it not to. The hook moves the check into the harness: the agent can't skip it.
scamgate-hook (or scamgate hook) reads one hook event as JSON on stdin and writes a decision.
It speaks Hermes shell hooks and Claude Code PreToolUse hooks.
Links: every URL in a web, browser, media, terminal, code or unknown MCP tool call is checked, including disguised forms (
evilsh%6fp.com,http://3405803781).denyblocks the call,asksends it to you for approval. One bad link in a batch blocks the whole batch.Payments: money-moving tool calls (wallet, banking or payments MCP tools, or any tool whose arguments carry an amount and an IBAN, wallet or account) get the same check as
check_payment, with your payee history, whatever the agent claims.denyblocks,asksends it to you. A payment whose payee or amount can't be read, crypto CLIs (cast send,bitcoin-cli sendtoaddress,solana transfer…) and write calls to payment APIs (Stripe, PayPal, Wise, Revolut Business…) go to you. Each approval covers one account and amount.Hidden code: code that's decoded or downloaded and then run (
… | base64 -d | bash,exec(b64decode(…)),curl … | sh,powershell -enc) goes to you, network keyword or not.Hidden destinations: a network step scamgate can't read goes to you, judged per command (
curl https://example.org; curl "$URL"still asks): variables,$(…), string-built, base64/hex-decoded or reversed links,requests.get(open(…).read()), installs of named packages,git clone, new git remotes. In code, only a literal URL counts as readable.Routine dev work stays quiet: installs from a lockfile, manifest or local path (
pip install -r requirements.txt,uv sync,npm ci),git push/pull/fetchto remotes you already have, and commands that only read text (grep requests.get src/). Any URL in them is still checked.Running blind: with no feeds cached, links go to you for approval (
SCAMGATE_NO_FEEDS=warnturns that into a warning). Stale feeds are mentioned in every ask and block.Tampering: blocks agent writes (including editors like vim/ed/nano and
find -exec,rsync,patch) to scamgate's own code, state and feed cache and to the hook config, plus commands or code that revoke the hook, remove the server, promote rules, record payees or call scamgate's state APIs directly.The agent's stated reason can only add risk. It's scored with and without the reason, and the higher score wins.
Approvals stay narrow. Hermes remembers "always" per
rule_key; scamgate's keys include the host and the rules that fired (or a hash of the hidden command), so one "always" can't pre-approve other destinations. Still, pick "once" for asks you don't expect to repeat.Fails closed: bad input, input over 2 MB, deep nesting or an internal error blocks the call (exit 2).
Stays fast on long commands. The hook reads a command's words in one linear pass (no
shlex), so a padded command can't tie it up: a 2,000,000-character quoted command takes about 5 s on one Windows machine, down from over 200 s. Decisions are unchanged.Never says "allow". Silence means "no objection", so it can only tighten your agent's permissions, never loosen them.
Pipeline
hook.handle() is the only enforcement function. The shell hook and the Hermes plugin both call it. MCP does not.
Integrity check on a wheel install. A mismatch blocks every gated call. Editable and unpackaged installs pass as
dev.scamgate's own
check_url/check_paymenttools return silence, so asking the gate is not itself blocked.Tamper: writes to the package, state, feed cache, hook config, and other Hermes profiles, plus commands that uninstall the gate or call
promote/record-payment.Extra checks: reading the signing key; Hermes safety switches (
approvals.mode off,--yolo,plugins uninstall,--safe-mode,--dangerously-skip-permissions); writes to files other tools run; cron scripts under$HERMES_HOME/scripts; script files named bybash/python/node/pwsh/uv run, followed a few levels deep.URL extraction.
https:/hostandhttps:///hostare rewritten tohttps://host. Hosts are NFKC-normalised, percent-decoded, and numeric IPv4 is expanded. Segments split on newlines,;,&&,||and|.Score each URL with and without the agent's reason. Take the higher score, then the worst decision. An unreadable destination asks. More than 20 clean URLs asks.
A payment result wins a tie with a generic network ask. A URL deny still beats a payment ask.
Render
{}for silence, Hermesblock/approve, or Claudedeny/ask. Log the ask or deny.
rule_key is the host plus the rule ids, or a hash of the opaque command. One "always" does not cover another host.
The plugin sees per-chat /yolo and approvals.mode: off, and turns an ask into a block unless when_approvals_off is allow. The shell hook cannot see that session state. Measured on one Windows machine: about 0.5–0.7 s per shell-hook process, about 10 ms in-process. Not a benchmark suite.
It does not see a redirect after the request has left, the behaviour of a compiled binary, or a script that is not named in the call. Same-user code outside the agent can still read the signing key. Details: docs, Mechanics.
Hermes (~/.hermes/config.yaml). Needs a Hermes build whose shell hooks understand
"action": "approve" (older builds dropped it, turning every ask into a pass; fixed in issue
#92553, Sep 2026). Hermes asks you to approve a new hook the first time it runs in an interactive
session. Check it with hermes hooks list or hermes hooks doctor:
hooks:
pre_tool_call:
- matcher: ".*" # scamgate passes non-network tools straight through
command: "scamgate-hook"
timeout: 15
fail_closed: true
post_tool_call: # optional: records whether you approved each ask
- matcher: ".*"
command: "scamgate-hook"
timeout: 15Note: with the shell hook, /yolo or approvals.mode: off auto-approves every scamgate ask
(blocks still hold). The Hermes plugin below turns those asks into blocks instead, unless you set
plugins.entries.scamgate.settings.when_approvals_off: allow.
Hermes plugin (preferred). Same checks as the shell hook, in-process,
about 50× faster, and it can see a per-chat /yolo. Needs Hermes 0.19.1+. It's in the Hermes plugin
catalog, or install the src/scamgate directory as a plugin. It registers pre_tool_call,
scamgate_check_url, scamgate_check_payment, scamgate_update_feeds, /scamgate status | review | update and hermes scamgate update-feeds | feeds-status. It keeps the phishing lists fresh by itself:
see In the Hermes plugin. A plugin install has no scamgate command on
PATH; use /scamgate or hermes scamgate instead.
The Claude Code plugin sets this up for you. To wire it by hand instead:
Claude Code (.claude/settings.json). Needs Claude Code v2.1.295+ for onFailure.
Without it, a hook that crashes, times out or can't start lets the tool call through:
{"hooks": {
"PreToolUse": [{"matcher": ".*",
"hooks": [{"type": "command", "command": "/absolute/path/to/scamgate-hook",
"timeout": 15, "onFailure": "block"}]}],
"PostToolUse": [{"matcher": ".*",
"hooks": [{"type": "command", "command": "/absolute/path/to/scamgate-hook"}]}]
}}Use the absolute path (uv tool dir --bin shows it) so nothing earlier on PATH can stand in for
the hook.
Install a pinned copy for the hook (uv tool install scamgate==0.3.3, or git+https://github.com/odaiameera/scamgate@v0.3.3)
rather than running from a source checkout: an installed copy checks its own files against the
hashes recorded at install time on every call, and blocks everything if they changed. A source
checkout has nothing to check against.
Each gated call costs one process start (about 0.7 s on Windows). Calls that don't touch the network, or scamgate's files, pass straight through.
Ask log, review and promote
Every ask and deny is logged to $SCAMGATE_STATE_DIR (default ~/.local/state/scamgate)
with redacted targets, the rules that fired, the score, the agent's stated reason (stored for
you to read and marked untrusted; in scoring it can only add risk) and whether you then let it run.
scamgate review --days 7 # which hosts keep tripping it, and what you did
scamgate promote --host shop.example --decision deny
scamgate promote --host shop.example --decision allow --rule url.userinfoOnly a person can add an override. promote asks for confirmation, and refuses to run without an
interactive terminal or inside a known agent (Claude Code, Hermes, Codex), even when an agent
imports scamgate's code directly. Every override line is signed with a key in the state dir, so a
line typed into the file any other way is ignored. A deny covers the host and its subdomains.
An allow is narrow: it waives one rule on one exact host, and can never waive known-bad
feeds, your blocklist, intent checks or your own deny rules, so an attacker can't train the gate
into trusting a whole domain.
Payee history
check_payment doesn't take the agent's word for is_new_payee. It looks the payee up in a
local history of people you've actually paid (stored as salted hashes, never names or account
numbers). It flags:
a payee you've paid before showing up with a different account (changed bank details);
a near-name with a new account ("Acme Trading Ltd", "Acmee Ltd", a Cyrillic "е" in "Acme"): the classic invoice-redirection trick;
an account you've paid before under a different name;
an agent calling a payee "known" when there's no record of paying them (adds risk).
Only you add to it, after a real payment, the same way as overrides: interactive, not inside an agent, signed lines. If the history's key or salt goes missing, every payee is treated as new with changed details, rather than silently forgetting everyone.
scamgate record-payment --payee "Acme Ltd" --account IE29AIBK93115212345678Domain age (opt-in)
New domains are a strong scam-shop signal (url.domain_age, 30 points under 30 days old). Looking
one up asks the domain's registry over RDAP, which tells the registry which site you're checking,
so it's off unless you choose:
| What happens |
| No lookups. The rule never fires. |
| Uses ages already cached; never goes online. Fill the cache with |
| Looks up unknown domains (3 s timeout) and caches the answer for a year (failures for 7 days). |
Official brand domains, shared platforms, IPs and localhost are never looked up. Registries come from IANA's RDAP bootstrap list, cached in the feed cache dir.
Known-bad-link feeds
Only a feed refresh touches the network: scamgate update-feeds, the MCP update_feeds tool,
scamgate serve started with SCAMGATE_AUTO_UPDATE=1, or in the Hermes plugin
/scamgate update, hermes scamgate update-feeds, the scamgate_update_feeds tool and the background
refresh. They all use the same downloader. Checks read a local cache. No feed data is bundled in this repo.
scamgate update-feeds # default feeds
scamgate update-feeds --all # also the large opt-in feed
scamgate update-feeds -f openphish
scamgate feeds-statusThe cache lives in $SCAMGATE_CACHE_DIR, else $XDG_CACHE_HOME/scamgate, else ~/.cache/scamgate.
Each feed keeps its own files, and meta.json records fetch time, source and counts. A failed
download keeps that feed's previous cache. Verdicts warn when no feed is cached or a feed is older
than 24 hours. A running MCP server picks up a refreshed cache without a restart.
Feed | Default | Source | License / terms |
| yes | OpenPhish Terms: personal, non-commercial use only, no redistribution. Commercial use needs OpenPhish's written consent | |
| yes | Phishing.Database active domains | MIT |
|
| Phishing.Database active links (~65 MB download) | MIT |
| if key set | abuse.ch URLhaus online malware URLs | Free abuse.ch Auth-Key required ( |
You are responsible for following each feed's terms. Not used: PhishTank (new registrations are disabled) and Google Safe Browsing / Web Risk (non-commercial only, or paid).
Matching: an exact listed URL scores 100, an exact listed host 75, and a subdomain of a listed domain (domain feeds only) 50.
A long-running scamgate serve can refresh by itself: start it with SCAMGATE_AUTO_UPDATE=1 (the Claude
Code plugin does this unless you turn auto_update_feeds off). It downloads only when the lists are 6
hours old. Otherwise, refresh with cron, for example every 6 hours:
0 */6 * * * cd /path/to/scamgate && uv run scamgate update-feeds >> ~/.cache/scamgate/update.log 2>&1In the Hermes plugin
The plugin refreshes the default feeds by itself: about two minutes after Hermes starts, and then every 6 hours while Hermes runs. No cron line needed. A lock file in the cache folder makes sure only one Hermes process downloads at a time. Failures are logged and keep the old lists. To refresh by hand:
/scamgate update # in a chat
hermes scamgate update-feeds # in a terminal
hermes scamgate feeds-statusAgents get a scamgate_update_feeds tool so they can refresh stale lists themselves. It takes no
arguments: it always downloads the default feeds from their fixed sources into the configured cache,
and does nothing if they were refreshed in the last hour. An agent can ask for a refresh but can't
choose what is downloaded or where it goes. Shell commands that call the downloader with their own
arguments are blocked, and direct writes to the cache folder still are.
Turn the background refresh off with plugins.entries.scamgate.settings.auto_update_feeds: false or
SCAMGATE_AUTO_UPDATE=0. The refresh sends nothing about the links you check: it downloads whole
lists.
Your own blocklist: src/scamgate/data/blocklist.txt (or $SCAMGATE_BLOCKLIST). It
ships with a few made-up .example domains.
False positives
Feeds list single bad pages on big shared platforms (Google Docs and Sites, Firebase storage,
Microsoft login, Discord CDN, link shorteners). Flagging those whole hosts would block normal
sites. For domains under lists.feed_host_ignore in
rules.yaml (and their subdomains), and for official brand
domains, only an exact URL listing counts. One phishing page on x.vercel.app never flags
vercel.app. The trade-off: a scam on a subdomain of an ignored platform is only caught by exact URL.
Configuration
Weights, thresholds, brand lists and word lists live in
src/scamgate/config/rules.yaml. Point SCAMGATE_CONFIG at
your own copy to override it. To add a rule, write a function in
src/scamgate/rules/ decorated with @register("url.my_rule", kind="url"),
give it an entry under rules: in rules.yaml, and add tests and a scenario.
Bench and tests
scenarios/ holds 24 hand-written example cases (17 scam, 7 legit), each with
an expected decision.
$ uv run scamgate bench
...
Scams: 17 caught: 17 missed: 0
Legit: 7 false positives: 0
Exact decision match: 24/24This is a regression check for the rules, not a real-world detection rate. uv run pytest
runs the unit tests (1013 at v0.3.3; CI runs them on Python 3.11–3.13). They use fixtures and never touch the network.
Limits
Advisory by default. Over MCP it only warns, and the agent has to call it. Install the hook to enforce it. Even then it only sees tool calls: a link the agent reaches by a redirect, or a destination hidden in a variable, needs your approval rather than a check.
Heuristic rules miss scams that don't match them and sometimes flag legitimate actions. Treat
allowas "no known red flags", not "safe".Domain age needs opting in (
SCAMGATE_RDAP=cacheorlive, see below), because a live lookup tells the domain's registry which site you're checking.Known trade-offs: a word one letter off a long brand (
stripes.comvsstripe) still asks. A bank transfer to a new payee asks (25) unless your bank's Verification of Payee matched the name; card payments to new payees don't.The hook sees a tool call's arguments, not what a program does once it runs: a local script, binary or package install script that opens the network is invisible unless the command line shows it. Code that is decoded or downloaded and then run (
base64 -d | bash,exec(b64decode(…)),powershell -enc) asks.The brand pack covers about 50 of the most-phished brands plus Irish/UK banks, couriers and services; add your own under
lists.brands. Registrable domains use a curated suffix list (country second-levels and shared hosts likegithub.io,vercel.app), not the full PSL.Payee history only knows payees you've recorded with
record-payment. scamgate doesn't verify IBANs or call your bank.Tamper protection stops an agent's normal tools: writes to scamgate, its state and feed cache, its virtualenv (a new
.pthfile fails the integrity check),~/.claude(settings, hooks and installed plugins) and project.claude/settings*.json, and the Hermes config, however the path is spelled (~,$HOME, globs,cd+ relative,./,//,..). A path only counts when it names the folder itself:~/.cache/scamgate-feeds.logis not inside~/.cache/scamgate. Commands that only read (ls,cat,head,tail,grep,rg,find,stat,which,wc,du,sha256sum) pass on protected paths as long as they have no redirect other than to/dev/null, no$(…)or backticks, and no-exec,-delete,-fprintor--pre. Agent commands that disable or remove a Claude Code plugin, or start Claude in bare mode (which skips every hook), are blocked too. It isn't a sandbox: code running as you can read the signing key and rewrite any file, and code can be written to slip past pattern matching. For the strongest setup, give the agent no direct shell access, install scamgate where the agent's user can't write (uv tool installas another user, or read-only), and for Claude Code put the hook in managed settings withallowManagedHooksOnly.Hermes trusts a hook by its command string, not its file hash, so it won't notice if the installed scamgate is replaced. The integrity check covers edits to the installed files.
Disclaimer: scamgate is not financial, legal or security advice, and it doesn't replace your bank's fraud controls, Verification of Payee, or human judgement.
Roadmap
IBAN checksum validation and Verification of Payee integration
Redirect after-check: re-check the page a browser actually landed on
Optional local model as a tie-breaker on vague cases only, never overruling a hard rule
Full Public Suffix List (optional, cached)
More scenarios based on real fraud typologies
Contributing
Issues and pull requests are welcome, especially new rules, scenarios (scam and legit) and
false-positive reports. Please include a test or scenario with each rule change, and keep checks
offline and deterministic. Read CONTRIBUTING.md before you start, and
report security bugs privately (SECURITY.md). See AGENTS.md for repo conventions.
Thanks to @simin-yuan for the first outside contributions: the report in #20 and the fixes in #21 and #22 that made the hook linear on long commands.
License
MIT. Feed data is not included and stays under each provider's own terms.
Available Tools
3 toolscheck_paymentARead-onlyIdempotent
Check a payment before making it. Returns a verdict: decision (allow | ask | deny), risk_score 0-100 and plain-English reasons. If the decision is ask, stop and show the reasons to the human. If deny, do not proceed. Results are advisory, not a guarantee.
| Name | Required | Description | Default |
|---|---|---|---|
| amount | Yes | Amount to pay. | |
| budget | No | The spending limit the human gave for this task. | |
| method | Yes | card, bank_transfer, crypto, gift_card or other. | |
| currency | Yes | e.g. EUR, GBP, USDT. | |
| payee_name | Yes | Who is being paid. | |
| vop_result | No | Verification of Payee result, if available. | |
| context_text | No | The messages or page text that led to this payment. | |
| is_new_payee | No | True if this payee has never been paid before. scamgate checks your payee history rather than taking this on trust. | |
| payee_account | Yes | IBAN, wallet address, merchant id, etc. | |
| bank_details_changed | No | True if the payee's account details changed recently. |
Output Schema
| Name | Required | Description |
|---|---|---|
| reasons | No | |
| summary | Yes | |
| advisory | No | |
| decision | Yes | |
| warnings | No | Notes about the check itself, e.g. a stale feed cache |
| risk_score | Yes | |
| checked_host | No | For links: the host scamgate judged, the one a browser would open. If it isn't where you expected the link to go, treat that as a warning. Empty for payments and unreadable links. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnly, idempotent, non-destructive, closed-world, so the safety bar is met; the description adds the decision protocol the annotations can't express and an honest caveat that results are advisory, not a guarantee. It also makes clear this is a check, not the payment itself, consistent with readOnlyHint.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Four short sentences, front-loaded with purpose then the outcome-handling rules, with zero filler. Every sentence changes agent behavior.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
An output schema exists and the description still names the key verdict fields so the agent can act without a separate lookup. For a 10-parameter, 5-required tool with annotations plus an output schema, nothing needed to call it correctly is missing.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100% and 10 parameters are fully documented in-schema, so the baseline applies. The description adds no extra semantics about required vs optional fields or how context_text/budget influence the verdict.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb and resource ('Check a payment before making it') and immediately defines the output shape (decision/risk_score/reasons), so an agent knows exactly what it gets. It is unmistakable next to siblings check_url and update_feeds, which operate on entirely different resources.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Gives an explicit precondition ('before making it') plus branching instructions on the result: stop and show reasons on 'ask', do not proceed on 'deny'. This is the when-to-use and when-not-to-proceed guidance most tools omit.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
check_urlARead-onlyIdempotent
Check a link before opening it. Returns a verdict: decision (allow | ask | deny), risk_score 0-100, plain-English reasons, and checked_host, the host the link really opens. If the decision is ask, stop and show the reasons to the human. If deny, do not proceed. Results are advisory, not a guarantee.
| Name | Required | Description | Default |
|---|---|---|---|
| url | Yes | The full URL the agent is about to open. | |
| intent | No | What the agent is trying to do, e.g. "book a hotel in Galway". |
Output Schema
| Name | Required | Description |
|---|---|---|
| reasons | No | |
| summary | Yes | |
| advisory | No | |
| decision | Yes | |
| warnings | No | Notes about the check itself, e.g. a stale feed cache |
| risk_score | Yes | |
| checked_host | No | For links: the host scamgate judged, the one a browser would open. If it isn't where you expected the link to go, treat that as a warning. Empty for payments and unreadable links. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already cover read-only, idempotent, non-destructive behavior, and the description adds important context: the result shape, the advisory limitation, and what the agent should do for 'ask' and 'deny'. It also explains checked_host, which is useful behavioral detail.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is compact and front-loaded: it states the action first, then the verdict fields, then the required decision handling. Every sentence adds operational value.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Although an output schema exists, the description still gives enough context to use the tool correctly: what it returns, how to interpret 'ask' and 'deny', and its advisory limitation. Combined with rich annotations and full schema coverage, nothing important is missing.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%, so both the required url parameter and optional intent parameter are already well documented. The description does not add parameter-level meaning beyond the schema, so the baseline score of 3 applies.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description uses a specific verb and resource: 'Check a link before opening it.' It also states the verdict fields returned, making the tool's purpose unambiguous and distinguishable from siblings like check_payment and update_feeds.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
It clearly says to check a link before opening it and gives explicit downstream behavior for 'ask' and 'deny' verdicts. It does not name alternatives or state when not to use the tool, which keeps it just below a 5.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
update_feedsAIdempotent
Refresh scamgate's phishing lists now. Use it when a scamgate verdict or status says the lists are missing or stale. Takes no arguments: it always downloads the default lists from their fixed sources, and does nothing if they were refreshed in the last hour. With automatic refresh on, the lists also refresh by themselves every 6 hours.
| Name | Required | Description | Default |
|---|---|---|---|
No parameters | |||
Output Schema
| Name | Required | Description |
|---|---|---|
| failed | Yes | Lists that couldn't be downloaded; their old copy is kept. |
| skipped | Yes | "fresh" (refreshed in the last hour), "busy" (another refresh is running) or empty. |
| summary | Yes | One line per list, for the human. |
| updated | Yes | Lists downloaded now. |
| not_configured | Yes | Optional lists that need a key first. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Adds substantial behavior beyond annotations: the tool always downloads from fixed sources, is a no-op within a one-hour window, and lists also auto-refresh every six hours. That explains side effects (network fetch) and idempotence rationale that readOnlyHint/openWorldHint only imply.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Four short sentences, front-loaded with the action and the trigger condition. The final auto-refresh sentence is slightly tangential but still earns its place by explaining why staleness may resolve on its own.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a zero-argument mutation tool with an output schema already present, the description covers trigger, side effects, cooldown, and background behavior—nothing an agent needs to call it correctly is missing.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Zero parameters, so baseline is 4; the description usefully confirms 'Takes no arguments' and clarifies that sources are fixed and non-configurable, preventing an agent from hunting for filter options.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb and resource ('Refresh scamgate's phishing lists') plus timing ('now'), which is immediately distinguishable from siblings check_url and check_payment that query verdicts rather than update data.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Gives a concrete trigger: 'Use it when a scamgate verdict or status says the lists are missing or stale.' Usage conditions are clear but there is no explicit statement of when not to call (e.g. that calling unprompted is wasteful), though the cooldown note implies it.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
3 tool updates
v0.3.3- First observed
check_payment - First observed
check_url - First observed
update_feeds
TDQS
Scored across 3 tools
Each tool targets a distinct object and action: verifying a URL, verifying a payment, and refreshing phishing feeds. An agent can never confuse check_url with check_payment, and update_feeds is clearly a maintenance operation gated on stale/missing list status.
All three names follow the same verb_noun snake_case pattern (check_url, check_payment, update_feeds). The shared check_ prefix for the two verdict tools even reinforces the shared output contract.
Three tools is a narrow but coherent surface for a scam-verdict service, and each one earns its place with no filler. It sits at the low edge of the comfortable range, leaving little room for additional check types.
The core lifecycle is covered: verdicts for links and payments plus a way to remediate stale feed data, with consistent allow/ask/deny semantics. Gaps exist for other scam vectors (email senders, phone numbers, domains, QR/app downloads), which agents would have to handle elsewhere.
Maintenance
Related MCP Connectors
Pay-per-call safety checks for AI agents: screen a crypto address or URL before you transact.
Scam and phishing detection for AI agents: safe/warn/danger verdicts for URLs and messages.
Check if a counterparty is safe to pay: trust/risk score for AI agents. Scam/phishing screen.
Verify x402 payment endpoints before an AI agent pays: scam scan, on-chain checks, trust scores.
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceSecurity layer for AI agents that evaluates transaction intents and returns verdicts (ALLOW/WARN/DENY) using deterministic rules, on-chain checks, and simulation.5 npmMIT

attest-mcpofficial
AlicenseAqualityDmaintenanceEnables AI agents to scan payment endpoints for safety, returning a letter grade (A–F) and verdict before authorizing payments.215 npmMIT- AlicenseBqualityCmaintenanceEnables AI agents to perform token security audits, honeypot detection, liquidity analysis, and risk scoring to avoid crypto scams.1ISC
- AlicenseNot gradedqualityCmaintenanceEnables AI agents to check trustworthiness before recommending URLs, products, or organizations, with fail-closed pass/fail verdicts and attested-only recommendations.MIT