> ## Documentation Index
> Fetch the complete documentation index at: https://docs.webacy.com/llms.txt
> Use this file to discover all available pages before exploring further.

# dApp Browser Security

> Secure your dApp browser with URL phishing detection, transaction simulation, and contract verification to block lookalike domains before wallets connect.

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

<CardGroup cols={3}>
  <Card title="URL Protection" icon="link-slash">
    Block phishing sites before users connect their wallets
  </Card>

  <Card title="Transaction Preview" icon="eye">
    Show users what they're signing before they approve
  </Card>

  <Card title="Contract Verification" icon="shield-check">
    Verify the contracts users interact with are safe
  </Card>
</CardGroup>

**Why dApp browser developers choose Webacy:**

* **URL phishing detection** — Block known phishing and scam sites, and flag lookalikes of known projects
* **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](https://developers.webacy.co/billing))
* Basic familiarity with REST APIs or the [Webacy SDK](../sdk/installation)
* 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.

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST "https://api.webacy.com/url" \
    -H "x-api-key: YOUR_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"url": "https://uniswap-claim.com"}'
  ```

  ```typescript TypeScript theme={null}
  import { ThreatClient } from '@webacy-xyz/sdk';

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

  const result = await client.url.check('https://uniswap-claim.com');

  if (result.riskLevel === 'high') {
    // Block the connection
    showPhishingWarning({
      url: 'https://uniswap-claim.com',
      message: result.description,
    });
  }

  console.log(`Risk level: ${result.riskLevel}`);
  console.log(`Why: ${result.description}`);
  ```

  ```python Python theme={null}
  import requests

  response = requests.post(
      "https://api.webacy.com/url",
      headers={
          "x-api-key": "YOUR_API_KEY",
          "Content-Type": "application/json"
      },
      json={"url": "https://uniswap-claim.com"}
  )
  result = response.json()
  ```
</CodeGroup>

**Response:**

```json theme={null}
{
  "riskLevel": "high",
  "description": "This URL has been blacklisted.",
  "message": "Wrong response? Please report: https://forms.gle/4ievDbQDqRgrHcRV9"
}
```

| Field | Type | Description |
| - | - | - |
| `riskLevel` | `"high"` \| `"medium"` \| `"low"` \| `"unknown"` | `high`: known phishing or blocklisted site. `medium`: not listed, but similar to a known project's domain (possible impersonation). `low`: a known project's site. `unknown`: no verdict — including pages on shared hosts such as `sites.google.com`, `docs.google.com` or `storage.googleapis.com`, where anyone can publish, so the host's reputation says nothing about the page. |
| `description` | string | Human-readable reason for the verdict |
| `message` | string | Where to report a wrong verdict |

<Note>
  `unknown` means Webacy has no verdict for the site, not that it is safe. Treat it as unverified.
</Note>

<Info>
  The TypeScript samples use the `riskLevel` response and the `UrlRiskLevel` type, which need an `@webacy-xyz/sdk` release newer than 2.0.1. Older SDK versions type this response with fields the API doesn't return.
</Info>

### Mapping the Verdict to an Action

Turn the verdict into a browser action.

```typescript theme={null}
async function checkUrlSafety(url: string): Promise<{
  safe: boolean;
  action: 'allow' | 'warn' | 'block';
  reason?: string;
}> {
  const result = await client.url.check(url);

  switch (result.riskLevel) {
    case 'high':
      // Known phishing or blocklisted: block outright
      return { safe: false, action: 'block', reason: result.description };
    case 'medium':
      // Looks like a known project's domain: possible impersonation
      return { safe: false, action: 'warn', reason: result.description };
    case 'low':
      return { safe: true, action: 'allow' };
    default:
      // 'unknown': no verdict. Allow, but don't show a "verified" badge
      return { safe: false, action: 'warn', reason: 'This site has not been verified.' };
  }
}
```

### Homograph Attack Detection

<Warning>
  **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.
</Warning>

Common homograph substitutions:

| Real | Fake | Unicode |
| - | - | - |
| 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 flags lookalikes of known projects spelled with ASCII characters (`medium`), but Unicode lookalikes are not reliably flagged and can come back `unknown`. For any non-ASCII host, show the user its punycode form (`xn--…`) alongside the verdict.

***

## Transaction Security

Every transaction request is an opportunity to protect users.

### Pre-Signing Preview

Simulate transactions before users sign them.

<CodeGroup>
  ```bash cURL theme={null}
  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": "suspicious-dapp.com"
    }'
  ```

  ```typescript TypeScript theme={null}
  import { Transaction } from 'ethers';

  // `raw` is the serialized unsigned transaction the dApp is asking 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: currentDappDomain,
    }
  );

  // One item per simulated asset movement or approval
  const { simulation } = result as unknown as {
    simulation: Array<{
      txData: { changeType: 'TRANSFER' | 'APPROVE'; rawAmount?: string; symbol?: string; assetType?: string; counterpartyAddress: string };
      counterpartyRisk: { address: string; high?: number; overallRisk?: number; issues?: Array<{ tags: Array<{ name: string }> }> };
    }>;
  };

  displayTransactionPreview({
    assetChanges: simulation.map((i) => i.txData),
    warnings: simulation.flatMap((i) => i.counterpartyRisk.issues?.flatMap((issue) => issue.tags.map((t) => t.name)) ?? []),
    // Block on a flagged recipient; an empty simulation is inconclusive, not safe
    blocked: simulation.some((i) => (i.counterpartyRisk.high ?? 0) > 0),
  });
  ```

  ````python Python theme={null}
  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": "connected-dapp.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("BLOCK: flagged recipient", [t["name"] for i in item["counterpartyRisk"].get("issues", []) for t in i["tags"]])

  </CodeGroup>

  **What to show users:**

  | Element | Priority | Description |
  | --- | --- | --- |
  | Asset changes | High | What tokens/ETH will move |
  | Warnings | High | Specific risks detected |
  | Risk level | Medium | Overall transaction safety |
  | Contract address | Medium | What contract they're interacting with |

  ### Contract Verification

  Check if the contract the user is interacting with is legitimate.

  <CodeGroup>

  ```bash cURL
  curl -X GET "https://api.webacy.com/addresses/0xContractAddress...?chain=eth" \
    -H "x-api-key: YOUR_API_KEY"
  ````

  ```typescript TypeScript theme={null}
  const analysis = await client.addresses.analyze(
    '0xContractAddress...',
    { chain: Chain.ETH }
  );

  // Check if it's a known legitimate contract
  if (analysis.labels?.includes('verified_protocol')) {
    console.log('✓ Verified protocol');
  } else if (analysis.overallRisk > 50) {
    console.warn('⚠️ High risk contract');
  }

  // Check for drainer patterns
  if (analysis.issues?.some(i => i.tag === 'drainer')) {
    console.error('🛑 Known drainer contract detected!');
  }
  ```

  ```python Python theme={null}
  response = requests.get(
      "https://api.webacy.com/addresses/0xContractAddress...",
      params={"chain": "eth"},
      headers={"x-api-key": "YOUR_API_KEY"}
  )
  analysis = response.json()
  ```
