SneakerLens β an AI sneaker authenticator built live
Java Meetup Β· June 2026
Notice the shape of Step 1 β Step 2: identify first, then use that identity to know what to look for. That two-step reasoning is exactly what "agentic" means β and it's what we're about to define properly.
A single prompt β single response is not an agent. It's a function call.
"Nike Air Jordan 1" β that exact string gets woven into Step 2's system prompt, changing what Step 2 looks forMost "agent" demos online are actually Tier 1 or 2 wearing a fancier name. The real signal is whether the question changes, not just the data.
If the answer to any one of these is "no," stay at a simpler tier β a workflow or a single call will serve you better than an agent.
Gemini 2.5 Flash takes images + text in the same request. No separate OCR step, no separate vision model glued on with string parsing.
We send up to 4 photos (top / side / sole / label) as Part.fromBytes() alongside the prompt text β one request, native understanding of both.
Authentication is inherently visual: stitching density, font kerning on logos, sole texture β things a human expert looks at, not reads.
A text-only LLM literally cannot do this task. Multimodal isn't a nice-to-have here β it's the entire premise.
Whenever a cheap signal can reject obviously-bad input before your expensive step runs: image quality checks, language detection, profanity filters, "is this even on-topic" gates.
Step 1's output (e.g. "Air Jordan 1") is woven into Step 2's system prompt β that's the agentic part.
Web MVC + Thymeleaf for server-rendered UI. JPA + MariaDB for users & analysis history. HttpSession for auth.
com.google.genai SDK directly (API key, not Vertex). spring-ai-model's BeanOutputConverter for structured JSON output.
DJL (Deep Java Library) + PyTorch engine running a pretrained ResNet18 β pure Java, no Python sidecar process.
Records as the contract Gemini's JSON has to match β @JsonPropertyDescription becomes the schema sent to the model
This is where the "agent" actually happens β Step 1's identity gets formatted directly into Step 2's system prompt string
Guardrail β Step 1 β Step 2. Plus the retry-with-backoff wrapper for Gemini's transient 503s
DJL Criteria builder loading a pretrained ResNet18 β runs once at startup, predicts per-upload at request time
Login β upload real sneaker photos β watch Step 0 / 1 / 2 run live β check the admin dashboard
Uploaded photos contained two visually distinct sneakers. Gemini "helpfully" described both β as a JSON array of two identity objects. Our schema expected exactly one. Jackson threw MismatchedInputException and the whole analysis crashed.
Fix: explicit prompt instruction ("always one object, never an array") + defensive code that unwraps an array and takes the first element rather than crashing.
The POST handler returned the result view directly. Browser's last action stayed a POST β hitting refresh (or even our own "reload" button) silently resubmitted the form, re-running both Gemini calls.
Fix: classic POST β Redirect β GET. Then went further: AJAX fetch() submission so the page never navigates at all β uploaded photos survive across errors and re-analysis too.
gemini-2.0-flash started returning 404 β model no longer available mid-build, with no code change on our side. A different model also briefly hit a free-tier quota of limit: 0.
This isn't a Gemini-specific quirk β it's the industry norm. OpenAI deprecates and retires model snapshots constantly (gpt-4-0314, gpt-4-32k, multiple preview tags gone within months). Anthropic and Google do the same on a slower cadence.
Fix: moved to gemini-2.5-flash, added retry-with-exponential-backoff for transient 503/429s, and β the real lesson β never hardcode a model ID as a fact you can forget about. Treat it as a config value you'll revisit, the same way you'd treat a third-party API version string.
Runs on your own CPU. No API cost, ~0.2-0.7s.
tokens (input-heavy β images dominate the cost)
tokens (more output β evidence, verdict, resale)
Shown live in the app's own "Agent Token Usage" panel β built so the audience can see the real cost, not a guess.
Part.fromBytes() β JSON over HTTP requires this to travel as base64 on the wire (~33% size inflation over raw bytes).Gemini's Files API lets you upload media once, get back a file reference, and pass that reference into multiple generateContent calls (valid 48h) β pay the upload cost once, not per step.
| Approach | How it works | Tradeoff |
|---|---|---|
| Inline base64 (what we do today) | Image bytes embedded directly in the JSON request body | Simple, but re-sent on every call β bigger payloads, more latency, no reuse |
| Files API reference (Gemini Developer API) | Upload once β get a file:// handle β reference it in N calls | Upload cost paid once across our 2-step chain; smaller request bodies |
| GCS URI (Vertex AI only) | Point Gemini at gs://bucket/photo.jpg directly | No re-upload at all if the file already lives in GCS; needs Vertex AI, not the plain API key |
| S3 URI | Not natively supported by Gemini | Google's stack only understands GCS β an S3 file has to be copied/proxied first |
For a 2-step agent chain reusing the same photos, Files API is the easy win β it's a same-vendor, same-API-key change.
Key change from today's demo: Redis-backed sessions β our current HttpSession lives in one JVM's memory and breaks the moment Cloud Run scales past 1 instance.
Data moving between systems β browserβLB, LBβCloud Run, Cloud RunβCloud SQL, Cloud RunβGemini.
Data sitting still β on disk in Cloud SQL, objects in GCS, Redis snapshots.
users table are exactly the kind of field CMEK is meant forRule of thumb: in transit is about who can intercept; at rest is about who can read the disk if they get physical/storage-level access.
There's no shortcut here β Gemini (or any model) cannot reason over ciphertext. Homomorphic encryption + LLM inference is still research, not something you ship. If the model needs to "see" the data, it has to be decrypted first.
Minimize what you send (crop/redact anything not needed for the task), use enterprise terms with data-retention guarantees (Vertex AI vs. default consumer API), or self-host an open model inside your own VPC so plaintext never leaves your trust boundary at all.