This is a submission for the Hacktoberfest Open-Source AI Challenge Week 2: Side Hustle

What I Built

If you sell on Instagram or WhatsApp in Nigeria, you've typed this caption before: "Available. DM to order." For the fifth product that day.

Oja ("market" in Yoruba) writes the listing for you. Upload one to three photos, add a hint if you like (what it is, its condition, the price you have in mind), pick a voice, and get back:

  • a title
  • an Instagram caption with hashtags
  • a longer description
  • a WhatsApp status, with an Open in WhatsApp button
  • a rough Naira price guide (a guide, never a promise)

The voices are Clear English, Naija Pidgin and Yoruba-flavoured English. Every piece has a copy button, because the people who'd use this are on their phones, between customers.

Oja in numbers

  • Live at oja-ng.tech, with the code on GitHub under the MIT licence.
  • 3 real sellers tried it, and all 3 said they'd pay. Two named a figure: about ₦10,000 and ₦15,000 a month.
  • About 150 to 215 s per listing running Gemma 3 locally on a CPU-only laptop, and about 7 to 34 s hosted with Gemma 4 (different models, details below).
  • 115 tests, and 0 axe accessibility violations in every state I tested.

I built it for Nigerian sellers because most tools I see are made with another market in mind. I wanted a seller to open something that already feels like theirs: products they recognise, a market they know, and a voice that sounds like how they talk to customers. If English isn't where they're most at home, Pidgin might be, and I wanted them to feel heard, not translated.

"Yoruba-flavoured" means English with light Yoruba touches. It is not full Yoruba. Real Yoruba output is the next thing I want to build.

Who would pay? Anyone who sells online and posts new stock regularly. All three sellers who tried Oja said they would pay. Two gave amounts (about ₦10,000 and ₦15,000 a month), and the third said it depends on the value of what he sells (details in My First User).

The moment I knew the prompt was wrong

An early hosted Pidgin test was a striped tote bag. The description came back like this:

This tote bag get beautiful rust color with yellow stripes and teal polka dots. E simple but e get style well well. You fit carry am go market, work, or even outing. We dey deliver for Lagos, Abuja and Port Harcourt…

Good Pidgin. But the WhatsApp status from the same run came back in plain English, and the caption was only half in voice. Only the description was fully in character. I rewrote the prompt so every field follows the chosen style, with a worked example per style. Here is a Pidgin listing for a jar of cherry jam, from the saved example on the live site (written by gemma-4-26b-a4b-it on 16 October 2026, with the prompt as it was after that fix):

Caption: Oya see fine Cherry Jam! Blue Pearl brand, e sweet well well. DM to order.
Description: This na Blue Pearl Cherry Jam inside glass jar. E get white label with cherry picture and red patterned lid. DM to order, we dey deliver for Lagos, Abuja and Port Harcourt.
WhatsApp status: Blue Pearl Cherry Jam don land! Sweet jam inside glass jar. DM to order, we dey deliver nationwide.
Price guide: ₦2,500 to ₦4,500, "Price fit change based on jar size"

Demo

Live: https://oja-ng.tech (a paid Render instance, so it doesn't sleep)

Oja showing a Naija Pidgin listing for a jar of cherry jam, with title, Instagram caption, description, WhatsApp status and Naira price guide

The hosted demo uses a free-tier model endpoint, which sometimes has a bad moment. If you see "The AI service had a hiccup", press the button again, or press Try an example to see a saved result straight away. (The saved examples were written before my last prompt change.)

Code

GitHub logo SeyinAlao / oja

Oja turns one product photo into Instagram and WhatsApp listings for Nigerian sellers, in English, Naija Pidgin or Yoruba-flavoured English. Open-weight Gemma, OpenAI-compatible endpoint, runs hosted or local.

Oja

Listing writer for small Nigerian sellers on Instagram and WhatsApp. Upload 1 to 3 product photos, add optional hints, pick a voice (Clear English, Naija Pidgin or Yoruba-flavoured English) and get a title, caption with hashtags, description, WhatsApp status and a rough price guide, ready to copy.

