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.curl -X POST "https://api.webacy.com/scan/transaction" \
-H "x-api-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"walletAddress": "0xUserAddress...",
"tx": {
"from": "0xUserAddress...",
"to": "0xContractAddress...",
"data": "0xTransactionData...",
"value": "0x0"
},
"chain": 1,
"domain": "app.example.com"
}'
import { ThreatClient, Chain } from '@webacy-xyz/sdk';
const client = new ThreatClient({ apiKey: process.env.WEBACY_API_KEY });
const simulation = await client.scan.scanTransaction(
'0xUserAddress...',
{
tx: {
from: '0xUserAddress...',
to: '0xContractAddress...',
data: '0xTransactionData...',
value: '0x0',
},
chain: 1, // Ethereum mainnet
domain: 'connected-dapp.com',
}
);
// Display to user before signing
console.log(`Risk level: ${simulation.riskLevel}`);
for (const change of simulation.assetChanges ?? []) {
if (change.type === 'transfer_out') {
console.log(`⚠️ You will SEND: ${change.amount} ${change.symbol}`);
} else if (change.type === 'transfer_in') {
console.log(`✓ You will RECEIVE: ${change.amount} ${change.symbol}`);
} else if (change.type === 'approval') {
console.log(`🔓 APPROVAL: ${change.spender} can spend ${change.amount} ${change.symbol}`);
}
}
import requests
response = requests.post(
"https://api.webacy.com/scan/transaction",
headers={
"x-api-key": "YOUR_API_KEY",
"Content-Type": "application/json"
},
json={
"walletAddress": "0xUserAddress...",
"tx": {
"from": "0xUserAddress...",
"to": "0xContractAddress...",
"data": "0xTransactionData...",
"value": "0x0"
},
"chain": 1,
"domain": "app.example.com"
}
)
simulation = response.json()
| Field | What It Means | Display Priority |
|---|---|---|
assetChanges[] | Tokens/ETH moving in or out | Always show |
riskLevel | Overall transaction risk | Show with color coding |
warnings[] | Specific threats detected | Show prominently if present |
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/eip712" \
-H "x-api-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"walletAddress": "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"
}'
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',
}
);
// Critical: Check risk level before allowing signature
if (result.riskLevel === 'critical' || result.riskLevel === 'high') {
console.error('⛔ DANGEROUS SIGNATURE DETECTED');
console.error(`Type: ${result.messageType}`);
console.error('This signature could drain your entire wallet.');
// Block the signature
}
response = requests.post(
"https://api.webacy.com/scan/eip712",
headers={
"x-api-key": "YOUR_API_KEY",
"Content-Type": "application/json"
},
json={
"walletAddress": "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"
}
)
| Red Flag | What It Means |
|---|---|
value = max uint256 | Unlimited token approval |
Unknown spender | Approving unknown contract |
Long deadline | Signature valid for extended period |
High riskLevel | Known malicious pattern detected |
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 } 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: string;
symbol: string;
amount: string;
}>;
warnings: string[];
blockReason?: string;
}
// Main pre-signing check
async function preSigningCheck(
userAddress: string,
request: TransactionRequest | EIP712Request,
chain: number,
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,
});
const warnings: string[] = [];
// Check for permit signatures
if (result.messageType?.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');
}
}
// Block critical risk
if (result.riskLevel === 'critical') {
return {
allowed: false,
riskLevel: 'critical',
warnings,
blockReason: 'This signature matches known malicious patterns and could drain your wallet.',
};
}
return {
allowed: true,
riskLevel: result.riskLevel as RiskLevel,
warnings,
};
}
// Transaction check
async function checkTransaction(
userAddress: string,
request: TransactionRequest,
chain: number,
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
const simulation = await client.scan.scanTransaction(userAddress, {
tx: {
from: userAddress,
to: request.to,
data: request.data,
value: request.value || '0x0',
},
chain,
domain: dappDomain,
});
// Collect warnings
warnings.push(...(simulation.warnings || []));
// Check for dangerous approvals in asset changes
const approvals = simulation.assetChanges?.filter(c => c.type === 'approval') || [];
for (const approval of approvals) {
if (approval.amount === 'unlimited') {
warnings.push(`Unlimited approval to ${approval.spender}`);
}
}
// Block critical transactions
if (simulation.riskLevel === 'critical') {
return {
allowed: false,
riskLevel: 'critical',
assetChanges: simulation.assetChanges,
warnings,
blockReason: 'This transaction has been identified as dangerous.',
};
}
return {
allowed: true,
riskLevel: simulation.riskLevel as RiskLevel,
assetChanges: simulation.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: number): 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 {
to?: string;
data?: string;
value?: 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/transaction | Transaction simulation | ~500ms |
POST /scan/eip712 | Signature analysis | ~300ms |
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
