WalletConnect v2 relay WebSocket stalls and ping timeouts in headless AI agents occur because NodeJS environments lack native browser WebSocket heartbeat listeners, causing TCP connections through cloud NATs and load balancers to silently drop after 30 seconds of inactivity. When the client fails to respond to relay ping frames, the server terminates the socket without firing an error. Implementing an explicit 15-second heartbeat keepalive interval resolves the drop.
Before executing state changes or signing transactions, verify your cryptographic parameters using our interactive ERC-4337 Calldata Decoder utility.
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.
[!TIP] Diagnostic Verification & Air-Gapped Testing: Before executing state changes or broad recovery steps, cross-reference failure parameters with our companion guide on WalletConnect Crypto.decode Silent Decryption Failure and verify transaction calldata or cryptographic envelopes using the Master Web3 & RPC Error Code Directory.