Built with Next.js (App Router), TypeScript (strict), Tailwind CSS and Framer Motion. The AI is any open-weight vision model behind an OpenAI-compatible endpoint; the default is gemma3:4b on local Ollama.

Oja on a wide screen: a Naija Pidgin listing for a jar of cherry jam, with copy buttons and a price guide Oja on a phone: a Naija Pidgin listing for an Ankara tote bag, with copy buttons and a price guide

Both screenshots are real generations from hosted gemma-4-26b-a4b-it through Google's OpenAI-compatible endpoint (the model badge in each shows it), not mock-ups.

Run locally

ollama pull gemma3:4b ollama serve # skip if Ollama is already running pnpm install pnpm dev # http://localhost:3000

Other scripts: pnpm test, pnpm lint, pnpm typecheck, pnpm build…

MIT licensed. The README covers running it locally with Ollama, pointing it at another endpoint, deploying to Render, model licences and known limitations.

How I Built It

Two ways to run the same app

Setup Model Time per listing
Local, Ollama, CPU only gemma3:4b 148 to 157 s on the server; 170 to 215 s through the UI
Hosted, Google AI Studio free tier gemma-4-26b-a4b-it about 7 to 11 s in my benchmark runs; 20 to 34 s in runs through the UI

These are different models, so please don't read this as a same-model speed test. It shows what each setup feels like to a seller. I haven't worked out why the UI numbers are higher than the server benchmark. Google's model picker describes Gemma 4 26B A4B as a mixture-of-experts model that activates about 4B parameters per inference.

App. Next.js (App Router), TypeScript in strict mode, Tailwind v4, Framer Motion and zod. One route, /api/generate, talks to any OpenAI-compatible chat/completions endpoint, and three environment variables choose the model:

# Hosted: Gemma 4 on Google AI Studio AI_BASE_URL=https://generativelanguage.googleapis.com/v1beta/openai/ AI_MODEL=gemma-4-26b-a4b-it AI_API_KEY=... # Local: Gemma 3 on Ollama, nothing leaves the machine AI_BASE_URL=http://localhost:11434/v1 AI_MODEL=gemma3:4b 

Prompting. The system prompt is written for Nigerian commerce: Naira formatting, "DM to order", delivery wording for Lagos, Abuja and Port Harcourt. Each style has its own voice rules and a worked example. The model first lists what it can see in the photos (colours, patterns, handles, straps), which pushes the copy toward concrete details. The reply must be strict JSON, validated with zod, with one repair call if it isn't.

Privacy. Photos are resized in the browser, sent once to the model and never stored. There's no database. The route has a rate limit and a request size cap.

Quality. 115 unit tests (vitest) and an axe-core check with 0 violations in every state I tested, including the saved-example mode. I have not done manual screen-reader testing yet.

How do I know the Pidgin isn't wrong?

One of my testers asked exactly this, and the honest answer is that Oja can't know. Here is what I do and don't have:

  • What I have: a worked example per style in the prompt, an automated check that voice markers appear in all three text fields, and my own reading of sample outputs. I'm a Yoruba speaker, so I read the Yoruba-flavoured outputs myself.
  • What that can't tell me: a marker check can see that "dey" appears. It can't tell whether a sentence sounds natural or fits the product. In some runs the Pidgin tote listings suggested "summer wear" and "beach", which the photo doesn't support, and they still passed the voice check. Pidgin and Yoruba also differ by region, age and context, and one reviewer is not a test.
  • Design choice: Oja shows the draft for the seller to read before posting, because they're the best judge of their own voice. The results page now says so.
  • Next: a "doesn't sound right" button for sellers, and a set of Pidgin and Yoruba listings reviewed by native speakers, which is the data you'd need to fine-tune an open-weight model.

Suggestions are a feature. Claims are not.

