Inspiration
Most of what reaches a small team is not a form. It's an email with the budget buried in paragraph three, a 47-second voicemail, a photo of a paper intake sheet with a coffee ring on it. Form builders handle the one input that's already structured. The new AI ones fill in the rest with confident guesses.
We wanted the opposite on both counts: a form builder that reads the messy input, and says "I'm not sure" when it isn't. We also wanted to remember the person on the other side of the form. Priya, on a bus, shouldn't have to thumb through nine boxes. She should be able to just talk.
What it does
DodoForm is an AI form builder for the web and Android.
- Build: describe a form in a sentence and DodoForm writes the fields. Want a change? Ask ("add a budget range"). No dragging, no dropping.
- Collect: respondents can fill a public form with a voice note, a photo, a PDF or pasted text. The AI fills the fields, then shows Review what AI heard, with anything it's unsure of in amber.
- Extract: owners drop in anything messy (text, image, audio, PDF). Every field comes back with a confidence score and ambiguity flags. The uncertain ones wait for a person instead of being guessed.
- Verify: approve a record with a swipe, with four seconds to undo. Corrections become examples the extractor uses the next time, on that form.
- Act: when a record needs a human, the human gets a push notification through OneSignal. Otherwise the phone stays quiet.
- Understand: Analytics reads the responses for you. It gives a plain-English summary, the trend with a 14-day projection, where people drop off, how they feel, and a lead score on every response. You can also ask it questions.
- Pay: Pro can be bought on Google Play through RevenueCat, or on the web. Either way, it's one entitlement.
- Trust: restore purchases, delete your account and report an AI output, all inside the app.
The Home screen keeps score of the manual entry you skipped, and prints its assumptions next to the number:
$$T = 90(N - P) + 240P + 60S \quad \text{(seconds)}$$
Here \( N \) is structured responses and \( P \) is the subset the AI read from messy input. \( S \) is records synced to another tool. On our demo workspace (\( N = 537 \), \( P = 157 \), \( S = 0 \)) that's \( 71{,}880 \) seconds, about 19 h 58 min. The rates are deliberately conservative: if the estimate is going to be wrong, it should be wrong low.
How we built it
Web and data. Next.js 16 and React 19 on Supabase Postgres and Auth, across 69 SQL migrations. Row Level Security is the authorization boundary.
App. Flutter talks to the same Supabase project directly, so the app needs no backend of its own. We use purchases_flutter for Play Billing, onesignal_flutter for push, and native Google Sign-In. The Supabase user ID is also the RevenueCat app user ID and the OneSignal External ID, so a subscription and a push audience always describe the same person.
AI. A router over 16 provider configurations (Groq, Gemini, OpenRouter, Amazon Bedrock and others), with circuit breakers, fast timeouts, and separate free and premium lanes. Voice notes go through Whisper large-v3 on Groq. Images and PDFs go to a multimodal model, Gemini 2.5 Flash by default. Every call must return a schema-validated envelope per field: value, confidence, source snippet and ambiguity flags.
Confidence. The prompt calibrates the scale: \( c_i \ge 0.9 \) only for values copied verbatim, \( c_i < 0.7 \) for anything inferred or approximate, and \( c_i = 0 \) when the value is absent. Then a check in code enforces the rule, because the prompt alone didn't:
$$\text{a record needs a human} \iff \min_i c_i < 0.7$$
Notifications. The in-app notification row is written first and is the record of truth. The push is a best-effort tap on the shoulder. Review pushes coalesce per form over a 15-minute window, so ten submissions in a minute produce one push. There's a single low-credit warning, at 10% remaining, and it says responses are still being captured.
Billing. RevenueCat and web-billing webhooks write one subscription record per user, on the server. The app never claims a plan itself, so there's nothing for a patched APK to forge. Events are idempotent and ordered by timestamp. A Play event can't overwrite an active web subscription:
if (existing && existing.provider !== "revenuecat" &&
["active", "trialing", "pending"].includes(existing.status)) {
return NextResponse.json({ ok: true, skipped: "web_subscription_active" });
}
Process. We planned in Kiro specs (requirements, design, tasks). We moved Android release builds to Codemagic CI: about 10.5 minutes a build, against 7 to 20 locally when it finished at all.
The demo video is code too. It's a Remotion composition on a 120 BPM grid:
$$\frac{60 \text{ s}}{120 \text{ beats}} \times 30 \text{ fps} = 15 \text{ frames per beat}$$
So every cut, camera move and word lands exactly on a frame. The narration is Gemini TTS, the score is Lyria RealTime, and the sound effects are synthesized in the score's key.
Challenges we ran into
- The model read the prompt too literally. We asked for confidence "below 0.7" on hedged answers. On the paid lane, a hedged value came back at exactly 0.7 in six out of six runs. That isn't below 0.7, so it auto-approved. The fix was a check in code, not a sterner prompt.
- Webhooks arrive in the wrong order. An expiration correctly revoked an account. Then a cancellation for the same subscription arrived one second later with a newer timestamp, and gave the plan back. Now a paid period that has already ended grants nothing, whatever the event says.
- Free AI tiers moved under us. Groq retired Llama 3.3 70B for free tiers, which silently knocked out our top provider until we switched to gpt-oss-120b. Groq's rate-limit error links to a billing page, so our circuit breaker read one busy minute as a billing failure and benched the provider for an hour. Of the seven free image-capable models on OpenRouter, exactly one answered both a text and an image request. And the failover chain could have sent free credits to a paid model, so paid providers now sit in a premium lane that free traffic can't reach.
- "Delivered" is hard to measure. OneSignal reported zero recipients for pushes that arrived on the phone. We now judge a send by its notification ID and confirm delivery afterwards. Tag values have to be strings: a Journey comparing "2" with 2 never fires, and reports nothing.
- A test that passed on garbage. The production build mangled our time-saved sentence into "Estimated at 9024060s per record copied into another tool." The test only checked that "90", "240" and "60" appeared, so it passed. It now checks the exact sentence.
- Android release plumbing. Google Sign-In error 10 came down to a missing Play app-signing certificate fingerprint. A 55-minute Gradle build and zombie daemons pushed us to CI. Then a missing variable group meant one CI workflow had never run once, while everything looked green.
Accomplishments that we're proud of
- A form builder that would rather ask than guess, all the way through: on the respondent's side, on the owner's side, and in the review queue.
- One entitlement across two stores, decided on the server, with out-of-order and paired webhook events handled.
- Notifications restrained enough that people will leave them on.
- A time-saved number you can argue with, because it shows its working.
- 96 web test files, 65 Flutter test files, and an AI layer that survived providers disappearing mid-hackathon.
- A demo where every claim is something the app does, on screen, in real footage.
What we learned
- Put promises in code. A prompt can ask for calibrated uncertainty; only a check delivers it.
- Treat every webhook as a claim, not an instruction. Check it against time and state before acting on it.
- Uncertainty is a feature. People trust "I'm not sure" more than a confident wrong answer, as long as the unsure part is easy to fix.
- Restraint keeps people coming back. Coalescing and a single warning did more than any extra push would have.
- Measure the real outcome. Real delivery, the exact string, the actual entitlement. Not the dashboard's stand-in for it.
What's next for DodoForm
- OneSignal Journeys on the tags the server already writes (
plan,trial_days_left), starting with a trial-ending campaign. - RevenueCat experiments (annual versus monthly as the default), targeted offerings, cancellation surveys and win-back offers.
- CRM connectors we can honestly call live: OAuth token refresh and real sandbox round-trips for HubSpot and Pipedrive, then ATS integrations.
- iOS, on the same server-side entitlement model.
- A deeper accuracy loop. Today, corrections become examples for each form. Next, they should tune the review threshold per field.
Built With
- amazon-web-services
- bedrock
- flutter

Log in or sign up for Devpost to join the conversation.