</CodeGroup>

***

## 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.

```typescript theme={null}
import type { UrlRiskLevel } from '@webacy-xyz/sdk';

// Your app's own "connect anyway?" prompt.
declare function confirmConnection(domain: string, reason: string): Promise<boolean>;

interface ConnectedDapp {
  domain: string;
  connectedAt: Date;
  lastChecked: Date;
  riskLevel: UrlRiskLevel;
}

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.riskLevel === 'high') {
      // Block connection
      return false;
    }
    if (result.riskLevel === 'medium' || result.riskLevel === 'unknown') {
      // Possible impersonation, or no verdict (most new phishing domains are
      // still unknown): the user decides before a wallet connects.
      if (!(await confirmConnection(domain, result.description))) return false;
    }

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

    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}`);
      const previous = dapp.riskLevel;

      dapp.lastChecked = new Date();
      dapp.riskLevel = result.riskLevel;

      // Flag risky verdicts, and a known-safe site that lost its verdict
      // (low → unknown) since the user connected.
      if (
        result.riskLevel === 'high' ||
        result.riskLevel === 'medium' ||
        (result.riskLevel === 'unknown' && previous === 'low')
      ) {
        flagged.push(dapp);
      }
    }

    return flagged;
  }
}
```

### Risk-Scored Connections

Show users the risk level of their connected dApps.

```typescript theme={null}
async function getConnectionsWithRisk(
  connectedDomains: string[]
): Promise<Array<{
  domain: string;
  riskLevel: UrlRiskLevel;
  recommendation?: string;
}>> {
  return Promise.all(
    connectedDomains.map(async domain => {
      const { riskLevel } = await client.url.check(`https://${domain}`);

      const recommendation =
        riskLevel === 'high' ? 'Disconnect immediately'
        : riskLevel === 'medium' ? 'Possible impersonation: verify the domain'
        : riskLevel === 'unknown' ? 'Unverified site: exercise caution'
        : undefined;

      return { domain, riskLevel, recommendation };
    })
  );
}
```

***

## Complete Integration Workflow

### Navigation Flow

```mermaid theme={null}
flowchart TD
    A[User Navigates to URL] --> B{URL Check}
    B -->|high| C["🛑 BLOCK + Show Warning"]
    B -->|medium| D["⚠️ Possible impersonation warning"]
    B -->|unknown| E["ℹ️ Unverified-site banner"]
    B -->|low| F["✅ Allow Navigation"]
    D --> G{User Proceeds?}
    E --> F
    G -->|Yes| F
    G -->|No| H[Return to Safety]