My first prompt told the model never to invent details, and it kept adding one short use suggestion anyway ("enjoy it with bread or pancakes" for jam). I liked that. It's what a good shop assistant says. So I changed my mind: the prompt now allows one short, generic use suggestion, and forbids stating facts the photos and hints don't support: material, size, brand, authenticity, condition, battery details, longevity and the like. That matters most for categories like perfume and used phones, where an invented "original" or "no scratches" could mislead a buyer.

I measured it with 10 live generations per style on two images (the jam jar photo, and a flat illustration of a tote bag), before and after the change:

Old prompt New prompt
Runs with an unsupported claim 2 of 60 0 of 60
Runs with a sensible use suggestion 32 of 60 57 of 60
Valid JSON on the first try 60 of 60 60 of 60
Voice check passed 59 of 60 59 of 60

The two old unsupported claims were a "sturdy" handle and "orange fabric" on the tote bag. My coding agent read all 120 outputs and counted them, rather than relying only on a regex. It's a small sample, and going from 2 to 0 is not a guarantee. That's why Oja now shows a line under the results: "Read it through and fix anything that isn't true for your product before you post."

What went wrong on the way

  • Smaller images made it worse. To cut latency I tried 768 px photos instead of 1024 px. In my test, 0 of 9 responses were valid JSON on the first try, and 2 failed outright. I kept 1024 px.
  • A latency setting that wasn't available. Google's endpoint rejected a reasoning-effort option for this model (HTTP 400 or 500).
  • I leaked my own endpoint. The "model unreachable" error panel echoed the model server URL to the browser. The error-path tests I had my coding agent write caught it before any visitor did.
  • A lost backslash. The agent's own edit made Retry-After: 1 wait 0 ms. A test caught that too.
  • My guard was too strict. The "no sellable product" check rejected a flat illustration of a tote bag as "not a real product", though the same image had produced a listing earlier. It works on real photos, but it can refuse mockups.
  • Buttons didn't look clickable. Tailwind v4 stopped giving buttons a pointer cursor. I noticed when I hovered over mine and nothing happened.

Reliability, honestly

The free hosted endpoint sometimes returned HTTP 500, 502 and 503. In a batch of 9 calls with my early prompt, 2 failed. On the live site, some listings worked at once and some needed up to four tries. I added one automatic retry on 429, 5xx and network errors, plus an optional fallback model. The fallback never triggered in my testing, so I can't claim it helped. By design it only fires after the retry fails on a 429, 5xx or network error, not on timeouts or invalid JSON. I also added a Try an example button that shows a saved result instantly, labelled as saved and not live, so a bad moment on the free tier doesn't leave a visitor with nothing.

What it can't do yet

  • Yoruba is "flavoured" English, not Yoruba.
  • It may refuse illustrations and mockups.
  • Local mode is far too slow on my CPU-only laptop to recommend today.
  • In hosted mode, the photo goes to the model provider. I haven't verified what Google's free tier does with prompts, so check before using real customer photos.
  • The price guide is a model's estimate, not market data.

Why Does Open Innovation Matter?

One app, two ways to run it. I moved Oja from local Ollama to Google's hosted Gemma by changing three environment variables. With a closed API, "where do my seller's photos go?" has one answer. With open weights it has two: a hosted endpoint when speed matters, or the seller's own machine when privacy does. Local mode was too slow on my laptop, but it works, and a machine with a GPU would change that. I haven't tested that.

No lock-in. If a provider changes its price or has a bad week, the plan is a config change, not a rewrite. I built the architecture for that but haven't proven it in a real outage.

A market a general model ignores. Today Pidgin and Yoruba come from prompting alone. Open weights are what make fine-tuning on reviewed Pidgin and Yoruba listings possible, so the voice is trained instead of prompted.

