I needed a WhatsApp message listener. Something that would boot up, scan a QR code, and start logging incoming messages—text, images, reactions, the works.
Simple enough requirement. The implementation journey turned out to be a fascinating case study in what Level 3 AI-assisted development actually looks like when the documentation doesn’t exist.
The Setup
WhatsApp doesn’t have an official API for personal accounts. If you want to programmatically interact with WhatsApp, you need to reverse-engineer the protocol.
Fortunately, some very talented people have already done this. The stack:
- whatsmeow: A Go library that implements the WhatsApp Web protocol. Essentially a clean-room reverse engineering of the Signal protocol that WhatsApp uses for E2E encryption.
- neonize: A Python wrapper around whatsmeow, maintained by a developer somewhere in rural Indonesia.
The architecture looks like this:
WhatsApp servers (WebSocket + protobuf)
↓
whatsmeow (Go library)
↓
neonize (Python bindings via ctypes)
↓
my logger.py
↓
messages.json + media/
Two layers of abstraction over a reverse-engineered protocol. What could go wrong?
The Documentation Problem
Here’s what the neonize README showed for getting started:
client = NewClient("bot", database="./neonize.db")
@client.event
def on_message(client, event):
print(event)
Clean. Simple. Doesn’t work.
The database parameter doesn’t exist—the function signature is NewClient(name, jid=None, props=None, uuid=None). The database is auto-created with the name you provide.
The @client.event decorator doesn’t work that way either. It needs the event type passed to it: @client.event(MessageEv).
And accessing message fields? The examples showed event.info.message_source.sender. The actual API uses PascalCase: event.Info.MessageSource.Sender.
The documentation wasn’t wrong in a “this is outdated” way. It was wrong in a “this was never tested” way. Skeleton examples that looked plausible but didn’t run.
What Claude Did
Here’s where it gets interesting.
I gave Claude the task: build a WhatsApp message logger using neonize. Initial attempts failed with the expected errors—AttributeError on fields that didn’t exist, decorator syntax that didn’t match the actual implementation.
Then Claude did something I didn’t expect: it started reading the source.
Not the documentation. The actual Python and protobuf files that define how neonize works.
from neonize.proto.Neonize_pb2 import MessageInfo
print([f.name for f in MessageInfo.DESCRIPTOR.fields])
# Output: ['ID', 'MessageSource', 'Type', 'Pushname', 'Timestamp', ...]
The protobuf descriptor tells you exactly what fields exist and what they’re called. Pushname, not PushName. The casing comes from whatsmeow’s Go implementation, which came from reverse-engineering WhatsApp’s actual protocol.
Claude traced through the code paths:
- Found that
MessageSourceis PascalCase - Discovered that
imageMessage.URLis uppercase butimageMessage.captionis lowercase - Identified that media detection requires checking the
.URLfield (not just whether the message object exists) - Figured out the event decorator syntax by reading the actual decorator implementation
This is the part that surprised me. I’d expected to spend hours in trial-and-error, reading source code, figuring out the idiosyncrasies. Instead, I watched Claude do that exploration systematically and come back with working code.
The Idiosyncrasies
What Claude discovered through source exploration:
Inconsistent field casing (inherited from WhatsApp’s protobuf definitions):
| What you’d expect | What it actually is |
|---|---|
PushName | Pushname |
url | URL |
fileName | fileName |
IsGroup | IsGroup |
No pattern. The casing comes from whatever the WhatsApp engineers chose when defining their internal protocol. The reverse-engineering preserved it faithfully.
Event decorator syntax:
# Doesn't work
@client.event
def on_message(client, event):
# Works
@client.event(MessageEv)
def on_message(client, message):
Client initialization:
# Documented
client = NewClient("name", database="./session.db")
# Actual
client = NewClient("name") # database auto-created as name.db
Signal handling:
The Go runtime that underlies whatsmeow intercepts signals. Ctrl+C doesn’t stop the script. You need pkill -f "python logger.py". The library prints “Press Ctrl+C to exit” but that’s from internal code you can’t change—it’s misleading.
Each of these would have been 15-30 minutes of debugging for me to figure out. Claude identified them by reading source code, then documented them so I wouldn’t hit them again.
The Working System
Three sessions of iteration produced:
logger.py - The core listener:
- Connects to WhatsApp via QR code scan
- Logs all incoming messages to JSONL format
- Downloads and saves media files
- Handles text, images, videos, audio, documents, stickers
- Captures reactions and reply context
- Watches an outbox folder for outgoing messages to send
parser_v1.py - Organises the logs:
- Parses messages.json into per-chat markdown files
- Resolves reactions to the messages they reference
- Resolves replies to include quoted text
- Archives processed messages
sender.py - Outgoing messages:
- Queues messages via JSON files in outbox/
- Allows Claude to send WhatsApp messages by writing to the outbox
- Archives sent messages for logging
The insight that made the outbox pattern work: WhatsApp only allows one connection per session. You can’t run a separate sender process. So the logger watches a folder and sends anything that appears there.
What This Demonstrates
This project is a good example of Level 3 AI-assisted development in practice.
The human role: I defined the requirement (WhatsApp listener), made architectural decisions (JSONL format, outbox pattern), and reviewed outputs. I didn’t write most of the code line by line.
The AI role: Claude explored undocumented APIs, traced through source code, identified idiosyncrasies, and produced working implementations. It did the kind of “figure out how this library actually works” exploration that normally takes hours of developer time.
What made it work:
-
Requirements clarity. “Build a WhatsApp listener that logs messages and media” is clear enough to execute against.
-
Iteration loop. Initial attempts failed. We debugged, learned, adjusted. The AI doesn’t get everything right first try—but it fails in informative ways.
-
Source code access. Claude could read the actual implementation files. This is the AI equivalent of “use the source, Luke.”
-
Human judgment. When Claude discovered the outbox pattern was necessary (only one connection per session), I decided that was the right solution. The AI proposed; I validated.
The Meta-Observation
Here’s what struck me about this project: AI can navigate poorly-documented code better than humans in some cases.
When I hit an AttributeError, my instinct is to Google the error, search Stack Overflow, maybe open an issue on GitHub. That works for popular libraries with active communities.
For neonize—a Python wrapper maintained by one person, around a Go library that reverse-engineers a proprietary protocol—there’s no Stack Overflow answer. The community is maybe a hundred people.
Claude’s approach was different: just read the source. Trace the code paths. Figure out what the functions actually do, regardless of what the documentation says.
This is something humans can do but often don’t. We’re trained to look for documentation, examples, community answers. When those don’t exist, we get stuck.
AI doesn’t have that training. It just… reads the code. And for undocumented libraries, that turns out to be remarkably effective.
Output Artifacts
The project now produces:
neonize-whatsapp/
├── logger.py # Main listener (runs 24/7)
├── sender.py # CLI for queueing outgoing messages
├── parser_v1.py # Parses logs into chat files
├── messages.json # JSONL message log
├── media/ # Downloaded media files
├── parsed_chats/ # Organised chat markdown files
├── outbox/ # Queued outgoing messages
├── outbox/sent/ # Archive of sent messages
├── whatsapp-logger # SQLite session database
├── README.md # Setup and usage docs
├── NEONIZE_PR_NOTES.md # Improvement proposals for upstream
└── CLAUDE.md # AI context persistence
The README and NEONIZE_PR_NOTES are particularly useful. Claude documented all the idiosyncrasies we discovered, both for my future reference and as potential contributions back to the library.
Lessons for Level 3 Development
-
Undocumented != unusable. AI can figure out libraries by reading source code, even when documentation is poor or wrong.
-
Iteration is normal. First attempts fail. The value is in how quickly you can debug, learn, and adjust.
-
Human judgment stays essential. The outbox pattern, the JSONL format, the decision to document idiosyncrasies for upstream—these were human decisions based on understanding the context.
-
Documentation is a byproduct. The README and notes we produced during development are now assets. Level 3 development naturally produces documentation because you’re articulating requirements and capturing decisions.
-
Niche tools work. You’re not limited to popular, well-documented libraries. AI can figure out obscure packages that would have been too time-consuming for manual exploration.
What’s Next
The WhatsApp listener is now running, feeding messages into my personal knowledge system. Next step: using the AI agents to actually respond to some of those messages.
The same pattern—AI exploring undocumented code, human providing judgment and direction—applies there too.
This is Part 5 of a series on AI-assisted software development. Previously: You Don’t Need Kubernetes. Next: Why Every AI System Needs a Consigliere.