Skip to main content
A phishing link can look exactly like Uniswap—the domain uniswaρ.com uses a Greek rho (ρ) instead of a ‘p’. In one attack, 2,400 users connected their wallets before anyone noticed, and $4.7 million was drained in 24 hours. This guide shows you how to protect your users and prevent this kind of attack.

Why dApp Browsers Need Security

URL Protection

Block phishing sites before users connect their wallets

Transaction Preview

Show users what they’re signing before they approve

Contract Verification

Verify the contracts users interact with are safe
Why dApp browser developers choose Webacy:
  • URL phishing detection — Catch lookalike domains, homograph attacks, and known scam sites
  • Transaction simulation — Show users exactly what will happen before signing
  • Contract verification — Check if the contract is legitimate or malicious
  • Real-time risk scoring — Fast enough for inline warnings without blocking navigation
  • Multi-chain support — Protect across all major networks

Prerequisites

Before implementing browser security, ensure you have:
  • A Webacy API key (sign up here)
  • Basic familiarity with REST APIs or the Webacy SDK
  • Your browser’s navigation and transaction signing flows identified

URL Security

The first line of defense is stopping users from connecting to malicious sites.

Phishing Detection

Check URLs when users navigate to new sites or before they connect their wallet.
curl -X POST "https://api.webacy.com/url/check" \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://uniswap-claim.xyz"}'
Response fields:
FieldTypeDescription
isPhishingbooleanKnown phishing site
isMalwarebooleanKnown malware distribution
riskScorenumber0-100 risk score
categoriesstring[]Risk categories (drainer, scam, etc.)

Malware Site Blocking

Block sites that attempt to install malware or exploit browser vulnerabilities.
async function checkUrlSafety(url: string): Promise<{
  safe: boolean;
  action: 'allow' | 'warn' | 'block';
  reason?: string;
}> {
  const result = await client.url.check(url);

  // Immediate block for known threats
  if (result.isPhishing) {
    return {
      safe: false,
      action: 'block',
      reason: 'This site has been identified as a phishing site.',
    };
  }

  if (result.isMalware) {
    return {
      safe: false,
      action: 'block',
      reason: 'This site distributes malware.',
    };
  }

  // Warn for high risk
  if (result.riskScore > 70) {
    return {
      safe: false,
      action: 'warn',
      reason: `High risk site (score: ${result.riskScore})`,
    };
  }

  // Allow with caution for medium risk
  if (result.riskScore > 40) {
    return {
      safe: true,
      action: 'warn',
      reason: 'Exercise caution with this site.',
    };
  }

  return {
    safe: true,
    action: 'allow',
  };
}

Homograph Attack Detection

Homograph attacks use Unicode characters that look identical to ASCII letters. аpple.com (Cyrillic ‘а’) and apple.com (Latin ‘a’) look the same but are completely different domains.
Common homograph substitutions:
RealFakeUnicode
aаCyrillic U+0430
eеCyrillic U+0435
oоCyrillic U+043E
pрCyrillic U+0440
cсCyrillic U+0441
xхCyrillic U+0445
The URL check API automatically detects these attacks.

Transaction Security

Every transaction request is an opportunity to protect users.

Pre-Signing Preview

Simulate transactions before users sign them.
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": "suspicious-dapp.com"
  }'
What to show users:
ElementPriorityDescription
Asset changesHighWhat tokens/ETH will move
WarningsHighSpecific risks detected
Risk levelMediumOverall transaction safety
Contract addressMediumWhat contract they’re interacting with

Contract Verification

Check if the contract the user is interacting with is legitimate.
curl -X GET "https://api.webacy.com/addresses/0xContractAddress...?chain=eth" \
  -H "x-api-key: YOUR_API_KEY"

Session Protection

Monitor the entire browsing session for threats.

Connected dApp Monitoring

Track all dApps the user has connected to and periodically recheck their safety.
interface ConnectedDapp {
  domain: string;
  connectedAt: Date;
  lastChecked: Date;
  riskScore: number;
  status: 'safe' | 'warning' | 'danger';
}

class DappSessionMonitor {
  private connectedDapps: Map<string, ConnectedDapp> = new Map();
  private checkInterval = 5 * 60 * 1000; // 5 minutes

  async onConnect(domain: string): Promise<boolean> {
    const result = await client.url.check(`https://${domain}`);

    if (result.isPhishing || result.isMalware) {
      // Block connection
      return false;
    }

    this.connectedDapps.set(domain, {
      domain,
      connectedAt: new Date(),
      lastChecked: new Date(),
      riskScore: result.riskScore,
      status: this.getStatus(result.riskScore),
    });

    return true;
  }

