Scam checks for AI agents
AI agents can check links before opening them, numbers before calling them, and wallets, IBANs or payment handles before paying. Bewary's MCP server at https://api.bewary.app/mcp runs the same code as the REST API. Each check and lookup returns a verdict your agent can act on, and a watch produces events when its domain, number, wallet or brand changes, read from GET /v1/events or pushed to signed webhooks. Every tool call needs an API key, and a free key gets 1,000 credits a month.
Give the agent one tool first
lookup_url covers what an agent meets most: a link to open, on a page, in an email or in a chat. Add lookup_phone if it places calls, and lookup_wallet, lookup_iban or lookup_payment_handle if it pays anyone. Every lookup returns the same verdict: scam, suspicious, safe or unknown.
Tell the agent when to call it
Pass this as the system prompt (every example below does). It decides whether the tool gets used, so it covers advising the user, not only acting:
SYSTEM_PROMPT = """Before you open a link, and before you advise the user about a link in a message,
look it up with lookup_url. Look up every link in the message, not only the first.
- scam: do not open it. Tell the user it looks like a scam and why, from the reason codes.
- suspicious: do not open it without the user's confirmation.
- unknown, or the lookup failed: say you could not confirm it is safe. Never treat unknown as safe.
- safe: continue."""MCP: Claude, Cursor, ChatGPT and VS Code
The MCP server is at https://api.bewary.app/mcp (streamable HTTP). It has 15 tools and runs the same code as the API. In Claude Code:
claude mcp add --transport http bewary https://api.bewary.app/mcp \
--header "Authorization: Bearer $BEWARY_API_KEY"Install steps for Cursor, VS Code, Windsurf and the Claude and ChatGPT apps are on the MCP page. Every tool call needs your key, as an Authorization: Bearer header or at the end of the URL. MCP apps that support sign-in, such as Claude and ChatGPT, can sign in to Bewary instead.
Keep the result small, and never let a failed lookup crash the agent
A full answer lists every signal and fact. A model needs a few fields, so pass it a summary and save tokens. A lookup that fails (a missing key, a timeout, our outage) must reach the model as unknown, not as an exception:
import os
import requests
BEWARY_API_KEY = os.environ["BEWARY_API_KEY"] # a free key: bewary.app/account
def lookup_url(url):
headers = {"Authorization": f"Bearer {BEWARY_API_KEY}"}
r = requests.get("https://api.bewary.app/v1/url", params={"u": url}, headers=headers, timeout=20)
r.raise_for_status()
return r.json()
def for_model(r):
"""The fields a model needs to decide and explain."""
return {
"verdict": r["verdict"],
"p_scam": r["p_scam"],
"why": r["reason_codes"],
"items": [{"type": e["type"], "value": e["value"], "verdict": e["verdict"]}
for e in r["entities"]],
}
def check_for_model(url):
try:
return {"checked": True, **for_model(lookup_url(url))}
except Exception as e: # quota, timeout or outage
return {"checked": False, "verdict": "unknown", "why": [f"lookup failed: {type(e).__name__}"]}The examples below use check_for_model(url).
OpenAI (Responses API)
import json
from openai import OpenAI
LOOKUP_URL = {
"type": "function",
"name": "lookup_url",
"description": "Look up a link before opening it or advising on it: Bewary checks its domain, "
"brand lookalikes and scam lists. Returns a verdict: scam, suspicious, safe or unknown.",
"strict": True,
"parameters": {
"type": "object",
"properties": {
"url": {"type": "string", "description": "The full link, as written."},
},
"required": ["url"],
"additionalProperties": False,
},
}
client = OpenAI()
history = [{"role": "user", "content": "I got: 'Your package is held, pay $1.99 at https://usps-redeliver.top'. Should I pay?"}]
while True:
response = client.responses.create(model="gpt-5.5", instructions=SYSTEM_PROMPT,
input=history, tools=[LOOKUP_URL])
calls = [item for item in response.output if item.type == "function_call"]
if not calls:
break
history += response.output
for call in calls:
args = json.loads(call.arguments)
history.append({"type": "function_call_output", "call_id": call.call_id,
"output": json.dumps(check_for_model(args["url"]))})
print(response.output_text)Anthropic Claude (Messages API)
import json
import anthropic
LOOKUP_URL = {
"name": "lookup_url",
"description": "Look up a link before opening it or advising on it: Bewary checks its domain, "
"brand lookalikes and scam lists. Returns a verdict: scam, suspicious, safe or unknown.",
"input_schema": {
"type": "object",
"properties": {
"url": {"type": "string", "description": "The full link, as written."},
},
"required": ["url"],
},
}
client = anthropic.Anthropic()
messages = [{"role": "user", "content": "I got: 'Your package is held, pay $1.99 at https://usps-redeliver.top'. Should I pay?"}]
while True:
response = client.messages.create(model="claude-sonnet-5-5", max_tokens=1024, system=SYSTEM_PROMPT,
tools=[LOOKUP_URL], messages=messages)
if response.stop_reason != "tool_use":
break
messages.append({"role": "assistant", "content": response.content})
messages.append({"role": "user", "content": [
{"type": "tool_result", "tool_use_id": block.id,
"content": json.dumps(check_for_model(block.input["url"]))}
for block in response.content if block.type == "tool_use"]})
print(response.content[-1].text)Vercel AI SDK
import { generateText, tool, stepCountIs } from "ai"
import { openai } from "@ai-sdk/openai"
import { z } from "zod"
const SYSTEM_PROMPT = "..." // the system prompt above
async function lookupUrl(url: string) {
const key = process.env.BEWARY_API_KEY! // a free key: bewary.app/account
const res = await fetch(`https://api.bewary.app/v1/url?u=${encodeURIComponent(url)}`, {
headers: { Authorization: `Bearer ${key}` },
signal: AbortSignal.timeout(20_000),
})
if (!res.ok) throw new Error(`HTTP ${res.status}`)
return res.json()
}
const lookup_url = tool({
description: "Look up a link before opening it or advising on it. Returns verdict: scam, suspicious, safe or unknown.",
inputSchema: z.object({
url: z.string().describe("The full link, as written."),
}),
execute: async ({ url }) => {
try {
const r = await lookupUrl(url)
return { checked: true, verdict: r.verdict, p_scam: r.p_scam, why: r.reason_codes }
} catch (e) { // quota, timeout or outage
return { checked: false, verdict: "unknown", why: [`lookup failed: ${String(e)}`] }
}
},
})
const { text } = await generateText({
model: openai("gpt-5.5"),
system: SYSTEM_PROMPT,
tools: { lookup_url },
stopWhen: stepCountIs(5),
prompt: "I got: 'Your package is held, pay $1.99 at https://usps-redeliver.top'. Should I pay?",
})
console.log(text)LangChain
from langchain.agents import create_agent
from langchain_core.tools import tool
@tool
def lookup_url_tool(url: str) -> dict:
"""Look up a link before opening it or advising on it. Returns verdict: scam,
suspicious, safe or unknown, and the reason codes."""
return check_for_model(url)
agent = create_agent("openai:gpt-5.5", tools=[lookup_url_tool],
system_prompt=SYSTEM_PROMPT.replace("lookup_url", "lookup_url_tool"))
reply = agent.invoke({"messages": [{"role": "user", "content": "Is this a scam: 'Your package is held, pay $1.99 at https://usps-redeliver.top'"}]})
print(reply["messages"][-1].content)Costs for an agent
A link lookup costs 1 credit. Every call needs a key, and a free key gets 1,000 credits a month, enough to build and test an agent.
Updated · This page as Markdown · Changelog