```

### Wallet Connection Flow

```mermaid theme={null}
flowchart TD
    A[dApp Requests Connection] --> B{URL Already Checked?}
    B -->|No| C[Check URL Safety]
    B -->|Yes, Safe| D[Show Connection Request]
    C -->|Safe| D
    C -->|Dangerous| E["🛑 BLOCK Connection"]
    D --> F{User Approves?}
    F -->|Yes| G[Connect Wallet]
    F -->|No| H[Cancel]
    G --> I[Add to Monitored Connections]
```

### Transaction Request Flow

```mermaid theme={null}
flowchart TD
    A[Transaction Request] --> B[Simulate Transaction]
    B --> C{Risk Level?}
    C -->|Critical| D["🛑 BLOCK Transaction"]
    C -->|High| E["⚠️ Strong Warning + Details"]
    C -->|Medium| F["ℹ️ Show Preview + Caution"]
    C -->|Low| G["Show Transaction Preview"]
    E --> H{User Confirms?}
    F --> H
    G --> I[User Signs]
    H -->|Yes| I
    H -->|No| J[Cancel]
    D --> K[Show Why Blocked]
```

### Full TypeScript Implementation

<Accordion title="Complete dApp Browser Security Module">
  ```typescript theme={null}
  import { ThreatClient, Chain, type ScanChainId } 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;
  }

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

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

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

    switch (result.riskLevel) {
      case 'high':
        // Known phishing or blocklisted site
        return {
          allowed: false,
          riskLevel: 'blocked',
          message: `${result.description} Do not connect your wallet.`,
        };
      case 'medium':
        // Similar to a known project's domain: possible impersonation
        return {
          allowed: true,
          riskLevel: 'danger',
          message: `${result.description} Check the address bar before connecting.`,
        };
      case 'low':
        return { allowed: true, riskLevel: 'safe' };
      case 'unknown':
      default:
        // Fail toward caution: an unexpected level is never treated as safe.
        return {
          allowed: true,
          riskLevel: 'warning',
          message: 'This site has not been verified. Exercise caution.',
        };
    }
  }

  // Transaction Security
  async function checkTransaction(
    userAddress: string,
    // `raw` = serialized unsigned tx (ethers `Transaction.unsignedSerialized`); `to` for the contract pre-check
    txData: { to: string; raw: string },
    chainId: ScanChainId,
    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 result = (await client.scan.scanTransaction(userAddress, {
      tx: { from: userAddress, raw: txData.raw },
      chain: chainId,
      domain: dappDomain,
    })) as unknown as {
      simulation: Array<{
        txData: { changeType: 'TRANSFER' | 'APPROVE'; rawAmount?: string; symbol?: string; assetType?: string; counterpartyAddress: string };
        counterpartyRisk: { high?: number; medium?: number; overallRisk?: number; issues?: Array<{ tags: Array<{ name: string }> }> };
        assetRisk: { high?: number; medium?: number };
        functionRisk?: { functionName: string; description: string };
      }>;
    };

    // Determine risk level from the recipient / spender and token profiles.
    // An empty simulation, or a recipient that could not be scored, is inconclusive.
    let riskLevel: RiskLevel = 'safe';
    const parties = result.simulation.flatMap((i) => [i.counterpartyRisk, i.assetRisk]);
    if (parties.some((p) => (p.high ?? 0) > 0)) {
      riskLevel = 'blocked';
    } else if (parties.some((p) => (p.medium ?? 0) > 0)) {
      riskLevel = 'danger';
    } else if (
      result.simulation.length === 0 ||
      result.simulation.some((i) => i.counterpartyRisk.overallRisk === undefined)
    ) {
      riskLevel = 'warning';
    }

    return {
      allowed: riskLevel !== 'blocked',
      riskLevel,
      preview: {
        assetChanges: result.simulation.map((i) => ({
          type: i.txData.changeType,
          symbol: i.txData.symbol ?? i.txData.assetType ?? 'asset',
          amount: i.txData.rawAmount ?? '',
        })),
        warnings: result.simulation.flatMap((i) => [
          ...(i.counterpartyRisk.issues?.flatMap((issue) => issue.tags.map((t) => t.name)) ?? []),
          ...(i.functionRisk ? [`Risky function: ${i.functionRisk.functionName}`] : []),
        ]),
      },
      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,
        };
      }

      this.connections.set(domain, {
        domain,
        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 refreshRiskLevels(): Promise<ConnectionInfo[]> {
      const flagged: ConnectionInfo[] = [];

      for (const [domain, info] of this.connections) {
        const { riskLevel } = await checkUrlSafety(`https://${domain}`);
        const previous = info.riskLevel;
        info.riskLevel = riskLevel;
        // Flag blocked/danger, and a site that was safe and is now unverified.
        if (
          riskLevel === 'blocked' ||
          riskLevel === 'danger' ||
          (riskLevel === 'warning' && previous === 'safe')
        ) {
          flagged.push(info);
        }
      }

      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; raw: string },
      chainId: ScanChainId,
      dappDomain: string
    ) {
      return checkTransaction(userAddress, txData, chainId, dappDomain);
    }

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

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

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

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

  // Export the controller
  export const browserSecurity = new DappBrowserSecurity();
  ```
</Accordion>

***

## 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`

<Warning>
  **Do not actually visit** these example domains—they may be active phishing sites. Use the URL check API to verify them safely.
</Warning>

### Legitimate Sites (for comparison)

| URL | Expected Result |
| - | - |
| `https://uniswap.org` | `low` (known project site) |
| `https://opensea.io` | `low` (known project site) |
| `https://app.aave.com` | `low` (subdomain of a known project) |

### Test Addresses

| Address | Chain | Description |
| - | - | - |
| `0xe7d13137923142a0424771e1778865b88752b3c7` | ETH | WalletConnect phishing campaign |
| `0x84672cc56b6dad30cfa5f9751d9ccae6c39e29cd` | ETH | Permit phishing drainer |
| `0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045` | ETH | Vitalik's wallet (safe baseline) |

***

## API Quick Reference

| Endpoint | Use Case | Response Time |
| - | - | - |
| `POST /url` | URL phishing detection | \~0.5s (p95 \~1s) |
| `POST /scan/{fromAddress}/transactions` | Transaction simulation | \~1–3s (cold), \~50ms (cached) |
| `GET /addresses/{address}` | Contract verification | \~500ms |
| `GET /addresses/{address}/quick-profile` | Fast risk check | \~200ms |

***

## Next Steps

<CardGroup cols={2}>
  <Card title="Get Your API Key" icon="key" href="https://developers.webacy.co/billing">
    Start protecting your browser users
  </Card>

  <Card title="API Reference" icon="book" href="../api-reference/introduction">
    Complete endpoint documentation
  </Card>

  <Card title="Phishing Guide" icon="fish" href="../glossary/phishing">
    Understand phishing attack patterns
  </Card>

  <Card title="Install the SDK" icon="download" href="../sdk/installation">
    TypeScript SDK for integration
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.