  async recheckConnectedDapps(): Promise<ConnectedDapp[]> {
    const flagged: ConnectedDapp[] = [];

    for (const [domain, dapp] of this.connectedDapps) {
      // Skip if recently checked
      if (Date.now() - dapp.lastChecked.getTime() < this.checkInterval) {
        continue;
      }

      const result = await client.url.check(`https://${domain}`);

      dapp.lastChecked = new Date();
      dapp.riskScore = result.riskScore;
      dapp.status = this.getStatus(result.riskScore);

      if (result.isPhishing || result.isMalware || result.riskScore > 70) {
        flagged.push(dapp);
      }
    }

    return flagged;
  }

  private getStatus(riskScore: number): 'safe' | 'warning' | 'danger' {
    if (riskScore > 70) return 'danger';
    if (riskScore > 40) return 'warning';
    return 'safe';
  }
}

Risk-Scored Connections

Show users the risk level of their connected dApps.
async function getConnectionsWithRisk(
  connectedDomains: string[]
): Promise<Array<{
  domain: string;
  riskScore: number;
  status: 'safe' | 'warning' | 'danger';
  recommendation?: string;
}>> {
  const results = await Promise.all(
    connectedDomains.map(async domain => {
      const result = await client.url.check(`https://${domain}`);

      let status: 'safe' | 'warning' | 'danger' = 'safe';
      let recommendation: string | undefined;

      if (result.isPhishing || result.isMalware) {
        status = 'danger';
        recommendation = 'Disconnect immediately';
      } else if (result.riskScore > 70) {
        status = 'danger';
        recommendation = 'Consider disconnecting';
      } else if (result.riskScore > 40) {
        status = 'warning';
        recommendation = 'Exercise caution';
      }

      return {
        domain,
        riskScore: result.riskScore,
        status,
        recommendation,
      };
    })
  );

  return results;
}

Complete Integration Workflow

Wallet Connection Flow

Transaction Request Flow

Full TypeScript Implementation

import { ThreatClient, Chain } from '@webacy-xyz/sdk';

const client = new ThreatClient({
  apiKey: process.env.WEBACY_API_KEY,
  defaultChain: Chain.ETH,
});

// Types
type RiskLevel = 'safe' | 'warning' | 'danger' | 'blocked';

interface UrlCheckResult {
  allowed: boolean;
  riskLevel: RiskLevel;
  message?: string;
  categories?: string[];
}

interface TransactionCheckResult {
  allowed: boolean;
  riskLevel: RiskLevel;
  preview: {
    assetChanges: Array<{
      type: string;
      symbol: string;
      amount: string;
    }>;
    warnings: string[];
  };
  requiresConfirmation: boolean;
}

interface ConnectionInfo {
  domain: string;
  riskScore: number;
  riskLevel: RiskLevel;
  connectedAt: Date;
  lastTransaction?: Date;
}

// URL Security
async function checkUrlSafety(url: string): Promise<UrlCheckResult> {
  const result = await client.url.check(url);

  // Immediate block
  if (result.isPhishing) {
    return {
      allowed: false,
      riskLevel: 'blocked',
      message: 'This site has been identified as a phishing site. Do not connect your wallet.',
      categories: result.categories,
    };
  }

  if (result.isMalware) {
    return {
      allowed: false,
      riskLevel: 'blocked',
      message: 'This site distributes malware. Navigation blocked for your safety.',
      categories: result.categories,
    };
  }

  // High risk warning
  if (result.riskScore > 70) {
    return {
      allowed: true,
      riskLevel: 'danger',
      message: 'This site has a high risk score. Proceed with extreme caution.',
      categories: result.categories,
    };
  }

  // Medium risk caution
  if (result.riskScore > 40) {
    return {
      allowed: true,
      riskLevel: 'warning',
      message: 'Exercise caution when interacting with this site.',
    };
  }

  return {
    allowed: true,
    riskLevel: 'safe',
  };
}

