Skip to main content
You visit a “token claim” site. The transaction looks normal—just a contract interaction. You approve it without a second thought. 48 hours later, your entire wallet is drained through a permit signature you didn’t understand. Transaction simulation would have caught it. This guide shows you how to build that last line of defense.

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
Why developers choose Webacy:
  • 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"
  }'
What to show users:
FieldWhat It MeansDisplay Priority
assetChanges[]Tokens/ETH moving in or outAlways show
riskLevelOverall transaction riskShow with color coding
warnings[]Specific threats detectedShow 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"
  }'
Key red flags to surface:
Red FlagWhat It Means
value = max uint256Unlimited token approval
Unknown spenderApproving unknown contract
Long deadlineSignature valid for extended period
High riskLevelKnown 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"

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"

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"

Complete Integration Workflow

Pre-Signing Check Flow

Approval Health Check Flow

Full TypeScript Implementation

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

AddressChainDescription
0x84672cc56b6dad30cfa5f9751d9ccae6c39e29cdETHAI Protocol user permit phishing
0x624Fc3Dc249E37E8BFd3e834C4dF81Ff2dA1D0CaBSCMalicious permit scammer

Address Poisoning

AddressChainDescription
0xd9A1C3788D81257612E2581A6ea0aDa244853a91ETH$68M WBTC attack
0x5f90e59d0a03fd2f8c56b8cc896c5b42594eb3a0ETH$50M poisoning drain

Known Drainers

AddressChainAttribution
0xe7d13137923142a0424771e1778865b88752b3c7ETHWalletConnect phishing campaign
0x1aDf5DAc035AE7FEC116e8345e005FB88d542f53ETHPhishing scammer

Clean Addresses (for comparison)

AddressChainDescription
0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045ETHVitalik’s wallet (low risk)
0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48ETHUSDC contract (verified)

API Quick Reference

EndpointUse CaseResponse Time
POST /scan/transactionTransaction simulation~500ms
POST /scan/eip712Signature analysis~300ms
GET /addresses/{address}/poisoningAddress poisoning~300ms
GET /addresses/{address}/quick-profileRecipient risk~200ms
GET /wallets/{address}/approvalsApproval list~400ms

Next Steps