Skip to main content

Integrating BlindHop with Smoldot Applications

BlindHop provides a transparent privacy layer for any application using smoldot, the Substrate light client. This guide shows how to add mixnet privacy to your dApp with minimal code changes.

Overview​

Without BlindHop:
Your dApp → smoldot → Direct WS → Full Node (sees your IP + queries)

With BlindHop (native):
Your dApp → smoldot → WS → blindhop-proxy → Nym Mixnet → Exit → Full Node

With BlindHop (browser):
Your dApp → BlindHop SDK → Nym Wasm → Nym Mixnet → Exit → Full Node

Option 1: Native Proxy (Simplest)​

The fastest way to add privacy — no code changes to your app.

How It Works​

The native blindhop-proxy acts as a transparent WebSocket proxy. You just change the smoldot endpoint from the full node to the local proxy:

// BEFORE — smoldot connects directly (IP exposed)
import * as smoldot from 'smoldot';

const client = smoldot.start();
const chain = await client.addChain({
chainSpec: kusamaChainSpec,
// Connects to wss://rpc.polkadot.io (your IP visible)
});

// AFTER — smoldot connects through BlindHop (IP hidden)
// 1. User runs: blindhop-proxy --privacy-mode full --exit-address <addr>
// 2. Change your app's RPC endpoint:
const chain = await client.addChain({
chainSpec: kusamaChainSpec,
potentialRelayChains: [],
// Override the default bootnodes to point at the local proxy
});

// Or connect via JSON-RPC over WebSocket:
const ws = new WebSocket('ws://127.0.0.1:9500'); // BlindHop proxy

For Desktop/CLI Apps​

# Start BlindHop proxy in the background
blindhop-proxy --privacy-mode full --exit-address <NYM_EXIT_ADDR> &

# Your app connects to ws://127.0.0.1:9500 instead of the chain directly

For Electron Apps​

Bundle blindhop-proxy as a sidecar binary and start it from your main process:

const { spawn } = require('child_process');

const proxy = spawn('./blindhop-proxy', [
'--privacy-mode', 'full',
'--exit-address', exitNymAddress,
'--listen', '127.0.0.1:9500',
]);

// Connect smoldot to the proxy
const ws = new WebSocket('ws://127.0.0.1:9500');

Option 2: Browser SDK (Zero Setup for Users)​

For web-based dApps where you want zero user setup — privacy works out of the box.

Architecture​

import { createBlindHopClient } from '@blindhop/browser';

// Initialize BlindHop with Nym Wasm client
const blindhop = await createBlindHopClient({
exitAddress: 'your-exit-service-nym-address',
privacyMode: 'full', // 'none' | 'full'
nymApiUrl: 'https://validator.nymtech.net/api',
});

// Send JSON-RPC requests through the mixnet
const response = await blindhop.sendRpc({
method: 'chain_getHeader',
params: [],
});

console.log('Latest block:', parseInt(response.result.number, 16));

// Clean up
await blindhop.disconnect();
note

The @blindhop/browser npm package is planned for Phase 2. The current MVP demo implements this pattern directly. See the demo source code for the working implementation.

Adding a Privacy Toggle to Your UI​

Give users control over their privacy level:

// React example
function PrivacyToggle({ blindhop }) {
const [mode, setMode] = useState('full');

const handleChange = async (newMode) => {
setMode(newMode);
await blindhop.setPrivacyMode(newMode);
};

return (
<div className="privacy-toggle">
<button
className={mode === 'none' ? 'active' : ''}
onClick={() => handleChange('none')}
>
🔴 Direct
</button>
<button
className={mode === 'full' ? 'active' : ''}
onClick={() => handleChange('full')}
>
🟢 Private
</button>
</div>
);
}

Option 3: Hybrid (Opt-in Local Proxy)​

The BlindHop demo uses the browser Nym client by default and connects to a local proxy only when the user explicitly opts in:

async function initPrivacy({ useLocalProxy }) {
if (useLocalProxy) {
// Only on explicit opt-in. The proxy must be started with
// --allowed-origin <your-site>, or it refuses the connection.
const ws = new WebSocket('ws://127.0.0.1:9500');
await waitForOpen(ws, 2000); // on failure, report it; don't silently switch
return { mode: 'native', transport: ws };
}
const { NymBrowserClient } = await import('./nym-client.js');
const nym = new NymBrowserClient();
await nym.connect(EXIT_NYM_ADDRESS); // on failure, stop; never fall back to direct
return { mode: 'browser', transport: nym };
}

:::warning Don't auto-probe localhost Silently probing ws://127.0.0.1:9500 from a public page lets any site detect a running proxy, and browsers may show a local-network permission prompt. Likewise, never fall back to a direct RPC connection when the Nym client fails: that exposes the user's IP without their consent. :::

This gives users the best of both worlds:

  • Power users who run the native proxy get full 3-mode support
  • Casual users get browser-based privacy (None/Full) with no setup

What Gets Protected​

DataWithout BlindHopWith BlindHop (Full)
Your IP addressVisible to full nodeHidden (Nym exit IP shown)
Which chain you queryVisibleHidden
Query timing patternsVisibleObscured by cover traffic
Transaction submissionIP linked to extrinsicIP hidden
Storage key readsVisible (reveals interests)Hidden

Limitations​

  • Latency: Full mode round-trips take ~2 s (p50; p90 ~3 s), Fast mode ~1.4–1.8 s
  • Throughput: Not suitable for high-frequency trading or sub-second queries
  • Subscriptions: Not supported — the exit forwards single request/response calls only and refuses subscription methods; poll instead (e.g. chain_getHeader)
  • Exit trust: The exit service sees your queries (but not your IP). Run your own exit for maximum privacy

Deploying Your Own Exit Service​

For full control, run your own blindhop-exit:

# On a server with good connectivity
cargo build -p blindhop-exit --release
./target/release/blindhop-exit --target-rpc wss://rpc.polkadot.io

# Copy the printed Nym address and use it in your app

See the deployment guide for production setup with systemd.