DEV Community

Cover image for This week I made a mutual-aid app using Sanity and Jev
Bramandia G Adam
Bramandia G Adam Subscriber

Posted on

This week I made a mutual-aid app using Sanity and Jev

Sanity Challenge Path Two Submission

This is a submission for the Sanity Challenge, Path Two: Vibe-Code Something Strange

What I Built

Vouch is a small mutual-aid app. Someone types or dictates what they need ("two bags of rice and a dozen eggs for my kids"). The app turns it into a checklist. Neighbors pledge items from that checklist. Whoever buys the items uploads the receipt, and the request closes with a certificate anyone can check.

The strange part is that the AI can't write a single sentence.

I didn't want a model rewriting a stranger's words, and I didn't want an app that says "verified" when nothing was checked. So the AI (Jev, a decision model from TypeSafe) only answers narrow, typed questions: yes or no with a probability, pick one of these options, score on this scale. Everything else is plain code and Sanity content: thresholds, quantities, and the sentences that explain why something happened. If anything is uncertain, a human volunteer decides.

Sanity is the trust gate. A request that hasn't been verified stays a Sanity draft, and the public dataset never shows drafts. Publishing means "verified". The whole lifecycle is a Sanity Workflows definition, so Jev and a volunteer push the same draft through the same steps.

Demo

Live app: https://vouch-sanity.vercel.app (no account needed)

Verifier password: "mbgb4ck8jcxws6"

A quick way to try it (about 10 minutes). The app is open to everyone, so the example requests may already have been pledged or fulfilled by someone else by the time you read this. The steps below make your own request instead, so they work no matter who came before you.

  1. Ask for help. Go to /ask and type (or dictate): "Toothpaste and soap for my kids." Jev proposes a checklist from the catalog. Edit it if you like, then submit. It should go live in about 10 seconds. Keep the private link you get: it's how you come back to your request.
  2. Pledge. Open your request from the feed and pledge an item. Open it in a second browser to see the count update live.
  3. Upload a receipt. On your request, click "Upload the receipt", then "Use the pharmacy receipt". It's a made-up receipt that covers toothpaste and soap. The receipt is read in your browser, Jev matches the lines, code counts the units, and you get a certificate. If you asked for something else, use the sample whose "covers" list matches your checklist, or edit your checklist to match.
  4. Be the verifier. Submit a second request that asks for money ("Could someone send me money by PayPal for rent?"). It stays private and waits for a volunteer, with the reason shown. Then go to /desk, passcode mbgb4ck8jcxws6, any display name, find your request and approve, send back, or reject it. The desk is shared, so you may see other people's requests too.
  5. Check a certificate. Open the one from step 3, or this older one, and change a single character in the payload box.

If you're short on time, these finished examples don't change: sent back, edited, published and a request in Spanish, approved by a volunteer.

Requests marked "Demo" were written by me. Each network can submit 10 requests and upload 10 receipts per hour, with a daily cap of 25 for each. Everyone sharing a network shares those limits, so a busy network may have to wait. Vouch is not an emergency service.

- Vouch's Sanity Studio

- The feed of verified requests
The feed of verified requests

A request starts with the person's own words
A request starts with the person's own words

An unclear request waits for a volunteer, with Jev's reasons
An unclear request waits for a volunteer, with Jev's reasons

The verifier desk sign-in gate
The verifier desk sign-in gate

A public request with an optional pledge form
A public request with an optional pledge form

Fulfilled, with units shown on the receipt
Fulfilled, with units shown on the receipt

Certificate with its hash checked in the browser
Certificate with its hash checked in the browser

Code

GitHub logo bO-05 / vouch-sanity

Vouch: verified mutual aid.

  • web/: Next.js 16 on Vercel.
  • studio/: Sanity Studio v6, the schema and a seed script.
  • web/src/workflows/need-lifecycle.ts: the Workflows definition.
  • web/src/lib/jev.ts: the only place Jev is called. Every call is saved as a decision document.

My Build Process

The app was built in the OpenCode and Codex desktop app with Claude Opus and GPT model. I never opened a terminal myself: the agent ran every command, from the Sanity login to deploys to browser checks. I started from an empty folder on Sep 26.

