I never went to school for this. No bootcamp. No senior engineer mentorship. Just curiosity, late nights, and the stubborn belief that I could ship something people would actually pay for. Three months after launch, Pwnagotchi Companion hit $1,700+ CAD in revenue and 300+ paid downloads with a 5.0-star rating. Here's how I did it — and what I learned along the way.
The Idea: Solving a Real Problem
It started with Pwnagotchi — an open-source Wi-Fi auditing device built on Raspberry Pi. The community was growing, but the device itself had no official interface. Users had to SSH into a command line or jury-rig third-party tools just to see their handshake counts.
That was the problem: there was no user-friendly iOS app. No way to check your stats on the go, no remote control, no visual dashboard. Everyone was asking for one. So I decided to build it.
Architecture: SwiftUI + WebSocket + Bluetooth
I chose SwiftUI because it was the future of iOS development, and honestly, I wanted to learn it properly. That meant dealing with its state management quirks and ObservableObject proliferation, but the result was clean, declarative code that was easy to iterate on.
The app architecture broke into three main pieces:
- ▸ WebSocket client for real-time data sync with the Pwnagotchi's Python backend plugin
- ▸ Core Bluetooth to send commands directly to the Raspberry Pi Zero W (no internet required)
- ▸ SwiftData (previously CoreData) for local caching of stats and handshake history
The WebSocket layer was the trickiest. The Pwnagotchi's existing plugin was written in Python and spitting out JSON blobs every second. I needed to parse that in real-time, update the UI without blocking the main thread, and handle reconnections gracefully when the Pi lost WiFi.
// Simplified WebSocket manager
@MainActor
class WebSocketManager: ObservableObject {
@Published var handshakes: Int = 0
@Published var isConnected: Bool = false
private var webSocketTask: URLSessionWebSocketTask?
private let url: URL
func connect() {
webSocketTask = URLSession.shared.webSocketTask(with: url)
webSocketTask?.resume()
listen()
}
private func listen() {
webSocketTask?.receive { [weak self] result in
switch result {
case .success(.string(let json)):
self?.handleIncoming(json)
self?.listen() // Keep listening
case .success(.data):
self?.listen()
case .failure:
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
self?.connect() // Reconnect
}
}
}
}
}
That listen() recursion took me a while to get right — initial implementations kept dropping connections or piling up memory. The fix was making sure every receive call immediately schedules the next one.
The App Store Gauntlet
Submitting to the App Store was my first major stress test. I'd never done this before, and there's a lot of unwritten rules that don't show up in Apple's guidelines.
Here's what tripped me up:
- ◆ Cryptocurrency miners: The Pwnagotchi ecosystem overlaps with crypto. I had to explicitly state in metadata and binary that the app does not mine, harvest, or transact cryptocurrency.
- ◆ Hacking tools clause: Apple's 5.1.1 clause worries about "exactly accurate" hacking tool representations. I made sure the app was marketed as a companion for a device you already own, not a tool for unauthorized network access.
- ◆ Bluetooth permissions: Apple wants to know exactly what you're doing with Bluetooth. I added NSBluetoothAlwaysUsageDescription and explained the device control use case clearly.
It took two rejections and one expedited appeal (via Apple's "App Review Board") before it got approved. The first rejection was vague. The second cited the hacking tools clause. I wrote a detailed appeal explaining what Pwnagotchi actually is (a learning platform for Wi-Fi security) and how my app just provides a UI for it. That did it.
Revenue & Reality Check
I priced it at $7.99 CAD. Not too cheap (devalues the work), not too expensive (limits adoption). Here's the actual numbers, tracked manually in a Google Sheet:
| METRIC | NUMBER |
|---|---|
| Paid Downloads | 300+ |
| Revenue (gross) | $2,200+ CAD |
| App Store Rating | 5.0★ (50+ ratings) |
| Time to first $100 | 4 days |
That's not quit-your-job money, but for a niche hardware companion app? I'll take it. More importantly, it proved that I could ship something that strangers voluntarily paid for.
The 5.0-star rating was the real win. I replied to every single review — good or bad — and within two weeks of launch I'd fixed the three most-requested features. That visibility helped; new users saw "recently updated" and "developer responds" and felt confident buying.
Customer Support As a Feature
My support setup was primitive: $HOME/support/ folder with markdown files. But I responded within hours, not days. One user had a PyNumLib compatibility issue — I shipped a fix in 8 hours. Another wanted dark mode — added it in 3 days.
Shipping fast, communicating clearly, and actually listening turned users into advocates. Several brought friends into the Discord channel to thank me. That social proof mattered more than any ad spend.
What I'd Do Differently
Hindsight's 20/20. If I were doing it again:
- ◆ Build an email list first — I announced on GitHub Issues and Discord, but a simple waitlist would have given me a launch-day boost.
- ◆ Better screenshots — my initial screenshots were meh. Professional mockups would convert better.
- ◆ App Store Optimization (ASO) — I didn't really understand keyword research until after launch. Did a huge disservice to discovery.
The Real Takeaway
The code was the easy part. The hard part was believing I could ship at all. The App Store review. The pricing decisions. The support messages at 11 PM. The fear that no one would buy it.
But I did it. And you can too. You don't need permission. You don't need a degree. You just need to build something real, solve an actual problem, and ship it.