[KongchangAI]
· 4 min read· 2,015 words

Sourcerer: An Offline AI Bargain-Hunting Assistant That Uses Code for Money and a Model for Recognition

Sourcerer: An Offline AI Bargain-Hunting Assistant That Uses Code for Money and a Model for Recognition

Open-source Sourcerer uses local Gemma 3 for item recognition and pure functions for max-bid calculation, enabling offline pricing decisions.

Sourcerer is an offline-first open-source tool that helps thrift sellers decide the maximum bid for an item without a network. It strictly splits tasks in two: a locally running Gemma 3 4B handles image recognition and keyword extraction, while pure TypeScript functions compute amounts based on platform fees, shipping, supplies, and target margin, ensuring offline and online conclusions are identical. Through four configurations evaluated on 30 items, the developer found JSON Schema eliminates brand fabrication, while anti-fabrication prompts improve confidence calibration rather than accuracy. Money conclusions are ultimately assembled by deterministic functions to prevent the model from flipping numbers. Engineering-wise it uses Temporal for task durability and candidly discloses limitations like 30–40 second recognition and the lack of free sold-price data.

Sourcerer app interface: after photographing an item, a four-stage pipeline runs in sequence, giving a buy-or-not decision and the maximum offer

In the basement of a thrift store, with no phone signal, how do you decide whether an $8 jacket is worth buying? Sourcerer's answer isn't "what is it worth" but "what's the most I should pay right now"—the ceiling after subtracting platform fees, shipping, supplies, and your desired profit margin.

This open-source project splits the work in two: a locally running Gemma 3 4B model handles "understanding" the item, while pure TypeScript functions handle "calculating the money." According to developer ItsPriyamSri's submission to the Hacktoberfest Open Source AI Challenge, the entire system is offline-first—no network, no API keys, zero cost per scan.

Why Split the Model from the Arithmetic

The developer's first attempt was to have a small model both recognize the item and give a buying recommendation. The problem turned out to be subtle: ask a 4B model whether you "should buy," and it gives a confident, fluent, but arithmetically wrong answer. What's actually needed is to solve a formula:

max_offer = (resale − resale·fee − shipping − supplies) / target_multiple

A language model is not the right tool for this. What it's good at is looking at a poorly lit photo of a tag and saying "this is a washed Pendleton flannel shirt, and it's shrunk."

So the system was permanently split in two: Gemma 3 4B reads the image and extracts keywords locally; pricing comes from live listings or the user's own sold history; the final amount is determined by pure functions in packages/shared/src/economics.ts. The decision functions involve no I/O, so the conclusion computed in the browser offline is the exact same code as the one computed on the server online—zero discrepancy.

"The decision happens where there's no signal." The developer emphasizes this isn't a cost problem but a physical one—the only solution is to put the model where the decision is made.

Gemma 3 4B is the small-parameter version in Google's open-source multimodal model family released in early 2025. It supports text and image input (i.e., "vision" capability) and can run on consumer hardware via local inference frameworks like Ollama. "4B" means roughly 4 billion parameters; after quantization, the VRAM footprint is typically in the 3–4 GB range, which makes image understanding on a laptop or phone without a network feasible. This also defines its capability boundary: it excels at common-sense recognition and tolerating blurry images, but multi-step arithmetic and precise numerical reasoning are of limited reliability—precisely the technical basis for the article treating it as a "recognizer" rather than a "decision-maker."

What a Single Prompt Can't Buy: The Evaluation Data Speaks

The developer ran four configurations on 30 manually labeled items using gemma3:4b at temperature 0.1. The results were somewhat counterintuitive:

ConfigurationParseable JSONBrand correctType correctBoth correctFabricated brandMedian latency
v1 prose prompt100%97%100%29/301/48.8s
v2 + JSON Schema100%100%100%30/300/48.6s
v3 full-rules prompt100%100%100%30/300/48.5s
v4 + anti-fabrication rules100%100%100%30/300/48.3s

The developer expected prompt engineering to be the main lever, but found that "parseability" was already 30/30 with a naive prompt. What actually mattered were two different things: JSON Schema reduced fabricated brands from 1 to 0; the anti-fabrication rules changed confidence calibration, not accuracy.

Item #21 illustrates this best: "Eames-style lounge chair, rosewood, no labels anywhere." Under the naive prompt, the model returned "brand": "Eames (Style)" with 0.95 confidence—a cleanly formatted, confident, machine-readable fake brand. After adding the Schema, the brand field was allowed to be null, the model chose "don't know," confidence dropped from 0.95 all the way to 0.6, and it proactively noted "no visible labels."

The developer admits a remaining issue: the title still says "Eames Lounge Chair Rosewood," and the title is exactly what gets pasted into a listing. Since pricing anchors to comparable sold prices rather than the title, the amount isn't affected, but the listing generator should refuse to use the designer name when the brand is null—that's the next step.

JSON Schema constrained decoding means injecting the syntactic structure of the output format (field names, types, whether null is allowed) as a constraint at inference time, so the model can only generate content that conforms to the schema. It solves the problems of "parseability" and "field validity"—for example, once the brand field is declared nullable, the model has a legitimate "don't know" exit rather than being forced to invent a plausible-looking value. Note that a Schema guarantees correct format, not truthful content; the drop in fabricated brands from 1 to 0 in the article is more because the null option removed the pressure to "fill something in" rather than a change in the model's knowledge boundary. Confidence calibration is another dimension—it refers to how well the model's self-reported probabilities match actual correctness, and typically requires dedicated prompting or post-processing to improve.

The Model Only Reads, Doesn't Compute; a Second Model Only Calls Tools

Sourcerer runtime log: showing which provider executed each stage, latency, and whether data left the device