Not every "open" is the same. Google's Gemma 4 license page is the Apache License 2.0. The Gemma 3 model I use locally comes under Google's own Gemma Terms of Use, which include a prohibited-use policy. Both let me run the weights myself, but the terms differ, so the README links both. Oja doesn't redistribute any weights, and anyone building a business on either should read the terms first.

Where open did not win. Running it locally is too slow on my laptop for a real seller, and the free hosted tier is flaky. I also didn't benchmark a closed API, so I can't claim Oja is cheaper or better than one. The hosted tier cost me nothing during the challenge, but I haven't worked out what a listing would cost at paid rates, and that's the number I'd need before charging sellers.

My Agent Session

I built Oja with Claude Code, an AI coding agent. The sessions are saved with DevRelay:

Building Oja: a Nigerian listing writer on open-weight AI (Gemma 3 + Ollama)
You

Build a brand-new project called Oja: a listing writer for small Nigerian sellers on Instagram and WhatsApp. Upload 1-3 product photos, optional hints, pick a style (Clear English, Naija Pidgin, Yoruba-flavoured English) and get ready-to-post copy. Next.js + TypeScript strict + Tailwind + Framer Motion, no DB, no auth. AI layer must call any OpenAI-compatible /v1/chat/completions endpoint (default local Ollama gemma3:4b), return strict JSON validated with zod, retry once with a repair prompt, log generation time. Verify against local Ollama and record the real generation time. Work end to end without asking questions.

Show more
Agent

Checking the environment first: Node, pnpm, git, Ollama and whether gemma3:4b is pulled.

▸ PowerShell node -v; pnpm -v; ollama list; Invoke-WebRequest http://localhost:11434/api/tags
node -v; pnpm -v; ollama list; Invoke-WebRequest http://localhost:11434/api/tags
Node v22.20.0, pnpm 11.1.2, gemma3:4b present, Ollama responding 200
Agent

Scaffolding by hand rather than create-next-app to avoid interactive prompts. Installing Next 16, React 19, zod, framer-motion, Tailwind v4, vitest.

▸ PowerShell pnpm add next react react-dom zod framer-motion; pnpm add -D typescript tailw...
pnpm add next react react-dom zod framer-motion; pnpm add -D typescript tailwindcss @tailwindcss/postcss eslint eslint-config-next vitest
Installed. First attempt failed because Set-Content wrote a BOM into package.json; regenerated it with node.
Agent

Writing the AI layer: zod schema (with coercion of '25,000' strings and hashtag normalisation), a brace-balanced JSON extractor that strips code fences, per-style voice prompts, an OpenAI-compatible client with a timeout, and generateListing() which retries once with a repair prompt naming the invalid fields.

▸ Write lib/schema.ts, lib/extract-json.ts, lib/prompts.ts, lib/ai-client.ts, lib/gen...
lib/schema.ts, lib/extract-json.ts, lib/prompts.ts, lib/ai-client.ts, lib/generate.ts, lib/rate-limit.ts, app/api/generate/route.ts
Files created
Agent

Writing vitest coverage for fence stripping, extraction edge cases (braces and escaped quotes inside strings, truncated objects), validation, the repair flow, and the rate limiter. Then the UI: dropzone with client-side 1024px resize, segmented style picker, skeleton loading with rotating messages, result cards with copy buttons, WhatsApp link, error panels.

▸ Bash pnpm typecheck; pnpm test; pnpm lint
$ pnpm typecheck; pnpm test; pnpm lint
typecheck clean, 19 tests passed. Lint initially crashed: typescript-eslint does not support TypeScript 7, so I pinned typescript to 6.0.3; lint then clean.
Agent

Build passes. No sample image existed, so I generated a synthetic Ankara-style tote bag with System.Drawing, started the production server and called the API once per style against real Ollama.

▸ Bash node scripts/smoke.mjs english | pidgin | yoruba
$ node scripts/smoke.mjs english | pidgin | yoruba
english 148.7s, pidgin 148.0s, yoruba 157.2s on 100% CPU gemma3:4b; all returned valid JSON on the first attempt
Agent

