Why Transaction Simulation Matters
Last Line of Defense
Users trust your app to protect them—make that trust count
Permit Protection
Catch EIP-712 signatures that can drain wallets without transactions
Approval Awareness
Show users their existing exposure before they add more
- Pre-signing simulation — Show users exactly what will happen before they approve
- EIP-712 signature analysis — Decode and risk-score permit/signature requests
- Approval risk detection — Identify dangerous existing approvals
- Recipient verification — Catch address poisoning before users confirm
- Multi-chain support — Same APIs for ETH, Polygon, Arbitrum, Base, and more
Prerequisites
Before implementing transaction simulation, ensure you have:- A Webacy API key (sign up here)
- Basic familiarity with REST APIs or the Webacy SDK
- Your application’s transaction signing flow identified for integration
Pre-Signing Protection
The moment before a user signs a transaction is your last chance to protect them.Transaction Simulation
Simulate every transaction before it gets signed. The endpoint isPOST /scan/{fromAddress}/transactions, where fromAddress is the signer, and it takes the serialized unsigned transaction (raw), not the to/data/value fields. Every EVM library produces it as a 0x hex string: ethers Transaction.from({...}).unsignedSerialized, viem serializeTransaction(...), web3.js bytesToHex(tx.serialize()) (its serialize() returns bytes). A 66-character tx hash is also accepted for a transaction that is already mined.
curl -X POST "https://api.webacy.com/scan/0xUserAddress.../transactions" \
-H "x-api-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"tx": {
"from": "0xUserAddress...",
"raw": "0x02f00180843b9aca008506fc23ac00830186a094d8da6bf26964af9d7eed9e03e53415d37aa96045872386f26fc1000080c0"
},
"chain": 1,
"domain": "app.example.com"
}'
import { ThreatClient } from '@webacy-xyz/sdk';
import { Transaction } from 'ethers';
const client = new ThreatClient({ apiKey: process.env.WEBACY_API_KEY });
// The transaction your dApp is about to ask the user to sign
const raw = Transaction.from({
type: 2,
chainId: 1,
to: '0xContractAddress...',
data: '0xTransactionData...',
value: 0n,
nonce: 12,
gasLimit: 100_000n,
maxFeePerGas: 30_000_000_000n,
maxPriorityFeePerGas: 1_000_000_000n,
}).unsignedSerialized;
const result = await client.scan.scanTransaction('0xUserAddress...', {
tx: { from: '0xUserAddress...', raw },
chain: 1, // Ethereum mainnet
domain: 'connected-dapp.com',
});
// One item per simulated asset movement (or approval): party = the signer,
// counterparty = who receives / can spend, asset = the token contract.
const { simulation } = result as unknown as TransactionScanResponse;
for (const item of simulation) {
const { txData, counterpartyRisk, assetRisk } = item;
console.log(`${txData.changeType} ${txData.rawAmount ?? ''} ${txData.symbol ?? txData.assetType} → ${txData.counterpartyAddress}`);
if ((counterpartyRisk.high ?? 0) > 0 || (assetRisk.high ?? 0) > 0) {
console.warn('⛔ High risk: ', counterpartyRisk.issues?.flatMap((i) => i.tags.map((t) => t.name)));
}
if (counterpartyRisk.overallRisk === undefined) {
// The recipient could not be scored (provider outage or budget) — treat as unknown, not safe
console.warn('⚠️ Recipient risk unavailable, ask the user to double-check the address');
}
}
interface TransactionScanResponse {
simulation: Array<{
partyRisk: AddressRisk;
counterpartyRisk: AddressRisk;
/** `address` is `null` for the native asset (ETH) */
assetRisk: AddressRisk & { address: string | null };
txData: {
changeType: 'TRANSFER' | 'APPROVE';
assetType?: 'NATIVE' | 'ERC20' | 'ERC721' | 'ERC1155';
rawAmount?: string;
symbol?: string;
partyAddress: string;
counterpartyAddress: string;
assetAddress: string | null;
};
/** Attached to the first item only, when the calldata calls a known-risky function */
functionRisk?: { selector: string; functionName: string; riskLevel: string; description: string };
}>;
block?: number | null; // absent / null for a pending (unsigned) transaction
chain: string;
descriptor: string; // signed TLV descriptor (hardware-wallet integrations)
timestamp: string;
}
// When a party could not be scored (provider outage or time budget) only `address` is present —
// never read a missing score as "clean".
interface AddressRisk {
address: string;
overallRisk?: number; // 0–100
high?: number; // count of high-severity findings
medium?: number;
issues?: Array<{ tags: Array<{ key: string; name: string; description: string; severity: number }> }>;
}
import requests
response = requests.post(
"https://api.webacy.com/scan/0xUserAddress.../transactions",
headers={
"x-api-key": "YOUR_API_KEY",
"Content-Type": "application/json"
},
json={
"tx": {
"from": "0xUserAddress...",
"raw": "0x02f00180843b9aca008506fc23ac00830186a094d8da6bf26964af9d7eed9e03e53415d37aa96045872386f26fc1000080c0"
},
"chain": 1,
"domain": "app.example.com"
}
)
result = response.json()
for item in result["simulation"]:
tx = item["txData"]
print(tx["changeType"], tx.get("rawAmount"), tx.get("symbol") or tx.get("assetType"), "->", tx["counterpartyAddress"])
if item["counterpartyRisk"].get("high", 0) > 0:
print("HIGH RISK counterparty:", [t["name"] for i in item["counterpartyRisk"].get("issues", []) for t in i["tags"]])
| Field | What It Means | Display Priority |
|---|---|---|
simulation[].txData | Each asset movement (TRANSFER) or approval (APPROVE) the transaction would cause: amount, token, recipient / spender | Always show |
simulation[].counterpartyRisk | Risk profile of the recipient or spender (overallRisk 0–100, high / medium counts, tagged issues) | Show with color coding — high > 0 means block or strong warning |
simulation[].assetRisk | Risk profile of the token contract involved | Show when high > 0 |
simulation[0].functionRisk | Present on the first item when the calldata calls a known-risky function (e.g. setApprovalForAll) | Show prominently if present |
block | Absent (or null) for a pending transaction, the block number for a mined one | Informational |
The chain can be passed either as the numeric
chain in the body (1, 56, 137, 10, 42161, 8453) or as the ?chain= query (eth, bsc, pol, opt, arb, base); the query wins when both are present. tx.from must match the fromAddress in the path.An empty
simulation array is inconclusive, not safe. It means the simulation produced no asset movement and the calldata named no recipient or spender to score — a call that reverts, a contract call with no transfer, or a degraded simulation read. Treat it as unknown and ask the user to verify the address, never as a green light. A party whose overallRisk / high / medium are missing (only address present) could not be scored in time — same rule.Supported chain IDs: 1 (ETH), 56 (BSC), 137 (Polygon), 10 (Optimism), 42161 (Arbitrum), 8453 (Base)
EIP-712 Permit Signature Verification
Permit signatures are one of the most dangerous attack vectors. A single signature can authorize unlimited token spending without any on-chain transaction.The Silent Drain: Unlike regular transactions, permit signatures don’t show up on block explorers until the attacker uses them. A user can sign a permit and see nothing happen—until days later when everything is gone.
curl -X POST "https://api.webacy.com/scan/0xUserAddress.../eip712" \
-H "x-api-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"msg": {
"from": "0xUserAddress...",
"data": {
"types": {
"EIP712Domain": [
{"name": "name", "type": "string"},
{"name": "version", "type": "string"},
{"name": "chainId", "type": "uint256"},
{"name": "verifyingContract", "type": "address"}
],
"Permit": [
{"name": "owner", "type": "address"},
{"name": "spender", "type": "address"},
{"name": "value", "type": "uint256"},
{"name": "nonce", "type": "uint256"},
{"name": "deadline", "type": "uint256"}
]
},
"primaryType": "Permit",
"domain": {
"name": "USD Coin",
"version": "2",
"chainId": 1,
"verifyingContract": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
},
"message": {
"owner": "0xUserAddress...",
"spender": "0xSuspiciousContract...",
"value": "115792089237316195423570985008687907853269984665640564039457584007913129639935",
"nonce": 0,
"deadline": 1893456000
}
}
},
"domain": "suspicious-site.com"
}'
const result = await client.scan.scanEip712(
'0xUserAddress...',
{
msg: {
from: '0xUserAddress...',
data: {
types: {
EIP712Domain: [
{ name: 'name', type: 'string' },
{ name: 'version', type: 'string' },
{ name: 'chainId', type: 'uint256' },
{ name: 'verifyingContract', type: 'address' },
],
Permit: [
{ name: 'owner', type: 'address' },
{ name: 'spender', type: 'address' },
{ name: 'value', type: 'uint256' },
{ name: 'nonce', type: 'uint256' },
{ name: 'deadline', type: 'uint256' },
],
},
primaryType: 'Permit',
domain: {
name: 'USD Coin',
version: '2',
chainId: 1,
verifyingContract: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
},
message: {
owner: '0xUserAddress...',
spender: '0xSuspiciousContract...',
value: '115792089237316195423570985008687907853269984665640564039457584007913129639935',
nonce: 0,
deadline: 1893456000,
},
},
},
domain: 'suspicious-site.com',
}
);
// The spender / recipient named in the typed data is scored as the
// counterparty; the verifying contract (or `token` / `contract` fields) as the party.
const { simulation } = result as unknown as Eip712ScanResponse;
if ((simulation.counterpartyRisk?.high ?? 0) > 0 || simulation.domainRisk?.riskLevel === 'high') {
console.error('⛔ DANGEROUS SIGNATURE DETECTED');
console.error(`Spender ${simulation.counterpartyRisk?.address} is flagged:`,
simulation.counterpartyRisk?.issues?.flatMap((i) => i.tags.map((t) => t.name)));
console.error('This signature could drain your entire wallet.');
// Block the signature
}
interface Eip712ScanResponse {
simulation: {
partyRisk?: AddressRisk & { allAddressesChecked: string[] };
counterpartyRisk?: AddressRisk & { allAddressesChecked: string[] };
domainRisk?: { riskLevel: 'low' | 'medium' | 'high' | 'unknown'; description: string };
functionRisk?: { selector: string; functionName: string; riskLevel: string; risks: string[] };
};
chain: string;
descriptor: string;
timestamp: string;
}
response = requests.post(
"https://api.webacy.com/scan/0xUserAddress.../eip712",
headers={
"x-api-key": "YOUR_API_KEY",
"Content-Type": "application/json"
},
json={
"msg": {
"from": "0xUserAddress...",
"data": {
"types": {
"EIP712Domain": [
{"name": "name", "type": "string"},
{"name": "version", "type": "string"},
{"name": "chainId", "type": "uint256"},
{"name": "verifyingContract", "type": "address"}
],
"Permit": [
{"name": "owner", "type": "address"},
{"name": "spender", "type": "address"},
{"name": "value", "type": "uint256"},
{"name": "nonce", "type": "uint256"},
{"name": "deadline", "type": "uint256"}
]
},
"primaryType": "Permit",
"domain": {
"name": "USD Coin",
"version": "2",
"chainId": 1,
"verifyingContract": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
},
"message": {
"owner": "0xUserAddress...",
"spender": "0xSuspiciousContract...",
"value": "115792089237316195423570985008687907853269984665640564039457584007913129639935",
"nonce": 0,
"deadline": 1893456000
}
}
},
"domain": "suspicious-site.com"
}
)
| Red Flag | What It Means |
|---|---|
value = max uint256 | Unlimited token approval |
Unknown spender | Approving unknown contract |
Long deadline | Signature valid for extended period |
simulation.counterpartyRisk.high > 0 | The spender / recipient is a known drainer, hacker or sanctioned address |
simulation.domainRisk.riskLevel = high | The dApp domain you passed is a known phishing site |
simulation.functionRisk present | The typed data embeds calldata for a risky function (Safe execTransaction, delegatecall, ownership transfer, …) |
Only Ethereum mainnet is supported for EIP-712 scans:
data.domain.chainId must be 1 (or pass ?chain=eth). msg.from must match the fromAddress in the path. Pass the dApp origin as domain to get simulation.domainRisk.Approval Risk Management
Users often have approvals they’ve forgotten about. Help them understand their exposure.Current Approval Scanning
Show users their existing approvals before they add more.curl -X GET "https://api.webacy.com/wallets/0xUserAddress.../approvals?chain=eth" \
-H "x-api-key: YOUR_API_KEY"
const approvals = await client.wallets.getApprovals(
'0xUserAddress...',
{ chain: Chain.ETH }
);
// Categorize by risk
const dangerous = approvals.approvals.filter(a =>
a.isUnlimited && (a.spenderRisk === 'high' || !a.spenderVerified)
);
const unlimited = approvals.approvals.filter(a =>
a.isUnlimited && a.spenderRisk !== 'high'
);
console.log(`⛔ ${dangerous.length} dangerous approvals`);
console.log(`⚠️ ${unlimited.length} unlimited approvals`);
console.log(`Total approvals: ${approvals.approvals.length}`);
response = requests.get(
"https://api.webacy.com/wallets/0xUserAddress.../approvals",
params={"chain": "eth"},
headers={"x-api-key": "YOUR_API_KEY"}
)
Dangerous Approval Detection
Flag approvals that could be used to drain the wallet.interface ApprovalRisk {
token: string;
spender: string;
riskFactors: string[];
recommendedAction: 'revoke' | 'review' | 'ok';
}
function assessApprovalRisk(approval: Approval): ApprovalRisk {
const riskFactors: string[] = [];
// Check for unlimited approval
if (approval.isUnlimited) {
riskFactors.push('Unlimited approval amount');
}
// Check if spender is known
if (!approval.spenderName) {
riskFactors.push('Unknown spender contract');
}
// Check if spender contract still exists
if (approval.spenderDeployed === false) {
riskFactors.push('Spender contract no longer exists');
}
// Check spender risk score
if (approval.spenderRisk === 'high') {
riskFactors.push('Spender flagged as high risk');
}
// Determine action
let recommendedAction: 'revoke' | 'review' | 'ok' = 'ok';
if (riskFactors.length >= 2 || approval.spenderRisk === 'high') {
recommendedAction = 'revoke';
} else if (riskFactors.length === 1) {
recommendedAction = 'review';
}
return {
token: approval.symbol,
spender: approval.spender,
riskFactors,
recommendedAction,
};
}
Recipient Verification
Before a user sends funds, verify the recipient address isn’t part of an attack.Address Poisoning Check
curl -X GET "https://api.webacy.com/addresses/0xRecipient.../poisoning?chain=eth" \
-H "x-api-key: YOUR_API_KEY"
const poisoning = await client.addresses.checkPoisoning(
'0xRecipient...',
{ chain: Chain.ETH }
);
if (poisoning.is_poisoned) {
// Stop the transaction
return {
blocked: true,
reason: 'Address Poisoning Detected',
message: 'This address appears to be part of an address poisoning attack. ' +
'It closely resembles a legitimate address you\'ve interacted with. ' +
'Please verify the full address character by character.',
similarTo: poisoning.similar_addresses
};
}
response = requests.get(
"https://api.webacy.com/addresses/0xRecipient.../poisoning",
params={"chain": "eth"},
headers={"x-api-key": "YOUR_API_KEY"}
)
Risk Profiling
Get a quick risk assessment of any recipient.curl -X GET "https://api.webacy.com/addresses/0xRecipient.../quick-profile?chain=eth" \
-H "x-api-key: YOUR_API_KEY"
const profile = await client.addresses.getQuickProfile(
'0xRecipient...',
{ chain: Chain.ETH }
);
// Show risk information on the confirmation screen
const riskDisplay = {
score: profile.riskScore,
level: profile.riskLevel,
accountAge: profile.accountAge,
warning: profile.riskLevel === 'high' ?
'This address has been flagged as high risk' : null
};
response = requests.get(
"https://api.webacy.com/addresses/0xRecipient.../quick-profile",
params={"chain": "eth"},
headers={"x-api-key": "YOUR_API_KEY"}
)
Complete Integration Workflow
Pre-Signing Check Flow
Approval Health Check Flow
Full TypeScript Implementation
Complete Transaction Security Module
Complete Transaction Security Module
import { ThreatClient, Chain, type ScanChainId } from '@webacy-xyz/sdk';
const client = new ThreatClient({
apiKey: process.env.WEBACY_API_KEY,
defaultChain: Chain.ETH,
});
type RiskLevel = 'low' | 'medium' | 'high' | 'critical';
interface PreSigningResult {
allowed: boolean;
riskLevel: RiskLevel;
assetChanges?: Array<{
type: 'TRANSFER' | 'APPROVE';
symbol: string;
amount: string;
counterparty: string;
}>;
warnings: string[];
blockReason?: string;
}
// Shape of the API responses (see the tables above). A party that could not be
// scored in time carries only `address` — `levelOf` treats that as unknown.
interface AddressRisk {
address: string;
overallRisk?: number;
high?: number;
medium?: number;
issues?: Array<{ tags: Array<{ key: string; name: string }> }>;
}
interface TxScanResponse {
simulation: Array<{
partyRisk: AddressRisk;
counterpartyRisk: AddressRisk;
assetRisk: AddressRisk;
txData: { changeType: 'TRANSFER' | 'APPROVE'; assetType?: string; rawAmount?: string; symbol?: string; counterpartyAddress: string };
functionRisk?: { functionName: string; description: string };
}>;
}
interface Eip712ScanResponse {
simulation: {
counterpartyRisk?: AddressRisk;
partyRisk?: AddressRisk;
domainRisk?: { riskLevel: string };
functionRisk?: { functionName: string; risks: string[] };
};
}
// Map an address risk profile to a display level
function levelOf(...risks: Array<AddressRisk | undefined>): RiskLevel {
if (risks.some((r) => (r?.high ?? 0) > 0)) return 'critical';
if (risks.some((r) => (r?.medium ?? 0) > 0)) return 'high';
if (risks.some((r) => (r?.overallRisk ?? 0) >= 30)) return 'medium';
// A party that could not be scored (no overallRisk) is unknown, not low
if (risks.some((r) => r && r.overallRisk === undefined)) return 'medium';
return 'low';
}
function tagNames(risk?: AddressRisk): string[] {
return risk?.issues?.flatMap((i) => i.tags.map((t) => t.name)) ?? [];
}
// Main pre-signing check
async function preSigningCheck(
userAddress: string,
request: TransactionRequest | EIP712Request,
chain: ScanChainId,
dappDomain?: string
): Promise<PreSigningResult> {
// Determine request type
if (isEIP712Request(request)) {
return checkEIP712Signature(userAddress, request, dappDomain);
} else {
return checkTransaction(userAddress, request, chain, dappDomain);
}
}
// EIP-712 signature check
async function checkEIP712Signature(
userAddress: string,
request: EIP712Request,
dappDomain?: string
): Promise<PreSigningResult> {
const result = (await client.scan.scanEip712(userAddress, {
msg: {
from: userAddress,
data: request.typedData,
},
domain: dappDomain,
})) as unknown as Eip712ScanResponse;
const warnings: string[] = [];
const { counterpartyRisk, partyRisk, domainRisk, functionRisk } = result.simulation;
// Check for permit signatures
if (request.typedData.primaryType.toLowerCase().includes('permit')) {
warnings.push('This is a PERMIT signature - it can authorize token spending');
// Check for unlimited approval
const message = request.typedData.message;
if (message.value === '115792089237316195423570985008687907853269984665640564039457584007913129639935') {
warnings.push('UNLIMITED token approval requested');
}
}
if (functionRisk) {
warnings.push(`Risky function in typed data: ${functionRisk.functionName}`, ...functionRisk.risks);
}
if (domainRisk?.riskLevel === 'high') {
warnings.push(`${dappDomain} is a known malicious domain`);
}
// Block critical risk: the spender / recipient is a flagged address.
// No scored party at all (neither counterparty nor contract) is
// inconclusive, not safe — never answer 'low' for it.
const scored = levelOf(counterpartyRisk, partyRisk);
const riskLevel: RiskLevel =
!counterpartyRisk && !partyRisk && scored === 'low' ? 'medium' : scored;
if (riskLevel === 'critical' || domainRisk?.riskLevel === 'high') {
return {
allowed: false,
riskLevel: 'critical',
warnings: [...warnings, ...tagNames(counterpartyRisk)],
blockReason: 'This signature matches known malicious patterns and could drain your wallet.',
};
}
return {
allowed: true,
riskLevel,
warnings,
};
}
// Transaction check
async function checkTransaction(
userAddress: string,
request: TransactionRequest,
chain: ScanChainId,
dappDomain?: string
): Promise<PreSigningResult> {
const warnings: string[] = [];
// Check recipient for poisoning if present
if (request.to) {
const poisoning = await client.addresses.checkPoisoning(request.to, {
chain: chainIdToChain(chain),
});
if (poisoning.is_poisoned) {
return {
allowed: false,
riskLevel: 'critical',
warnings: ['Address poisoning attack detected'],
blockReason: 'This address appears to be part of an address poisoning scam.',
};
}
}
// Simulate transaction — `raw` is the serialized unsigned transaction
// (ethers `Transaction.from({...}).unsignedSerialized`, viem `serializeTransaction`)
const result = (await client.scan.scanTransaction(userAddress, {
tx: { from: userAddress, raw: request.raw },
chain,
domain: dappDomain,
})) as unknown as TxScanResponse;
const assetChanges = result.simulation.map((item) => ({
type: item.txData.changeType,
symbol: item.txData.symbol ?? item.txData.assetType ?? 'asset',
amount: item.txData.rawAmount ?? (item.txData.changeType === 'APPROVE' ? 'approval' : ''),
counterparty: item.txData.counterpartyAddress,
}));
// Approvals: the spender is the risk — an unlimited approval to a flagged
// address is the dominant drainer pattern
for (const item of result.simulation) {
if (item.txData.changeType === 'APPROVE') {
warnings.push(`Approval to ${item.txData.counterpartyAddress}${(item.counterpartyRisk.high ?? 0) > 0 ? ' (FLAGGED ADDRESS)' : ''}`);
}
if (item.functionRisk) {
warnings.push(`Risky function: ${item.functionRisk.functionName} — ${item.functionRisk.description}`);
}
}
// Block critical transactions: any recipient / spender / token flagged high
const scored = levelOf(
...result.simulation.flatMap((item) => [item.counterpartyRisk, item.assetRisk]),
);
// An empty simulation is inconclusive, not safe (see the warning above):
// nothing was scored, so never answer 'low' for it.
const riskLevel: RiskLevel =
result.simulation.length === 0 && scored === 'low' ? 'medium' : scored;
if (riskLevel === 'critical') {
return {
allowed: false,
riskLevel: 'critical',
assetChanges,
warnings: [...warnings, ...result.simulation.flatMap((item) => tagNames(item.counterpartyRisk))],
blockReason: 'This transaction has been identified as dangerous.',
};
}
return {
allowed: true,
riskLevel,
assetChanges,
warnings,
};
}
// Approval health check
async function getApprovalHealth(
userAddress: string,
chain: Chain
): Promise<{
dangerous: number;
unlimited: number;
total: number;
needsAttention: boolean;
approvals: ApprovalInfo[];
}> {
const result = await client.wallets.getApprovals(userAddress, { chain });
const categorized = result.approvals.map(a => ({
...a,
category: categorizeApproval(a),
}));
const dangerous = categorized.filter(a => a.category === 'dangerous').length;
const unlimited = categorized.filter(a => a.category === 'unlimited').length;
return {
dangerous,
unlimited,
total: result.approvals.length,
needsAttention: dangerous > 0,
approvals: categorized,
};
}
function categorizeApproval(approval: Approval): 'dangerous' | 'unlimited' | 'safe' {
if (approval.spenderRisk === 'high' || !approval.spenderVerified) {
return 'dangerous';
}
if (approval.isUnlimited) {
return 'unlimited';
}
return 'safe';
}
// Helper to convert chain ID to Chain enum
function chainIdToChain(chainId: ScanChainId): Chain {
const mapping: Record<number, Chain> = {
1: Chain.ETH,
56: Chain.BSC,
137: Chain.POL,
10: Chain.OPT,
42161: Chain.ARB,
8453: Chain.BASE,
};
return mapping[chainId] || Chain.ETH;
}
// Type guards
function isEIP712Request(request: any): request is EIP712Request {
return 'typedData' in request;
}
interface TransactionRequest {
/** Serialized unsigned transaction (ethers `Transaction.unsignedSerialized`, viem `serializeTransaction`) */
raw: string;
/** Recipient, when known, for the address-poisoning pre-check */
to?: string;
}
interface EIP712Request {
typedData: {
types: Record<string, Array<{ name: string; type: string }>>;
primaryType: string;
domain: Record<string, any>;
message: Record<string, any>;
};
}
Example Addresses for Testing
Permit Phishing
| Address | Chain | Description |
|---|---|---|
0x84672cc56b6dad30cfa5f9751d9ccae6c39e29cd | ETH | AI Protocol user permit phishing |
0x624Fc3Dc249E37E8BFd3e834C4dF81Ff2dA1D0Ca | BSC | Malicious permit scammer |
Address Poisoning
| Address | Chain | Description |
|---|---|---|
0xd9A1C3788D81257612E2581A6ea0aDa244853a91 | ETH | $68M WBTC attack |
0x5f90e59d0a03fd2f8c56b8cc896c5b42594eb3a0 | ETH | $50M poisoning drain |
Known Drainers
| Address | Chain | Attribution |
|---|---|---|
0xe7d13137923142a0424771e1778865b88752b3c7 | ETH | WalletConnect phishing campaign |
0x1aDf5DAc035AE7FEC116e8345e005FB88d542f53 | ETH | Phishing scammer |
Clean Addresses (for comparison)
| Address | Chain | Description |
|---|---|---|
0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045 | ETH | Vitalik’s wallet (low risk) |
0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 | ETH | USDC contract (verified) |
API Quick Reference
| Endpoint | Use Case | Response Time |
|---|---|---|
POST /scan/{fromAddress}/transactions | Transaction simulation | ~1–3s (cold), ~50ms (cached) |
POST /scan/{fromAddress}/eip712 | Signature analysis | ~1–3s (cold), ~50ms (cached) |
GET /addresses/{address}/poisoning | Address poisoning | ~300ms |
GET /addresses/{address}/quick-profile | Recipient risk | ~200ms |
GET /wallets/{address}/approvals | Approval list | ~400ms |
Next Steps
Get Your API Key
Start protecting your users
API Reference
Complete endpoint documentation
Install the SDK
TypeScript SDK for integration
Address Poisoning
Learn more about this attack vector
