It started with 404 errors. I have been suffering from this a lot during the past years from China Gov, almost all of my accounts on social or content platforms were banned. The craziest time was the COVID-19 period, all the articles, videos, and accounts were gone without explanation...
That was the last straw. I decided to migrate to Nostr (a decentralized, censorship-resistant protocol). But as a developer based in a "restrictive network environment" (if you know, you know), I hit two massive walls immediately:
The Access Wall: Direct WebSocket (wss://) connections to global relays like Damus or Primal are constantly reset, throttled, or blocked by the firewall.
The Content Wall: Web3 is free, but empty. I missed the depth of my favorite Web2 RSS blogs, tech news, and YouTube channels.
So, I locked myself in a room for a month and coded EchoDeck. Here is how I engineered a "Dual-Core" client that bridges the legacy web with the sovereign web, bypassing firewalls along the way.
Challenge 1: The Tunnel (Accessing the Unreachable)
Nostr relies heavily on persistent WebSocket connections. In some regions, these are easily sniffed and killed. Using a local VPN works for somebody, but I wanted to build a tool that works for anyone, straight out of the box, without requiring them to be a networking expert.
The Architecture: I couldn't rely on the client connecting directly to Global Relays. Instead, I deployed a custom Nginx Reverse Proxy on an edge node in Hong Kong (a Cloud instance). You have to set the proxy in your nginx.conf file, can't display here though.
The flow looks like this: User (China) -> SSL Handshake -> My Hong Kong Node (wss://echodeck.io/relay) -> Global Relays
The "Switching" Logic: I didn't want everyone to use my expensive proxy bandwidth—only those who needed it. I wrote a client-side detector in Next.js that checks the user's timezone. If they are in the UTC+8 zone (China), the app automatically routes them through the tunnel. If they are in the US/EU, they connect directly to global relays for speed.
Here is the actual code from my relayConfig.ts:
// lib/relayConfig.ts
export const MAIN_RELAY = 'wss://echodeck.io/relay'; // My custom edge tunnel
export const getSmartRelays = (): string[] => {
// Server-side fallback
if (typeof window === 'undefined') return GLOBAL_LIST;
try {
// Detect China zone by checking timezone offset
// China is UTC+8, which means offset is -480 minutes
const timezoneOffset = new Date().getTimezoneOffset();
const isChinaTimezone = timezoneOffset === -480;
// Also check locale as a secondary indicator
const locale = navigator.language || '';
const isChinaLocale = locale.toLowerCase().includes('zh-cn');
// If user is behind the wall, force them through the Tunnel
if (isChinaTimezone || isChinaLocale) {
console.log('🛡️ Restricted Region Detected: Activating Tunnel Mode.');
return [MAIN_RELAY];
}
// Otherwise, use the decentralized global list
return GLOBAL_LIST;
} catch (error) {
return GLOBAL_LIST;
}
};
This simple logic ensures that a user in Beijing opens the app and sees content immediately, while a user in New York connects via the standard decentralized path.
Challenge 2: The Bridge (Fixing the "Empty Feed")
I didn't just want a social network; I wanted a reader. But RSS (XML) and Nostr (JSON Events) are like oil and water. Plus, Client-side RSS fetching is a nightmare due to CORS policies.
The Solution: I built a Server-Side Aggregator using Next.js API Routes. It acts as a "Transformer." It fetches the XML, cleans it, and maps it to a structure compatible with Nostr events.
The YouTube Problem: Standard RSS parsers fail on YouTube channels because YouTube is aggressive with rate limiting and dynamic rendering. I had to write a specific "Smart Extractor" to masquerade as a browser and regex-match Channel IDs when the standard feed discovery failed.
Here is a snippet from my route.ts showing the extraction strategy:
// app/api/rss/discover/route.ts
async function extractYoutubeChannel(url: string): Promise<string | null> {
try {
// 1. Masquerade as a browser to avoid 403s
const res = await fetch(url, {
headers: {
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)...'
},
signal: AbortSignal.timeout(8000)
});
const html = await res.text();
// 2. Strategy A: Regex match the Canonical URL
const channelMatch = html.match(/link\s+rel="canonical"\s+href="https:\/\/www\.youtube\.com\/channel\/(UC[\w-]{21}[AQgw])"/);
if (channelMatch && channelMatch[1]) {
return `https://echodeck.io/rsshub/youtube/channel/${channelMatch[1]}`;
}
// 3. Strategy B: Brute-force match the JSON blob
const jsonMatch = html.match(/"externalId":"(UC[\w-]{21}[AQgw])"/);
if (jsonMatch && jsonMatch[1]) {
return `https://echodeck.io/rsshub/youtube/channel/${jsonMatch[1]}`;
}
return null;
} catch (e) {
return null;
}
}
This allows EchoDeck users to paste any link—a blog, a YouTube channel, a Substack—and follow it just like a user profile.
The Killer Feature? Since we treat RSS items as Nostr events, we can attach Lightning Wallets to them. You can now Zap (tip via Bitcoin) a standard Web2 blog post directly from the app. We are literally streaming value back to the open web.
Conclusion
Building EchoDeck wasn't just about coding; it was about sovereignty.
Now, I have a unified timeline where a cryptographic note from a cypherpunk sits right next to a tech review from The Verge. No VPNs required. No takedowns possible.
I'm currently running a Closed Beta to fine-tune the server load on my Hong Kong node (bandwidth is expensive!).
If you are a developer who wants to test the "Tunnel" performance or just wants a cleaner Nostr experience: I’ve generated 10 Tier-0 Genesis Codes for the DEV community. Each code unlocks 10 invites.
ECHO-GEN-6JHTRU
ECHO-GEN-6JSZSE
ECHO-GEN-6TQWUK
ECHO-GEN-74FM7W
ECHO-GEN-7AGPAX
ECHO-GEN-85UBE4
ECHO-GEN-8VLEBJ
ECHO-GEN-9HL6HT
ECHO-GEN-9MJGJP
ECHO-GEN-9P38RK
(If these run out, check the comments or DM me.)
Welcome to EchoDeck
Let's break the walls. 🧱🔨

Top comments (0)