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

# Build a TLSNotary Webapp

> Step-by-step tutorial to build a browser-based TLSNotary attestation application

# Build a TLSNotary Webapp

This tutorial walks you through building a complete browser-based TLSNotary attestation application from scratch. By the end, you'll have a working webapp that can:

* Connect to the Demos Network
* Request attestation tokens
* Perform TLS attestations on HTTPS endpoints
* Store cryptographic proofs on-chain

## Prerequisites

* Node.js 18+ or Bun
* Basic knowledge of React and TypeScript
* A Demos wallet with some DEM tokens for fees

## Project Setup

### Step 1: Initialize Project

```bash theme={null}
mkdir tlsnotary-app
cd tlsnotary-app
bun init -y
```

### Step 2: Install Dependencies

<Info>
  The Demos SDK (`@kynesyslabs/demosdk`) includes all TLSNotary WASM files and webpack helpers. You don't need to install `tlsn-js` separately!
</Info>

```bash theme={null}
bun add @kynesyslabs/demosdk react react-dom comlink
bun add -d webpack webpack-cli webpack-dev-server ts-loader \
  html-webpack-plugin copy-webpack-plugin typescript \
  @types/react @types/react-dom \
  crypto-browserify stream-browserify vm-browserify buffer process
```

### Step 3: Create Project Structure

```
tlsnotary-app/
├── src/
│   ├── app.tsx           # Main React application
│   ├── worker.ts         # Web Worker for WASM
│   ├── TLSNotaryClient.ts # WASM client wrapper
│   └── index.html        # HTML template
├── webpack.config.cjs    # Webpack configuration
├── tsconfig.json         # TypeScript configuration
└── package.json
```

## Configuration Files

### tsconfig.json

```json theme={null}
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "jsx": "react-jsx",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "lib": ["DOM", "ES2020"]
  },
  "include": ["src/**/*"]
}
```

### webpack.config.cjs

<Warning>
  The cross-origin isolation headers are **required** for TLSNotary WASM to work. Without them, `SharedArrayBuffer` won't be available and the attestation will fail.
</Warning>

The SDK provides webpack helpers that automatically configure WASM file copying and required polyfills:

```javascript theme={null}
const webpack = require('webpack');
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

// Import TLSNotary webpack helpers from SDK - no need to install tlsn-js!
const {
  getTlsnWebpackConfig,
  getCrossOriginHeaders,
} = require('@kynesyslabs/demosdk/tlsnotary/webpack');

// Get the TLSNotary webpack configuration
// Output to dist root (.) because tlsn-js expects WASM files at the web root
const tlsnConfig = getTlsnWebpackConfig({
  wasmOutputPath: '.', // Output to dist/ root
});

module.exports = {
  mode: 'development',
  entry: {
    app: path.join(__dirname, 'src', 'app.tsx'),
  },
  output: {
    filename: '[name].bundle.js',
    path: path.resolve(__dirname, 'dist'),
    clean: true,
    publicPath: '/',
  },
  module: {
    rules: [
      {
        test: /\.(ts|tsx)$/,
        exclude: /node_modules/,
        use: ['ts-loader'],
      },
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader'],
      },
    ],
  },
  resolve: {
    extensions: ['.js', '.jsx', '.ts', '.tsx'],
    fallback: {
      // TLSNotary polyfills from SDK
      ...tlsnConfig.resolve.fallback,
      // Additional polyfills for this app
      crypto: require.resolve('crypto-browserify'),
      stream: require.resolve('stream-browserify'),
      vm: require.resolve('vm-browserify'),
      // Stub out unused Node.js modules
      constants: false,
      readline: false,
    },
  },
  plugins: [
    // TLSNotary plugins (copies WASM files from SDK)
    ...tlsnConfig.plugins,
    // App plugins
    new HtmlWebpackPlugin({
      template: path.join(__dirname, 'src', 'index.html'),
      filename: 'index.html',
    }),
    // Provide Node.js globals for browser
    new webpack.ProvidePlugin({
      process: 'process/browser',
      Buffer: ['buffer', 'Buffer'],
    }),
  ],
  experiments: {
    // Enable async WASM loading
    ...tlsnConfig.experiments,
  },
  devServer: {
    port: 3000,
    host: 'localhost',
    hot: true,
    // Required for SharedArrayBuffer (WASM threads) - from SDK helper
    headers: getCrossOriginHeaders(),
  },
};
```