// Transaction Security
async function checkTransaction(
  userAddress: string,
  txData: { to: string; data: string; value: string },
  chainId: number,
  dappDomain: string
): Promise<TransactionCheckResult> {
  // First, verify the contract
  const chain = chainIdToChain(chainId);
  const contractAnalysis = await client.addresses.analyze(txData.to, { chain });

  // Check for known drainers
  const isDrainer = contractAnalysis.issues?.some(i =>
    i.tag.toLowerCase().includes('drainer')
  );

  if (isDrainer) {
    return {
      allowed: false,
      riskLevel: 'blocked',
      preview: {
        assetChanges: [],
        warnings: ['This contract has been identified as a wallet drainer.'],
      },
      requiresConfirmation: false,
    };
  }

  // Simulate the transaction
  const simulation = await client.scan.scanTransaction(userAddress, {
    tx: {
      from: userAddress,
      to: txData.to,
      data: txData.data,
      value: txData.value,
    },
    chain: chainId,
    domain: dappDomain,
  });

  // Determine risk level
  let riskLevel: RiskLevel = 'safe';
  if (simulation.riskLevel === 'critical') {
    riskLevel = 'blocked';
  } else if (simulation.riskLevel === 'high') {
    riskLevel = 'danger';
  } else if (simulation.riskLevel === 'medium') {
    riskLevel = 'warning';
  }

  return {
    allowed: riskLevel !== 'blocked',
    riskLevel,
    preview: {
      assetChanges: simulation.assetChanges ?? [],
      warnings: simulation.warnings ?? [],
    },
    requiresConfirmation: riskLevel === 'danger' || riskLevel === 'warning',
  };
}

// Connection Management
class ConnectionManager {
  private connections: Map<string, ConnectionInfo> = new Map();

  async connect(domain: string): Promise<{
    success: boolean;
    riskLevel: RiskLevel;
    message?: string;
  }> {
    const urlCheck = await checkUrlSafety(`https://${domain}`);

    if (!urlCheck.allowed) {
      return {
        success: false,
        riskLevel: urlCheck.riskLevel,
        message: urlCheck.message,
      };
    }

    const result = await client.url.check(`https://${domain}`);

    this.connections.set(domain, {
      domain,
      riskScore: result.riskScore,
      riskLevel: urlCheck.riskLevel,
      connectedAt: new Date(),
    });

    return {
      success: true,
      riskLevel: urlCheck.riskLevel,
      message: urlCheck.message,
    };
  }

  disconnect(domain: string): void {
    this.connections.delete(domain);
  }

  getConnections(): ConnectionInfo[] {
    return Array.from(this.connections.values());
  }

  async refreshRiskScores(): Promise<ConnectionInfo[]> {
    const flagged: ConnectionInfo[] = [];

    for (const [domain, info] of this.connections) {
      const result = await client.url.check(`https://${domain}`);

      info.riskScore = result.riskScore;

      if (result.isPhishing || result.isMalware) {
        info.riskLevel = 'blocked';
        flagged.push(info);
      } else if (result.riskScore > 70) {
        info.riskLevel = 'danger';
        flagged.push(info);
      } else if (result.riskScore > 40) {
        info.riskLevel = 'warning';
      } else {
        info.riskLevel = 'safe';
      }
    }

    return flagged;
  }
}

// Browser Security Controller
class DappBrowserSecurity {
  private connectionManager = new ConnectionManager();

  // Call when user navigates to a new URL
  async onNavigate(url: string): Promise<UrlCheckResult> {
    return checkUrlSafety(url);
  }

  // Call when dApp requests wallet connection
  async onConnectionRequest(domain: string) {
    return this.connectionManager.connect(domain);
  }

  // Call when dApp requests transaction signature
  async onTransactionRequest(
    userAddress: string,
    txData: { to: string; data: string; value: string },
    chainId: number,
    dappDomain: string
  ) {
    return checkTransaction(userAddress, txData, chainId, dappDomain);
  }

  // Call periodically to check connected dApps
  async refreshConnections() {
    return this.connectionManager.refreshRiskScores();
  }

  // Get all active connections
  getActiveConnections() {
    return this.connectionManager.getConnections();
  }

  // Disconnect a dApp
  disconnect(domain: string) {
    this.connectionManager.disconnect(domain);
  }
}

// Helper function
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;
}

// Export the controller
export const browserSecurity = new DappBrowserSecurity();

Example URLs for Testing

Known Phishing Sites

Test with URLs that mimic legitimate sites but use different domains:
  • uniswap-claim.com
  • opensea-claim.xyz
  • metamask-support.io
Do not actually visit these example domains—they may be active phishing sites. Use the URL check API to verify them safely.

Legitimate Sites (for comparison)

URLExpected Result
https://uniswap.orgLow risk, verified
https://opensea.ioLow risk, verified
https://app.aave.comLow risk, verified

Test Addresses

AddressChainDescription
0xe7d13137923142a0424771e1778865b88752b3c7ETHWalletConnect phishing campaign
0x84672cc56b6dad30cfa5f9751d9ccae6c39e29cdETHPermit phishing drainer
0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045ETHVitalik’s wallet (safe baseline)

API Quick Reference

EndpointUse CaseResponse Time
POST /url/checkURL phishing detection~200ms
POST /scan/transactionTransaction simulation~500ms
GET /addresses/{address}Contract verification~500ms
GET /addresses/{address}/quick-profileFast risk check~200ms

Next Steps