About 150s per listing is too close to the 180s default timeout, so I raised the default to 300s and changed the last loading message to set expectations. Then drove the real UI with headless Edge: upload, resize, pick Pidgin, generate, copy. It completed in 170s with the Copied confirmation, and the only console error was a missing favicon, which I fixed.

▸ Bash playwright-core UI run (outside the repo)
$ playwright-core UI run (outside the repo)
UI wall 170.6s, copied label shown, 1 favicon 404
Agent

Finished: README with env vars, endpoint swapping, Render notes, measured timings and a Why open models section; 8 conventional commits with no AI attribution; lint, typecheck, tests and build green.

Shipping Oja: model badge, error-path tests, Render prep, a11y and a public repo
You

Oja already works (Next.js, TypeScript strict, Tailwind v4, Framer Motion, OpenAI-compatible AI layer, default local Ollama gemma3:4b). Do, in order, with small conventional commits: 1) a model badge showing the model and whether it runs locally or on a hosted endpoint, via a /api/health route that never exposes keys or the URL, plus an elapsed counter for local runs; 2) exercise the 429, 413, timeout and unreachable paths with tests and fix what is broken; 3) a README table for Ollama, OpenRouter, Google AI Studio and generic providers, verified, and make sure the client works with and without AI_API_KEY; 4) render.yaml with pinned pnpm and Node, health check, sync:false env vars, manual deploy and domain steps; 5) metadata, OG image and an axe-core accessibility pass with keyboard check; 6) real screenshots; 7) create the public GitHub repo and push; 8) final lint, typecheck, test, build. No questions, no Claude attribution, never print keys.

Show more
Agent

Added endpointInfo() (model plus a local flag from the base URL hostname, with a lookalike-host test), a /api/health route, a ModelBadge, and an elapsed-seconds counter in the loading state that only shows for local models.

▸ Write lib/ai-client.ts (endpointInfo), app/api/health/route.ts, components/ModelBad...
lib/ai-client.ts (endpointInfo), app/api/health/route.ts, components/ModelBadge.tsx, components/LoadingState.tsx
created
Agent

Wrote route-handler tests with fetch stubbed: 429 with Retry-After after 11 requests, 413 by header and by real body size, 504 on timeout, 502 on unreachable, 502 after one repair attempt on garbage. One test failed: the unreachable message echoed AI_BASE_URL back to the browser. That is a real leak for hosted endpoints, so I changed the message to a generic one and kept the test as a regression guard.

▸ Bash pnpm test
$ pnpm test
46 tests passed after the fix (1 failed before it)
Agent

Provider facts were fetched from each provider's docs rather than written from memory: OpenRouter base URL and Bearer auth, Google's OpenAI-compatible base URL with image_url data URLs, Render blueprint fields, Node version precedence and DNS records. Google's Gemma page lists two Gemma IDs on the Gemini API, but nothing confirms them on the OpenAI-compatible path, so the README labels them as unverified examples.

▸ WebFetch provider and Render documentation pages
provider and Render documentation pages
OpenRouter https://openrouter.ai/api/v1; Google .../v1beta/openai/; Render region frankfurt valid; apex ALIAS/ANAME or A 216.24.57.1; www CNAME
Agent

Render prep: packageManager pinned to pnpm@11.1.2, corepack in the build command, and --prod=false because Render's NODE_ENV during build is undocumented. Verified in a clean clone with NODE_ENV=production that a frozen-lockfile install and next build succeed and that allowBuilds in pnpm-workspace.yaml is honoured. This was a Windows run, not a Render run, and the README says so.

▸ Bash git clone; NODE_ENV=production pnpm install --frozen-lockfile --prod=false; p...
$ git clone; NODE_ENV=production pnpm install --frozen-lockfile --prod=false; pnpm build
install and build succeeded
Agent