<Tip>
  The `getTlsnWebpackConfig()` helper automatically:

  * Configures CopyWebpackPlugin to copy WASM files from the SDK
  * Sets up Node.js polyfills required by TLSNotary
  * Enables async WebAssembly experiments

  The `getCrossOriginHeaders()` helper returns the required COOP/COEP headers for SharedArrayBuffer.
</Tip>

### package.json scripts

Add these scripts to your `package.json`:

```json theme={null}
{
  "scripts": {
    "dev": "webpack serve --mode development",
    "build": "webpack --mode production"
  }
}
```

## Source Files

### src/index.html

```html theme={null}
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>TLSNotary Attestation App</title>
  <style>
    body {
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
      max-width: 800px;
      margin: 0 auto;
      padding: 20px;
      background: #f5f5f5;
    }
    .card {
      background: white;
      padding: 20px;
      border-radius: 8px;
      margin-bottom: 20px;
      box-shadow: 0 2px 4px rgba(0,0,0,0.1);
    }
    input, button, textarea {
      padding: 10px;
      margin: 5px 0;
      border-radius: 4px;
      border: 1px solid #ddd;
    }
    button {
      background: #3B82F6;
      color: white;
      border: none;
      cursor: pointer;
    }
    button:hover { background: #2563EB; }
    button:disabled { background: #ccc; cursor: not-allowed; }
    .log {
      background: #1a1a1a;
      color: #00ff00;
      padding: 15px;
      border-radius: 4px;
      font-family: monospace;
      font-size: 12px;
      max-height: 300px;
      overflow-y: auto;
    }
    .error { color: #ff4444; }
    .success { color: #44ff44; }
  </style>
</head>
<body>
  <div id="root"></div>
</body>
</html>
```

### src/worker.ts

The Web Worker loads and exposes the TLSNotary WASM module. All imports come from the SDK:

```typescript theme={null}
/**
 * Web Worker for TLSNotary WASM
 *
 * Uses Comlink to expose the tlsn-js API to the main thread.
 * The WASM runs in a worker to not block the UI.
 *
 * NOTE: We import from the SDK which re-exports tlsn-js classes.
 * This means users don't need to install tlsn-js separately!
 */
import * as Comlink from 'comlink';
import {
  init,
  Prover,
  Presentation,
  NotaryServer,
  Transcript,
} from '@kynesyslabs/demosdk/tlsnotary';

// Expose the API to the main thread
Comlink.expose({
  init,
  Prover,
  Presentation,
  NotaryServer,
  Transcript,
});
```

### src/TLSNotaryClient.ts

This wrapper handles the WASM initialization and provides a clean API. All types come from the SDK:

```typescript theme={null}
import * as Comlink from 'comlink';
import {
  Transcript,
  type TLSNotaryConfig,
  type AttestResult,
  type AttestOptions,
  type VerificationResult,
  type ProxyRequestResponse,
  type StatusCallback,
} from '@kynesyslabs/demosdk/tlsnotary';
import type { Prover as TProver, Presentation as TPresentation } from '@kynesyslabs/demosdk/tlsnotary';

// Create worker with Comlink
const worker: any = Comlink.wrap(
  new Worker(new URL('./worker.ts', import.meta.url))
);

export class TLSNotaryClient {
  private config: TLSNotaryConfig;
  private initialized = false;

  constructor(config: TLSNotaryConfig) {
    this.config = {
      loggingLevel: 'Info',
      ...config,
    };
  }

  async initialize(): Promise<void> {
    if (this.initialized) return;
    await worker.init({ loggingLevel: this.config.loggingLevel });
    this.initialized = true;
    // Expose worker globally for direct Prover access
    (window as any).__worker = worker;
  }

  async requestProxy(targetUrl: string): Promise<ProxyRequestResponse> {
    if (!this.config.rpcUrl) {
      throw new Error('No RPC URL configured for dynamic proxy requests.');
    }

    const response = await fetch(this.config.rpcUrl, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        method: 'nodeCall',
        params: [{ message: 'requestTLSNproxy', data: { targetUrl } }],
      }),
    });

    const result = await response.json();
    if (result.result !== 200) {
      throw new Error(`Failed to request proxy: ${result.response?.message || 'Unknown error'}`);
    }
    return result.response;
  }

  async verify(presentationJSON: any): Promise<VerificationResult> {
    if (!this.initialized) {
      throw new Error('TLSNotary not initialized. Call initialize() first.');
    }

    const proof = await new worker.Presentation(presentationJSON.data);
    const verifierOutput = await proof.verify();

    const transcript = new Transcript({
      sent: verifierOutput.transcript?.sent || [],
      recv: verifierOutput.transcript?.recv || [],
    });

    return {
      time: verifierOutput.connection_info.time,
      serverName: verifierOutput.server_name || 'Unknown',
      sent: transcript.sent(),
      recv: transcript.recv(),
      notaryKey: 'N/A',
      verifyingKey: 'N/A',
    };
  }

  isInitialized(): boolean {
    return this.initialized;
  }

  getConfig(): TLSNotaryConfig {
    return { ...this.config };
  }
}
```

