If you have ever used a blockchain dApp on a desktop browser, you have probably seen a QR code that you scan with your phone wallet to connect. That interaction is powered by WalletConnect, an open protocol that lets web applications communicate with crypto wallets running on entirely different devices. After integrating WalletConnect v2 into a Chia blockchain game, I have a much deeper appreciation for how the protocol works and why certain design decisions were made.

The Core Problem

Blockchain transactions require private key signatures. Your private keys live in your wallet app (on your phone, a hardware device, or a browser extension). A dApp running in a browser tab needs to request those signatures without ever touching the private keys directly. This separation is fundamental to security — the dApp should never have access to your keys.

Browser extension wallets like MetaMask solve this by injecting a JavaScript provider directly into the page. But what about wallets that run on a completely different device? A Chia wallet on your phone cannot inject JavaScript into a Chrome tab on your laptop. WalletConnect bridges this gap using a relay server as an intermediary.

How the Relay Protocol Works

WalletConnect v2 uses a publish-subscribe messaging pattern over WebSocket connections. Both the dApp and the wallet maintain persistent WebSocket connections to a relay server (typically wss://relay.walletconnect.com). Neither connects directly to the other. The flow works like this:

  1. The dApp generates a pairing URI containing a unique topic (a random 32-byte hex string) and a symmetric encryption key. This URI is encoded into a QR code.
  2. The wallet scans the QR code, extracts the topic and key, and subscribes to that topic on the relay server.
  3. Both sides exchange encrypted messages on the shared topic. The relay server stores and forwards messages but cannot read them because they are encrypted with the symmetric key that was shared out-of-band via the QR code.
  4. After pairing, a session is established with a new topic and new encryption keys, derived through a key agreement protocol.

The critical insight is that the relay server is a dumb pipe. It stores messages temporarily and forwards them to subscribers, but it cannot decrypt any content. Security comes from the symmetric key being shared through the QR code — an out-of-band channel that the relay never sees.

Session Namespaces and Capabilities

When a dApp initiates a connection, it specifies which blockchain methods it needs access to through a namespaces object. This tells the wallet exactly what the dApp wants to do:

var connectResult = await signClient.connect({
    requiredNamespaces: {
        chia: {
            methods: [
                "chia_getNfts",
                "chia_getAddress",
                "chia_signMessageByAddress"
            ],
            chains: ["chia:mainnet"],
            events: []
        }
    }
});

The wallet displays these requested capabilities to the user, who can approve or reject the connection. This is similar to OAuth scopes — the dApp declares what it needs, and the user grants or denies permission. Once approved, the session object records which methods are allowed, and the wallet will reject any requests for methods that were not approved.

JSON-RPC Over the Relay

All communication between the dApp and wallet uses JSON-RPC 2.0. The dApp sends a request with a method name and parameters; the wallet processes it and sends back a response. For example, fetching NFTs from a Chia wallet looks like this:

var result = await signClient.request({
    topic: session.topic,
    chainId: "chia:mainnet",
    request: {
        method: "chia_getNfts",
        params: { limit: 500, offset: 0 }
    }
});
// result.nfts is an array of NFT objects

This request gets encrypted, published to the session topic on the relay, forwarded to the wallet, decrypted, processed, and the response travels back the same way. The entire round trip typically takes 1-3 seconds depending on network conditions and wallet responsiveness.

Session Persistence and Auto-Reconnection

WalletConnect sessions persist across page reloads. The SignClient stores session data in localStorage, and on the next page load, the dApp can check for existing sessions and restore them without requiring the user to scan a new QR code:

// Check for a saved session topic
var savedTopic = localStorage.getItem("wc_session_topic");
if (savedTopic) {
    var sessions = signClient.session.getAll();
    for (var i = 0; i < sessions.length; i++) {
        if (sessions[i].topic === savedTopic) {
            // Session restored - no QR scan needed
            activeSession = sessions[i];
            break;
        }
    }
}

This is important for user experience. Nobody wants to scan a QR code every time they reload a page. However, session restoration is not the same as re-fetching data. The session lets you make new requests to the wallet, but any data you previously fetched (NFT lists, balances, addresses) needs to be cached separately if you want instant page loads.

The Two-Tier Wallet Compatibility Problem

One of the biggest practical challenges with WalletConnect is that different wallet implementations support different methods. The Chia ecosystem has wallets like Sage and the reference Chia wallet, and they expose different APIs through WalletConnect:

A robust dApp must handle both APIs. The pattern is to try the simpler API first, and if it fails or returns empty results, fall back to the more complex one:

var nfts = [];
try {
    // Try Sage-style API first
    var result = await request("chia_getNfts", { limit: 500 });
    nfts = result.nfts;
} catch (err) {
    // Fall back to reference wallet API
    var wallets = await request("chia_getWallets");
    var nftWalletIds = wallets.filter(function(w) {
        return w.type === 10; // NFT wallet type
    });
    for (var i = 0; i < nftWalletIds.length; i++) {
        var batch = await request("chia_getNFTs", {
            walletIds: [nftWalletIds[i].id]
        });
        nfts = nfts.concat(batch);
    }
}

This kind of defensive coding is essential when building on a protocol where the other side of the connection is software you do not control.

Common Pitfalls

Several issues came up repeatedly during integration:

Security Considerations

WalletConnect's security model is reasonably strong for its use case:

The primary risk is phishing: a malicious dApp could request broad permissions and trick users into signing harmful transactions. This is a UX problem more than a protocol problem — wallets need to clearly display what the user is approving.

Is WalletConnect the Future?

WalletConnect solves a real problem elegantly. By using a relay and QR codes, it enables cross-device, cross-platform wallet connectivity without any browser extensions, native app bridges, or platform-specific code. The dApp is just a web page. The wallet is just an app. The relay is just a message broker.

For developers building blockchain-integrated web applications, understanding how WalletConnect works at the protocol level — not just copying SDK examples — helps you build more resilient integrations, handle edge cases gracefully, and debug the inevitable connectivity issues that arise when your application depends on real-time communication through a third-party relay.