The recognition model and the tool-calling model are separate—this is a capability boundary, not an accident. ollama show gemma3:4b shows Gemma 3's capability list as completion, vision—no tool calling.

So the advisor agent runs on llama3.2:3b, which has tools capability and answers the same amount question in about 2 seconds. The developer tried Qwen 3 4B: tool calling was correct, but each step emitted about 35KB of thinking content, and answering a $215 sale question within a six-step budget took over four minutes.

More critical is the question of who owns the money conclusion. The model correctly repeats the numbers but flips the conclusion: for a $150 snare drum with a $58.82 ceiling, three runs gave three different answers—one saying "below the ceiling," one saying "you should buy," and one saying "you should pass."

The final solution is that the money sentence is no longer written by the model: feeMath assembles the reply using the same decideVerdict() as the scan interface. Ask the same question five times and the answer is character-for-character identical:

PASS. Most I would pay is $58.82. At $150 you would make $26.46 if it resells at $215.

What's left for the agent is understanding the request and calling tools with the right parameters. A three-turn transcript shows "I'll be shipping across state lines, use $35 for shipping" converted to shippingCost: 35, the ceiling changing from 58.82 to 49.82, and the third turn still remembering the first.

Tool calling is the ability to have a language model output structured function-call requests during generation, which an external runtime executes and returns results to. It typically relies on the model having seen specific call formats during training, so not all models have it—the capability tags listed by ollama show let you judge at a glance. A model without this capability, even if prompted to "please call a tool," can only generate text that looks like a call and cannot be reliably parsed. In addition, tool-calling models often produce "chain-of-thought"-style intermediate output, and the token count directly determines latency; the article's mention of Qwen 3 4B emitting about 35KB of thinking content per step and causing responses over four minutes is a typical cost of this mechanism on small models.

Engineering Details: Durable Tasks and Honest Data

eBay moved completed-transaction listings behind a login wall, so free sources of real sold prices are gone. The most common temptation is to return asking prices labeled as sold prices, but the developer chose to tag every comparable with kind: "asking", display "asking" on every row in the UI, and multiply by a publicly named factor:

export const ASKING_TO_SOLD_FACTOR = 0.86;  // annotated at every application site

The bulk repricing task runs on Temporal, with two activities per item: refresh comparables and apply the new price. The developer verified durability by SIGKILLing the worker mid-scan: with 12 items, the first worker refreshed 3, and after restart the second worker refreshed 9, with 0 duplicate refreshes and 24 activity executions (expected value 24). The crash cost only time, nothing else.

Performance data is equally candid: recognition from a photo takes 30–40 seconds, from typed text 8.3–8.8 seconds; the first scan after a restart waits an extra ~75 seconds to load weights; a local 1B model generates a listing draft in about 2 seconds. The developer acknowledges that 30 seconds is slower than the "ten-second decision" this tool targets—the main gap yet to be closed.

Connecting to real services also exposed three defects, two of them silent: Ollama 0.40 removed POST /api/embeddings, so every write stored a NULL vector and "have I bought this before" quietly degraded to keyword matching; 32 seed records timed out because Ollama only answers one model request at a time; and the comparables step recorded network calls as provider: cache, when that row's very job is to tell the user whether a query left the device.

Temporal is an open-source durable execution workflow engine. It persists workflow state, activity invocation records, and retry history to a database, so after a worker crashes another worker resumes from the checkpoint, ensuring each activity runs "at least once" and can be configured for idempotency. What the article verifies by SIGKILLing a process is exactly this property: completed activities don't rerun, and unfinished ones are taken over by a new worker. The accompanying concept is activity idempotency—if an activity itself writes to an external system, business-side deduplication is needed to achieve a "exactly once" effect, which is why "0 duplicate refreshes" in the article needs to be understood in light of the specific implementation.

Why Open Weights Matter

The developer's answer isn't a vague "privacy" but something specific: decisions happen where there's no signal—basements, back rooms, barns, rural estate sales, the corners of flea market warehouses. Any architecture that needs a round trip to a server to price something fails at the very moment the product exists.

Open weights make this possible: no per-call billing, no rate limits, no vendor able to deprecate the model version the prompts depend on. Running cost is just electricity—for a part-time seller handling 20–40 items a month, a cent or two per call amounts to a few dollars a year, so cost isn't the argument; the round trip is.

The developer also doesn't shy away from limitations: frontier models have a larger world-knowledge base for genuinely obscure collectibles and no quantization loss on fine label details. This 4B model is confident about Levi's and Pyrex but shaky on unmarked mid-century chairs. If you're appraising a $4,000 item, go to a serious model; Sourcerer optimizes for a different scenario—forty $12 items, ten seconds each.

Other honest boundaries: the price model is empty without sold history, so conclusions in the first week lean toward PASS; nobody has paid for this, so pricing is just an assumption; the largest evaluation is only 30 items, labeled by one person on one machine in an afternoon.

Design Trade-offs Worth Noting

The most valuable part of Sourcerer isn't how many models it uses but its restraint about what models should do. Recognition goes to the model because blurry photos need tolerance and common sense; arithmetic goes to code because money needs to be auditable. When the model got the conclusion backwards, the developer's response wasn't more prompts but taking that sentence away from the model.

The project is MIT-licensed; after cloning, run pnpm install && bash scripts/setup.sh, then open two terminals to start brain and web respectively—no API keys needed to see the core loop work. For developers wanting to study local multimodal model deployment or interested in offline-first architecture, this is a clearly structured reference implementation with verifiable evidence.

#offline AI#Gemma 3#local LLM#open weights#Temporal#thrift resale#multimodal model
Share:

Related articles