### src/app.tsx

The main React application with full TLSNotary integration:

```tsx theme={null}
import React, { useState, useCallback, useEffect, useRef } from 'react';
import { createRoot } from 'react-dom/client';
import { Demos } from '@kynesyslabs/demosdk/websdk';
import { TLSNotaryService } from '@kynesyslabs/demosdk/tlsnotary/service';
import { TLSNotaryClient } from './TLSNotaryClient';

// Global instances
let tlsnClient: TLSNotaryClient | null = null;
let demosInstance: Demos | null = null;
let tlsnService: TLSNotaryService | null = null;

function App() {
  // State
  const [logs, setLogs] = useState<Array<{ msg: string; type: string }>>([]);
  const [wasmReady, setWasmReady] = useState(false);
  const [walletConnected, setWalletConnected] = useState(false);
  const [isLoading, setIsLoading] = useState(false);
  const [tokenId, setTokenId] = useState('');
  const [wsProxyUrl, setWsProxyUrl] = useState('');
  const [result, setResult] = useState<any>(null);

  // Configuration
  const [notaryUrl, setNotaryUrl] = useState('http://localhost:7047');
  const [rpcUrl, setRpcUrl] = useState('http://localhost:53550');
  const [mnemonic, setMnemonic] = useState('');
  const [targetUrl, setTargetUrl] = useState('https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=usd');
  const [method, setMethod] = useState('GET');
  const [maxRecv, setMaxRecv] = useState(4096);

  const logsEndRef = useRef<HTMLDivElement>(null);

  // Logger
  const log = useCallback((msg: string, type: string = 'info') => {
    const timestamp = new Date().toLocaleTimeString();
    setLogs(prev => [...prev, { msg: `[${timestamp}] ${msg}`, type }]);
  }, []);

  // Auto-scroll logs
  useEffect(() => {
    logsEndRef.current?.scrollIntoView({ behavior: 'smooth' });
  }, [logs]);

  // Initialize WASM on mount
  useEffect(() => {
    const init = async () => {
      try {
        log('Initializing TLSNotary WASM...', 'info');
        tlsnClient = new TLSNotaryClient({ notaryUrl, rpcUrl });
        await tlsnClient.initialize();
        setWasmReady(true);
        log('WASM initialized successfully!', 'success');
      } catch (error: any) {
        log(`WASM init failed: ${error.message}`, 'error');
      }
    };
    init();
  }, []);

  // Connect wallet
  const handleConnectWallet = useCallback(async () => {
    if (!mnemonic.trim()) {
      log('Please enter a mnemonic', 'error');
      return;
    }

    setIsLoading(true);
    try {
      log('Connecting to Demos Network...', 'info');
      demosInstance = new Demos();
      await demosInstance.connect(rpcUrl);
      log('Connected to node', 'success');

      // For development: direct mnemonic
      await demosInstance.connectWallet(mnemonic.trim());

      // For production: use Wallet Extension instead
      // await demosInstance.connectWalletExtension();

      tlsnService = new TLSNotaryService(demosInstance);
      setWalletConnected(true);
      log('Wallet connected!', 'success');
    } catch (error: any) {
      log(`Wallet connection failed: ${error.message}`, 'error');
    } finally {
      setIsLoading(false);
    }
  }, [mnemonic, rpcUrl, log]);

  // Request attestation token
  const handleRequestToken = useCallback(async () => {
    if (!tlsnService) {
      log('Please connect wallet first', 'error');
      return;
    }

    setIsLoading(true);
    try {
      log('Requesting attestation token (burns 1 DEM)...', 'info');

      const response = await tlsnService.requestAttestation({
        targetUrl: targetUrl,
      });

      setTokenId(response.tokenId);
      setWsProxyUrl(response.proxyUrl);

      log(`Token ID: ${response.tokenId}`, 'success');
      log(`Proxy URL: ${response.proxyUrl}`, 'info');
      log(`Expires: ${new Date(response.expiresAt).toLocaleTimeString()}`, 'info');
    } catch (error: any) {
      log(`Token request failed: ${error.message}`, 'error');
    } finally {
      setIsLoading(false);
    }
  }, [targetUrl, log]);

  // Perform attestation
  const handleAttest = useCallback(async () => {
    if (!wsProxyUrl) {
      log('Please request a token first', 'error');
      return;
    }

    setIsLoading(true);
    try {
      log('Starting TLS attestation...', 'info');

      const notarizeConfig = {
        notaryUrl: notaryUrl,
        websocketProxyUrl: wsProxyUrl,
        maxSentData: 16384,
        maxRecvData: maxRecv,
        url: targetUrl,
        method: method as 'GET' | 'POST',
        headers: { Accept: 'application/json' },
        commit: {
          sent: [{ start: 0, end: 100 }],
          recv: [{ start: 0, end: Math.min(200, maxRecv) }],
        },
        serverIdentity: true,
      };

      // Use the WASM worker directly
      const presentationJSON = await (window as any).__worker.Prover.notarize(notarizeConfig);
      log('Attestation complete!', 'success');

      // Verify locally
      log('Verifying proof...', 'info');
      const verification = await tlsnClient?.verify(presentationJSON);
      log('Verification passed!', 'success');

      setResult({ presentation: presentationJSON, verification });
    } catch (error: any) {
      log(`Attestation failed: ${error.message}`, 'error');
    } finally {
      setIsLoading(false);
    }
  }, [wsProxyUrl, targetUrl, notaryUrl, method, maxRecv, log]);

  // Full attestation (request token + attest in one click)
  const handleFullAttestation = useCallback(async () => {
    if (!tlsnService) {
      log('Please connect wallet first', 'error');
      return;
    }

    setIsLoading(true);
    try {
      // Step 1: Request token
      log('Requesting attestation token...', 'info');
      const tokenResponse = await tlsnService.requestAttestation({
        targetUrl: targetUrl,
      });

      setTokenId(tokenResponse.tokenId);
      setWsProxyUrl(tokenResponse.proxyUrl);
      log(`Token received: ${tokenResponse.tokenId}`, 'success');

      // Step 2: Perform attestation
      log('Starting TLS attestation...', 'info');
      const notarizeConfig = {
        notaryUrl: notaryUrl,
        websocketProxyUrl: tokenResponse.proxyUrl,
        maxSentData: 16384,
        maxRecvData: maxRecv,
        url: targetUrl,
        method: method as 'GET' | 'POST',
        headers: { Accept: 'application/json' },
        commit: {
          sent: [{ start: 0, end: 100 }],
          recv: [{ start: 0, end: Math.min(200, maxRecv) }],
        },
        serverIdentity: true,
      };

      const presentationJSON = await (window as any).__worker.Prover.notarize(notarizeConfig);
      log('Attestation complete!', 'success');

      // Step 3: Verify
      const verification = await tlsnClient?.verify(presentationJSON);
      log('Verification passed!', 'success');

      setResult({ presentation: presentationJSON, verification });
    } catch (error: any) {
      log(`Full attestation failed: ${error.message}`, 'error');
    } finally {
      setIsLoading(false);
    }
  }, [targetUrl, notaryUrl, method, maxRecv, log]);

  // Store proof on-chain
  const handleStoreProof = useCallback(async (storageType: 'onchain' | 'ipfs') => {
    if (!result || !tlsnService || !tokenId) {
      log('Missing result, service, or token ID', 'error');
      return;
    }

    setIsLoading(true);
    try {
      const proofString = typeof result.presentation === 'string'
        ? result.presentation
        : JSON.stringify(result.presentation);

      const proofSizeKB = Math.ceil(proofString.length / 1024);
      const estimatedFee = tlsnService.calculateStorageFee(proofSizeKB);

      log(`Storing proof (${proofSizeKB} KB, ~${estimatedFee} DEM fee)...`, 'info');

      const storeResult = await tlsnService.storeProof(
        tokenId,
        proofString,
        { storage: storageType }
      );

      log(`Proof stored on ${storageType}!`, 'success');
      log(`Transaction: ${storeResult.txHash}`, 'info');
      log(`Fee: ${storeResult.storageFee} DEM`, 'info');
    } catch (error: any) {
      log(`Store failed: ${error.message}`, 'error');
    } finally {
      setIsLoading(false);
    }
  }, [result, tokenId, log]);

  return (
    <div>
      <h1>TLSNotary Attestation App</h1>

      {/* Configuration */}
      <div className="card">
        <h3>Configuration</h3>
        <div>
          <label>Notary URL:</label>
          <input
            type="text"
            value={notaryUrl}
            onChange={(e) => setNotaryUrl(e.target.value)}
            style={{ width: '100%' }}
          />
        </div>
        <div>
          <label>RPC URL:</label>
          <input
            type="text"
            value={rpcUrl}
            onChange={(e) => setRpcUrl(e.target.value)}
            style={{ width: '100%' }}
          />
        </div>
        <p>WASM Status: {wasmReady ? '✅ Ready' : '⏳ Loading...'}</p>
      </div>

      {/* Wallet Connection */}
      <div className="card">
        <h3>Wallet Connection</h3>
        <p style={{ fontSize: '12px', color: '#666' }}>
          For production apps, use the Demos Wallet Extension instead of entering a mnemonic directly.
        </p>
        <textarea
          placeholder="Enter your mnemonic (for testing only)"
          value={mnemonic}
          onChange={(e) => setMnemonic(e.target.value)}
          rows={2}
          style={{ width: '100%' }}
        />
        <button
          onClick={handleConnectWallet}
          disabled={!wasmReady || isLoading || walletConnected}
        >
          {walletConnected ? '✅ Wallet Connected' : 'Connect Wallet'}
        </button>
      </div>

      {/* Attestation */}
      <div className="card">
        <h3>Attestation</h3>
        <div>
          <label>Target URL:</label>
          <input
            type="text"
            value={targetUrl}
            onChange={(e) => setTargetUrl(e.target.value)}
            style={{ width: '100%' }}
          />
        </div>
        <div style={{ display: 'flex', gap: '10px', marginTop: '10px' }}>
          <select value={method} onChange={(e) => setMethod(e.target.value)}>
            <option value="GET">GET</option>
            <option value="POST">POST</option>
          </select>
          <input
            type="number"
            value={maxRecv}
            onChange={(e) => setMaxRecv(Number(e.target.value))}
            placeholder="Max receive bytes"
            style={{ width: '150px' }}
          />
        </div>
        <div style={{ marginTop: '10px' }}>
          <button
            onClick={handleFullAttestation}
            disabled={!walletConnected || isLoading}
            style={{ marginRight: '10px' }}
          >
            {isLoading ? 'Processing...' : 'Full Attestation'}
          </button>
          <button
            onClick={handleRequestToken}
            disabled={!walletConnected || isLoading}
            style={{ marginRight: '10px', background: '#6366F1' }}
          >
            1. Request Token
          </button>
          <button
            onClick={handleAttest}
            disabled={!wsProxyUrl || isLoading}
            style={{ background: '#10B981' }}
          >
            2. Attest
          </button>
        </div>
        {tokenId && <p>Token ID: <code>{tokenId}</code></p>}
      </div>

      {/* Result */}
      {result && (
        <div className="card">
          <h3>Attestation Result</h3>
          <p>✅ Proof generated and verified!</p>
          <p>Sent data: {result.verification?.sent?.substring(0, 100)}...</p>
          <p>Received data: {result.verification?.recv?.substring(0, 100)}...</p>
          <div style={{ marginTop: '10px' }}>
            <button
              onClick={() => handleStoreProof('onchain')}
              disabled={isLoading}
              style={{ marginRight: '10px', background: '#F59E0B' }}
            >
              Store On-Chain
            </button>
            <button
              onClick={() => handleStoreProof('ipfs')}
              disabled={isLoading}
              style={{ background: '#8B5CF6' }}
            >
              Store on IPFS
            </button>
          </div>
        </div>
      )}

      {/* Logs */}
      <div className="card">
        <h3>Logs</h3>
        <div className="log">
          {logs.map((l, i) => (
            <div key={i} className={l.type}>{l.msg}</div>
          ))}
          <div ref={logsEndRef} />
        </div>
        <button onClick={() => setLogs([])} style={{ marginTop: '10px', background: '#666' }}>
          Clear Logs
        </button>
      </div>
    </div>
  );
}

// Render
const root = createRoot(document.getElementById('root')!);
root.render(<App />);
```

