Fix: WalletConnect Relay WebSocket Stalls & Ping Timeouts in Headless AI Agents
Autonomous Web3 agents (built on frameworks like ElizaOS, Biconomy AI, or custom LangChain trading loops) frequently interact with wallets and dApps via WalletConnect v2. Unlike human users clicking wallet connect buttons in a browser, autonomous agents run continuously as headless Node.js processes on cloud servers (AWS EC2, Railway, Docker).
However, engineers operating autonomous agent swarms frequently notice that after 15 to 45 minutes of uptime, the agent stops responding to wallet signatures and RPC requests:
[WalletConnect] Connection to wss://relay.walletconnect.org/ stalled...
[RelayClient] WebSocket error: code 1006 (Abnormal Closure)
[AgentRuntime] Error: Pairing ping timeout after 30000ms. No response from remote peer.
[AgentRuntime] Fatal: Session topic 7b9a1c... dropped from relay registry.
If your agent is also experiencing silent decryption failures when reading RPC responses, see our companion diagnostic guide on Fixing WalletConnect Crypto.decode Silent Decryption Failures.
Technical Forensics: Why Headless WebSockets Die Silently
The WalletConnect v2 protocol relies on a persistent WebSocket connection to the decentralized Relay network (wss://relay.walletconnect.org). Messages are encrypted point-to-point and routed via published topics.
The Idle Timeout Trap in Serverless and Container Runtimes
In consumer browsers, background tabs still receive periodic microtask execution. But in containerized environments (Docker / Kubernetes):
- Cloud NAT Gateways: AWS NAT Gateways and Docker bridge networks aggressively drop idle TCP connections after 350 seconds of inactivity.
- Missing TCP Keepalives: Standard Node.js
wsclients without explicitkeepAlive: trueand application-level pings do not emit network packets when no transactions are actively occurring. - Ghost Sockets: The Node.js socket object remains in state
OPEN(readyState === 1), but the cloud proxy has already severed the connection. When the agent attempts to publish a transaction payload, the write buffer queues indefinitely until a timeout error surfaces 30 seconds later.
Solution 1: Active Heartbeat & Connection Health Monitor
To prevent NAT drops and detect zombie sockets within seconds, attach an explicit heartbeat monitor to the core.relayer instance:
// walletconnect-heartbeat.ts
import { Core } from '@walletconnect/core';
import type { ICore } from '@walletconnect/types';
export function attachRelayerHeartbeat(core: ICore, intervalMs: number = 20000) {
let isAlive = true;
// Listen to relay connection events
core.relayer.on('relayer_connect', () => {
console.log('[Relayer] WebSocket connected to relay.walletconnect.org');
isAlive = true;
});
core.relayer.on('relayer_disconnect', () => {
console.warn('[Relayer] Disconnected from relay network. Initializing retry...');
isAlive = false;
});
// Background ping interval
const pingTimer = setInterval(async () => {
if (!core.relayer.connected) {
console.log('[Heartbeat] Relayer disconnected. Forcing reconnect...');
try {
await core.relayer.restartTransport();
} catch (err) {
console.error('[Heartbeat] Failed to restart transport:', err);
}
return;
}
// Ping active relay server
try {
// Send lightweight heartbeat ping
const timeoutPromise = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Heartbeat timeout')), 5000)
);
await Promise.race([
core.relayer.provider.connection.send({
id: Date.now(),
jsonrpc: '2.0',
method: 'waku_ping', // or internal relay ping
params: {},
}),
timeoutPromise,
]);
isAlive = true;
} catch (error) {
console.error('[Heartbeat] Ping failed — ghost socket detected. Reconnecting...');
isAlive = false;
await core.relayer.restartTransport();
}
}, intervalMs);
return () => clearInterval(pingTimer);
}
Solution 2: Automated Session State Persistence in SQLite
When an agent restarts or recovers from a network drop, losing the in-memory pairing keys means the user must scan a fresh QR code.
Configure ElizaOS or your backend service to use a persistent key-value store rather than volatile memory:
// agent-walletconnect-init.ts
import { SignClient } from '@walletconnect/sign-client';
import { Core } from '@walletconnect/core';
export async function createPersistentAgentSignClient(storagePath: string) {
const core = new Core({
projectId: process.env.WALLETCONNECT_PROJECT_ID!,
// Ensure custom storage adapter points to SQLite / leveldb in Docker volumes:
storageOptions: {
database: storagePath, // e.g., '/data/walletconnect-store.json'
},
});
const signClient = await SignClient.init({
core,
metadata: {
name: 'Autonomous Security Sentinel Agent',
description: 'Headless Web3 Security and Settlement Daemon',
url: 'https://layerzerofault.site',
icons: ['https://layerzerofault.site/fallback-og.png'],
},
});
// Handle automatic session expiration pruning
signClient.on('session_expire', ({ topic }) => {
console.log(`[Session] Expired topic ${topic} pruned from store`);
});
return signClient;
}
Diagnostic Verification via CLI
Verify that your agent server maintains an active TCP connection to WalletConnect relay servers without dropping packets:
# 1. Test WebSocket latency and TLS handshake
curl -i -N -H "Connection: Upgrade" \
-H "Upgrade: websocket" \
-H "Host: relay.walletconnect.org" \
-H "Origin: https://layerzerofault.site" \
https://relay.walletconnect.org
# 2. Inspect active socket states in Linux / Docker:
netstat -tnp | grep 443 | grep ESTABLISHED
By enforcing an application-level ping cycle, handling abnormal 1006 terminations, and ensuring session keys are backed by persistent storage volumes, autonomous AI agents maintain resilient, 24/7 connectivity to decentralized wallet networks.