Accessibility: scripts/a11y.mjs runs axe-core on the empty, loading, result and error states with the API mocked, then tabs through the form. First run found small card headings below contrast, an h1 to h3 jump, and a duplicate tab stop on the hidden file input. Fixed with a darker ink token, h2 headings and tabIndex -1. All four states now report 0 violations.

▸ Bash node scripts/a11y.mjs http://localhost:3010
$ node scripts/a11y.mjs http://localhost:3010
empty/loading/result/error: 0 violations; focus order: Add photo, three inputs, style radio, Write my listing; A11Y OK
Agent

Generated the OG image from a script in the app palette, captured mobile and result screenshots (the result is one real gemma3:4b generation: 215 s through the UI on CPU), scanned the repo for secrets and attribution, then created the public repo and pushed. Lint, typecheck, 46 tests and the production build pass.

▸ Bash gh repo create SeyinAlao/oja --public --source . --push
$ gh repo create SeyinAlao/oja --public --source . --push
https://github.com/SeyinAlao/oja, main pushed
Getting Oja ready for Hacktoberfest: licence, honest README, secret scan
You

Project: "Oja" (Windows, PowerShell), a working Next.js + TypeScript strict app (Tailwind v4, pnpm, vitest), public on GitHub at SeyinAlao/oja, live at https://oja-ng.tech, entered in the Hacktoberfest 2026 Open-Source AI Challenge Week 2 (Side Hustle). Work without asking questions. Do NOT add Claude as a co-author or add any Claude or Claude Code attribution anywhere. Never print, log or commit API keys. Keep any code and documentation clean and professional, with no filler comments. Use small conventional commits and push to origin main.

  1. Add an MIT LICENSE file (year 2026, holder "Seyin Alao") and the matching license field in package.json.
  2. README: add near the top a short block with the live demo (https://oja-ng.tech), the demo video (https://www.youtube.com/shorts/oaQ9pkC1Vhw) and one line saying it was built for the Hacktoberfest 2026 Open-Source AI Challenge Week 2 (Side Hustle). Add a "Known limitations" section stating plainly: the free-tier hosted model endpoint sometimes returns HTTP 5xx errors and some listings need a retry; the optional fallback model has not been observed to trigger in testing; "Yoruba-flavoured English" is English with light Yoruba touches, not full Yoruba; manual screen-reader (NVDA/VoiceOver) testing has not been done; in hosted mode photos are sent to the model provider, so users should check that provider's terms; local CPU inference takes minutes. Make sure the timings table in the README clearly labels which model each number came from. Confirm docs/mobile.png and docs/result.png exist, are referenced correctly, and contain no people or private information.
  3. If the gh CLI is authenticated, run: gh repo edit SeyinAlao/oja --description "..." --homepage https://oja-ng.tech --add-topic hacktoberfest --add-topic gemma --add-topic open-weight-models --add-topic ollama --add-topic nextjs --add-topic typescript --add-topic nigeria --add-topic ai. If gh is not authenticated, give me the exact manual steps for the repo page.
  4. Security check: confirm .env* files are gitignored, and search tracked files and the full git history for anything resembling an API key (Google API keys typically start with "AIza") or a pasted .env value. Report only file names and commit hashes, never the matched text. If anything is found, stop and tell me what to rotate.
  5. Run lint, typecheck, tests and build, push, and reply with a short summary.
Show more
Agent

I'll work in the Oja repo. Starting by looking at the repo state.

▸ Bash git status && git log --oneline -8 && git remote -v && ls -la && ls docs && c...
$ git status && git log --oneline -8 && git remote -v && ls -la && ls docs && cat package.json && cat .gitignore
On branch main, up to date with origin/main, working tree clean. Latest: 6c2f936 docs: refresh screenshots with real products photo docs/: mobile.png, result.png package.json: scripts dev/build/start/lint/typecheck/test/a11y/og; no license field. .gitignore: node_modules, .next, .env, .env.local, *.tsbuildinfo, next-env.d.ts, test-assets/, .shots/, .shots-server.log
▸ Bash cat README.md && git ls-files
$ cat README.md && git ls-files
README (about 16 KB): intro, screenshots credited to local gemma3:4b, Run locally, Environment variables, hosted providers, AI layer, Decisions, Measured generation time (local and hosted tables), Accessibility, Privacy, Deploy on Render, Why open models. Tracked files: app/, components/, lib/, scripts/, docs/*.png, .env.example and config files. .env.local is not tracked.

Security check first, since a finding would stop everything else.

▸ Bash git check-ignore -v .env .env.local .env.production .env.development.local gi...
$ git check-ignore -v .env .env.local .env.production .env.development.local git ls-files | grep -i '\.env' git log --all --name-only --pretty=format: | grep -i '\.env' | sort -u git grep -l -E 'AIza[0-9A-Za-z_-]{20,}' for every commit in git rev-list --all: git grep -l -E 'AIza[0-9A-Za-z_-]{20,}' for every commit: git grep -l -I -E '(sk-...|sk-or-v1-...|ghp_...|github_pat_|hf_...|gsk_...|AI_API_KEY=\S+|AI_FALLBACK_API_KEY=\S+|BEGIN PRIVATE KEY)'
Ignored: .env, .env.local (not .env.production or .env.development.local) Tracked env files: .env.example only, ever. AIza in tracked files: none. AIza in history: none. Other patterns: README.md in commits 2f38c88, 55e7679, b4812cf, fad5e09. 32 commits scanned.

No AIza keys anywhere. Four old README versions matched a broader pattern; checking which pattern and whether it's a placeholder, without printing the value.

▸ Bash for c in 2f38c88 55e7679 b4812cf fad5e09: classify each match by prefix and r...
$ for c in 2f38c88 55e7679 b4812cf fad5e09: classify each match by prefix and remaining length only
All four: line 38, prefix AI_API_KEY=, remaining length 5, an angle-bracket placeholder.

Those four are a 5-character <...> placeholder in a docs example, not a key. Checking every historical .env.example value and long high-entropy strings.

▸ Bash for each .env.example version: print variable names and value lengths only fo...
$ for each .env.example version: print variable names and value lengths only for every commit: git grep -l -I -E '[A-Za-z0-9_-]{39,}' (excluding lockfile and images)
4 versions of .env.example: AI_API_KEY and AI_FALLBACK_API_KEY always length 0. No long opaque tokens in any commit.
▸ Bash git log --all --format=%B | grep -c -E 'AIza|sk-|AI_API_KEY=' file docs/*.png
$ git log --all --format=%B | grep -c -E 'AIza|sk-|AI_API_KEY=' file docs/*.png
0 matches in commit messages. docs/mobile.png: PNG 499 x 917 docs/result.png: PNG 780 x 5116
▸ Read docs/mobile.png, docs/result.png
docs/mobile.png, docs/result.png
[images viewed] mobile.png: wide two-column layout, a Naija Pidgin listing for a jar of Blue Pearl cherry jam, model badge gemma-4-26b-a4b-it (hosted). result.png: phone layout, a Naija Pidgin listing for an Ankara tote bag (drawn illustration), model badge gemma-4-26b-a4b-it, 'Written in 27.8s'.

Neither screenshot shows a face or any personal data, but the README's description of them is out of date: both now show hosted gemma-4-26b-a4b-it, and mobile.png is a wide-screen result, not an empty phone state. Zooming in on the jar photo to check for hands or anything personal.

▸ Bash crop the product thumbnail from docs/mobile.png and enlarge it 6x (System.Dra...
$ crop the product thumbnail from docs/mobile.png and enlarge it 6x (System.Drawing) into a scratch file
[image viewed] Part of a hand holding the jar at the left edge; no face, too low resolution to identify anyone.

The jar photo has part of a hand holding it at the left edge. No face and not identifiable, but it is part of a person, so I'll flag it in the summary rather than altering the image. Checking gh auth.

Related posts