## Running the Application

```bash theme={null}
bun run dev
```

Open [http://localhost:3000](http://localhost:3000) in your browser.

## Usage Flow

<Steps>
  <Step title="Initialize WASM">
    The app automatically initializes the TLSNotary WASM module on load.
  </Step>

  <Step title="Connect Wallet">
    Enter your mnemonic and click "Connect Wallet" to connect to the Demos Network.
  </Step>

  <Step title="Configure Target">
    Enter the HTTPS URL you want to attest. Only HTTPS URLs are supported.
  </Step>

  <Step title="Perform Attestation">
    Click "Full Attestation" to request a token and perform the attestation in one step.
    This burns 1 DEM for the token request.
  </Step>

  <Step title="Store Proof (Optional)">
    After attestation, click "Store On-Chain" or "Store on IPFS" to persist the proof.
    This burns 1 DEM base + 1 DEM per KB of proof data.
  </Step>
</Steps>

## Production Considerations

### Wallet Extension Integration

For production applications, use the Demos Wallet Extension instead of direct mnemonic input. The wallet extension handles the 3-step transaction flow:

1. **Generate Transaction** - SDK creates the transaction
2. **Sign & Confirm** - User approves in wallet extension
3. **Broadcast** - Wallet sends signed transaction to network

```typescript theme={null}
// Connect via wallet extension (instead of mnemonic)
await demosInstance.connectWalletExtension();

// Use WithConfirmation methods for wallet extension apps
const { tlsn, tokenId } = await tlsnService.createTLSNotaryWithConfirmation(
  { targetUrl },
  {
    onConfirm: async (details) => {
      // In production: wallet extension shows approval UI
      // details.transaction contains the unsigned tx
      // details.amount shows fee (1 DEM for attestation)
      return await walletExtension.requestApproval(details.transaction);
    }
  }
);

// Perform attestation (no blockchain transaction needed)
const result = await tlsn.attest({ url: targetUrl });

// Store proof with user confirmation
await tlsnService.storeProofWithConfirmation(
  tokenId,
  JSON.stringify(result.presentation),
  { storage: 'onchain' },
  {
    onConfirm: async (details) => {
      // details.amount shows storage fee (1 + size_kb DEM)
      return await walletExtension.requestApproval(details.transaction);
    }
  }
);
```

The Wallet Extension provides better security as the mnemonic never leaves the extension.

### Cross-Origin Isolation in Production

For production deployments, configure your web server to send the required headers:

<Tabs>
  <Tab title="nginx">
    ```nginx theme={null}
    add_header Cross-Origin-Embedder-Policy "require-corp" always;
    add_header Cross-Origin-Opener-Policy "same-origin" always;
    ```
  </Tab>

  <Tab title="Express.js">
    ```javascript theme={null}
    app.use((req, res, next) => {
      res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp');
      res.setHeader('Cross-Origin-Opener-Policy', 'same-origin');
      next();
    });
    ```
  </Tab>

  <Tab title="Apache">
    ```apache theme={null}
    Header set Cross-Origin-Embedder-Policy "require-corp"
    Header set Cross-Origin-Opener-Policy "same-origin"
    ```
  </Tab>
</Tabs>

### Commit Ranges

The `commit` field in the notarization config specifies which parts of the request/response to include in the proof:

```typescript theme={null}
commit: {
  sent: [{ start: 0, end: 100 }],  // Reveal first 100 bytes of request
  recv: [{ start: 0, end: 200 }],  // Reveal first 200 bytes of response
}
```

<Warning>
  Larger ranges result in larger proofs, which increase storage costs. Only include the data you need to prove.
</Warning>

### Error Handling

Always implement proper error handling for production:

```typescript theme={null}
try {
  const response = await tlsnService.requestAttestation({ targetUrl });
} catch (error: any) {
  if (error.message.includes('HTTPS')) {
    // URL must be HTTPS
  } else if (error.message.includes('Token not created')) {
    // Token creation timed out - may need to retry
  } else if (error.message.includes('balance')) {
    // Insufficient DEM balance
  }
}
```

## Fee Summary

| Operation                 | Cost          |
| ------------------------- | ------------- |
| Request attestation token | 1 DEM         |
| Store proof (base fee)    | 1 DEM         |
| Store proof (per KB)      | +1 DEM per KB |

**Example**: Attesting and storing a 5KB proof costs:

* Token request: 1 DEM
* Storage: 1 (base) + 5 (size) = 6 DEM
* **Total: 7 DEM** + gas fees

## Next Steps

* Learn about [selective redaction](/sdk/web2/tlsnotary/api-reference#selective-redaction) to hide sensitive data in proofs
* Explore [use cases](/sdk/web2/tlsnotary/use-cases) for TLSNotary attestations
* Review the full [API reference](/sdk/web2/tlsnotary/api-reference)
