All projects
Personal

Promo Code Sniper 2025

A local vision pipeline built for a friend who kept losing limited-time gift codes to viewers with a shorter path to the game's redemption service. It samples the stream at 5 fps, reads candidate text with PaddleOCR, checks it with a small on-device model, and hands him the code in under 300ms — with no cloud APIs and no per-request cost.

Role
Sole developerPipeline, models & latency
Timeline
2025
Environment
Local, on-device
Latency
Under 300ms end to end
01 · Context

Context & problem

A friend of mine watches a live stream where the streamer drops random gift codes into a small announcement panel. The codes are limited, and the race is first-come-first-served.

He was watching from a region with a longer network path to the game's redemption service. Viewers in the US often got there first. Reading the code, typing it, and submitting it by hand added just enough delay to lose.

The obvious fix was to automate the boring part: watch the stream, find the code, and get it to him the moment it appears. It had to run on his machine, not in the cloud — paying for API calls to catch a free gift code would have been silly. That constraint shaped the whole design: local, fast, and cheap enough to leave running.

02 · Role

Role & scope

This was a personal project, built end to end for one user: my friend. I owned the capture loop, the OCR pipeline, the local validation model, code matching, the alert and clipboard handoff, and the profiling work that got it under the latency target.

I did not build the game, the stream, or the redemption service. The project is a client-side assistant that watches what is already on screen and helps him act on it.

The final version is OCR-first. I spent time with YOLO and TensorRT while pushing the latency down, but a separate object detector was not needed for this layout — PaddleOCR on the captured region was enough. The local model does the second-opinion check that keeps OCR false positives from wasting his time.

03 · Architecture

How the system fits together

Everything runs on one machine. The only network hops are the stream coming in and the game's redemption service on the other side.

The pipeline is a loop: sample the screen at 5 fps, crop the announcement region, run PaddleOCR, extract candidate strings, pass candidates to a small local model that decides whether the text is actually a gift code, then alert my friend and put the code on his clipboard. The final redeem is still his click; the tool removes the reading and typing time.

5 fps is deliberate. A code stays on screen long enough to be caught by the next frame, and the lower rate leaves headroom for the models. The expensive part is not looking at the screen; it is running recognition and validation, so the pipeline only sends plausible candidates to the local model.

The 300ms budget is the design constraint. The OCR and validation path returns in roughly 100ms; the rest is capture, candidate extraction, and getting the code into his hands. To get there, the models are quantized, preloaded, and warmed up before the loop starts, and the hot path avoids anything that waits on the network.

04 · Deep dives

The parts that took real expertise

Capture at 5 fps

The capture loop samples the screen five times a second. That sounds slow, but a code is not a single-frame event: it stays up long enough for the next sample. The trade-off is deliberate — more frames mean more OCR work and more heat, without a meaningful chance of catching a code earlier. The capture region is kept small so the OCR model sees the announcement panel, not the whole stream.

OCR with PaddleOCR

PaddleOCR does the recognition on the cropped region. The target is short alphanumeric codes, which are easy to confuse with ordinary stream text, usernames, or overlay graphics. I tuned the crop and the preprocessing, and used confidence plus the code's shape to decide what is worth passing to the validator. The OCR model is quantized and runs through a TensorRT pipeline.

Local model validation

OCR alone produced plausible strings that were not codes. The second stage is a small local model, run through llama.cpp, that looks at the candidate and decides whether it is actually a gift code. It is the difference between “this looks like a code” and “this is worth interrupting him for.” Running it locally keeps the check free and private; the model is preloaded and warmed up so the first code of the stream is not the one that pays for startup.

The 300ms budget

The first versions were too slow. The fix was not one clever trick; it was a series of small ones: quantize the models, preload them before the loop, warm up the inference engine, remove work from the hot path, and profile the Python pipeline until the expensive stages were obvious. TensorRT and the Ultralytics YOLO models were part of that learning curve; the final pipeline uses OCR and the local validator, but the profiling habits came from that exploration. The result is a path where recognition and validation return in roughly 100ms, leaving room for capture and handoff inside the 300ms target.

Handoff, not auto-redeem

The tool's job ends at the handoff. When a candidate survives validation, it alerts my friend and puts the code on his clipboard. He still opens the redemption flow and presses the final button. That was a deliberate boundary: the assistant removes the reading and typing time, but it does not pretend to be a bot that plays the game for him. It also means a false positive costs him a glance, not a bad redemption attempt.

Accuracy & measurement

The 94% figure is end-to-end: across the codes the pipeline flagged, 94% were valid and successfully redeemed. The remaining cases are the cost of running fast on a live stream. I cared about both sides of that trade-off: a miss loses a code, but a false positive pulls his attention away for nothing. The local validator is there to keep the second failure rare.

05 · Problems

Hard problems & trade-offs

False positives were the first real problem. OCR produced strings that looked exactly like codes but were not, and every one of them was a wasted interruption. The fix was the local model: instead of trusting the OCR output, the pipeline asks a second, slower check whether the text is actually a gift code. The trade-off is latency per candidate, so validation runs only on strings that already match the rough shape of a code.

The latency budget was the second. The early versions missed the 300ms target, and profiling showed that model startup and per-frame work were eating the budget. Quantization helped, but the bigger wins were preloading the models and warming up the inference engine before the loop started. The first code of a stream should not be the one that pays for initialization.

The third was the live-stream edge case. The code sits in a small panel, under overlays and compression, and a frame can catch half a character. 5 fps is a compromise: most codes stay up long enough, but a very short flash can fall between frames. The pipeline is tuned for this one layout; a different stream layout would need a new crop and a fresh accuracy check.

06 · Results

Results & impact

The number that mattered was not on a dashboard: before this, my friend had never caught a code. After it, he was winning regularly. The pipeline did not make his network faster; it removed the human delay — reading, typing, and reacting — so the latency gap stopped deciding the outcome. He went from zero successful redeems to consistent wins, which is the kind of jump that is hard to put a clean percentage on.

07 · Reflection

What I would do differently

The biggest latency win was boring: preload and warm up. The first code of a stream should not be the one that pays for model startup. I would build that into the first version next time, rather than treating it as an optimization pass.

I would also build the labeled test set earlier. 94% is only meaningful next to the method: how many codes, under what stream conditions, and whether misses were counted. Next time I would record a set of frames, label the codes, and measure precision and recall per stage instead of judging the pipeline by whether it felt fast on the night.

I explored YOLO for region detection and TensorRT for speed. The final pipeline did not need a separate detector — PaddleOCR on the captured region was enough. The lesson was to test the simplest architecture before adding another model. The local validator earned its place because it fixed a real failure mode; a detector would have been extra complexity for this layout.

08 · Expertise

Expertise demonstrated