How I worked with it. Before writing any app code I asked the agent to set up a handoff/ folder (plan, research notes, status, a build journal) and a small skill that reads it at the start of every session. After that most of my prompts were just "go". The chat was disposable. The files were the memory.

The one rule I set. My first attempt at this idea faked its trust layer. Proof checks always passed, and "escrow" was a boolean. So the first line I gave the agent was: no fake success. If Jev, Sanity, or OCR fails, show the failure or send it to a human. Almost every decision after that came from that sentence.

Things that went wrong, and what I learned

Calibrating the gate. I had the agent write 33 made-up requests, each with the answer a careful volunteer would give, and run the real gate over them with real Jev calls. The first run sent 13 of 16 perfectly fine requests to a volunteer, mostly because Jev is unsure about urgency (real requests sit between two levels). I first thought I needed a better threshold. The real fix was asking a better question: urgency should only sort the feed, never block anything. After that, 33 of 33 came out right.

A miss caught in production. "Could someone send me money by PayPal..." scored 0.30 on the money flag and was published automatically. My question listed gift cards and crypto but never just "money". Because the question lives in Sanity as policy content, I fixed the wording in the Studio with no redeploy. PayPal went from 0.21 to 0.99.

Telling Jev which line I mean. My first receipt matcher scored 5 of 10. Lines were in an array and the question said lines[5], and Jev kept answering about the neighboring line. Naming each line (line_06) fixed it. The lesson: point the model at things by name.

Sanity Workflows. I gave it a one-day timebox with a simpler fallback, and kept it after about 1.7 hours. It's early access and it showed: the CLI shipped an older engine than npm, and an effect's name is also its handler key, which took an error message to work out. Latency was the surprise. Each engine call made many sequential requests, and a response header showed the dataset was in Belgium while my functions ran in Washington. A project setting moved them to Paris. In the end a clean request takes about 9 seconds to go live instead of 1.4, and I chose to show the real stages rather than fight the engine.

Two bugs only a human would find.

  • Dictation on my Android phone gave "we we we need we need we need rice". Chrome on Android resends the whole sentence so far. The fix rebuilds the text from all results each time.
  • I played every role myself, and ended up with a request marked Fulfilled whose checklist still said "2 still needed". Worse, the receipt line "INFANT FORMULA 56.97" is really three cans at 18.99, and the app didn't know that. So now code counts the units on the receipt (Jev never counts), and only "fulfilled" when everything is there. A short receipt goes to a volunteer, who has to write a public note to accept it.

Also worth knowing: the browser's Tesseract reads the same receipt differently from Node's, so I kept fixing the samples until they worked in the browser.

Not built: the App SDK. The verifier desk is a normal Next.js page. I decided it was "only if everything else is done", and it wasn't.

Honest limits

  • One receipt has to show the whole checklist for an automatic "fulfilled". Receipts that don't print quantities count one per line and go to a volunteer.
  • Jev is strongest in English, so other languages go to a bilingual volunteer.
  • Rate limits are per network, so people on one network share them.
  • The urgency "confidence" in a request's trail is Jev's own number and often looks low. It never blocks anything.

Credit

Receipts are read in the browser with Tesseract.js. The decision model is Jev by TypeSafe. The visuals borrow from an earlier weekend sketch of mine; the logic was written from scratch during the challenge.

Sanity Project Details

How the schema carries the trust:

  • need: the requester's words are read-only in the Studio, so nobody edits them. It's created as a draft, and publishing means verified.
  • policy: thresholds and the exact yes/no questions live here as content, editable without a redeploy.
  • decision: every Jev call, with its questions, answers, model, and timing. The plea is stored only as a SHA-256 digest, so a private draft can't leak through a public decision.
  • pledge: written in the same guarded transaction as the checklist counter, so they always agree.
  • certificate: canonical JSON plus its SHA-256, which anyone can recompute in a browser.
  • Private by id: receipt photos, reviews, rate-limit counters, and workflow instances use dotted ids, which a public dataset doesn't expose.

Agent Session

The initial build was done in OpenCode, which DEV's session uploader doesn't list yet, so the moments that mattered are quoted in "My Build Process". The Oct 1 visual polish pass was done in Codex.

Top comments (0)