Skip to content
Role
Founder – product · design · full-stack · operations
Team
Solo
Period
2026.06 – 2026.07
Tools
Next.js, TypeScript, Supabase, Python, Tailwind, Figma

Startup grants × an anonymous builder community

Commit

Commit, live – from the builder feed to the notices tab, a field filter and one search

Summary

I took a product from zero to launch in 28 days. A daily pipeline collects and normalises government startup grants, with an anonymous builder community on top. Where the data was not certain, I left the field empty instead of filling it.

28 days
From plan to launch

Measured – 264 commits, Jun 16 – Jul 13, 2026

11,757
Grants collected

Output – as of Sep 6, 2026

22.1%
Grants showing an amount

Measured – the rest are left blank

Role – product · design · frontend · backend · data pipeline · operations (solo)

Overview

Turning public data into product information is half of Commit. The grant API returns raw text with 1,722 different ways of writing an agency's name. A crawler collects it daily, normalises it in seven steps, and the product shows only the cleaned result.

On top of that, a grant is treated as a post written by the agency's account. The grant list and the community feed share one skeleton, and an agency's page is drawn with the same component as a person's profile. Finding a grant, getting an idea tested anonymously and recording the work continue along one timeline. It's a mobile-first PWA – installed to the home screen, it opens like a native app.

Live: commit.ai.kr

Problem

Even finding grants is hard.

  • Scattered sources – 1,722 different ways of writing agency names in the production database.
  • Messy originals – duplicated agency names, broken "?" characters, and prefixes like "(re-posted)(extended)".
  • No amounts – the grant API has no funding amount; it lives only in a separate programme API.
  • Wrong order – the collected list arrives by posting date. The only hard deadline a grant has is its closing date.
  • Nothing after finding – half-formed ideas are hard to post in a real-name community.

Key decisions

  1. 01Leave an unknown amount blank; never estimate it with AI

    Joining the two APIs could produce an estimate. But one wrong amount costs more trust than many right ones earn, and the funding amount is the number people decide on.

    →Amounts show only where the rules matched with certainty – 2,595 of 11,757 grants (22.1%). The rest stay blank and link to the original. Coverage dropped; no wrong amount reaches the screen.

  2. 02Treat a grant as a post by the agency's account

    Then there's no reason to build grant search and the community separately – people and agencies post, get followed and get saved by the same rules.

    →The grant list and the feed share one component skeleton; agency pages reuse the profile header, and follow, bookmark and report work the same way.

  3. 03Pivot from community-first to grants-first

    People arriving from search were looking for grants. A community needs people to exist; grants only need a crawler. Live chat went for the same reason – without people online at once, it's an empty room.

    →Mentors, a demand-validation gate, build scores and ad slots were removed, and chat became permanent topics. The home is public without login, which is only asked for at actions like posting – which also fixed search indexing.

  4. 04Guarantee anonymity in the schema, not in a promise

    An anonymous community is only trustworthy if even the operator can't expose who wrote what. A read-anyone policy on the users table would have let the public key harvest every member's email and name.

    →Row-level security on every table from day one; posts are read only through a public view without author identifiers; personal columns are locked with column-level grants. A post reported by three different users hides itself, and a person then deletes or restores it.

Process

Research

K-Startup, Bizinfo, contest sites, startup media, Disquiet, Indie Hackers and Product Hunt, and Notion went into one comparison table. The conclusion wasn't "nobody does this" but "nobody connects it": there are places that collect grants, places that write startup news and places to share building in public – none that links the three.

Looking at the data itself mattered too: the founding-period and age fields often listed "every possible range", so tagging them as-is put every tag on every grant.

Design

"Toss's information hierarchy + Threads' timeline." Blue means action, green means alive, and no arbitrary values.

  • Tokens – a 7-step type scale (11 / 12 / 13 / 15 / 17 / 20 / 24), buttons at 36 / 44 / 48, radii 8 / 12 / 16 / full, 15 colour tokens × light and dark, one typeface (Pretendard) at −0.02em. All design values.
  • Action colour and link colour split – after changing the accent blue, blue links inside body text became hard to read, so the button colour and the in-text link colour were separated.
  • Three responsive layouts – under 768px / 768px+ / 1280px+, with a 640px centre column and a 360px right rail. Mobile tap targets grew from 36px to 44px.

Build

Collection and normalisation

A Python crawler collects grants daily and cleans them in seven steps. Titles pass five regex stages: strip the duplicated agency name, drop broken encoding, unwrap the leading bracket cluster, normalise whitespace, group editions of the same grant. The same rules were applied retroactively to past data.

Notifications are deduplicated by a unique key, every run writes a heartbeat, and an alert fires if nothing has been collected for 36 hours. It runs on GitHub Actions at no extra cost. Only the multi-page backfill runs on a personal Mac through launchd – if the Mac is off, that job doesn't run.

Database and search

Supabase (PostgreSQL): 24 tables and 12 views and RPCs. Search is built in three layers: typo-tolerant trigram search → pgvector semantic search (with the built-in gte-small embedding model) → autocomplete and personal weighting. Above that sit similar grants by embedding distance, past editions by title series, and similar agencies by the centroid of an agency's grant embeddings. Three cards on the grant page were cluttered, so they became one. If an RPC fails, client-side results take over.

The limits are written down too: there are no LLM calls – grant summaries and eligibility checks are rule-based. With a paid external API, cost alone would have kept search out of the product.

Proof component

The commit_logs table carries a CHECK constraint: a row can't be inserted unless at least one of figma_url, github_commit_hash, image_url or link_url is set. is_verified is a GENERATED column – true automatically once a Figma or GitHub link exists. The rule that proof has to come from doing, not saying, is enforced at the database level.

Logos, handled in three layers

Agency logos follow the same rule as amounts. A favicon service returned blank icons as "success", so automatic collection wasn't trusted: 116 hand-checked logos, 210 matched by domain, and 17 saved as files where hotlinking was blocked. Prefix matching covers subsidiary bodies. It still falls far short of all 1,722 name variants.

Scale and lessons learned

In 28 days, 938 files changed (+27,927 / −6,233 lines); the code is 118 files and 10,536 lines with 47 components – all output counts. Thirteen mistakes were logged: a feed limit that showed only four days of posts, dates off between UTC and Korean time, an ad script loaded only as a preload so ownership verification failed, and a white logo SVG that vanished in the light theme until it became an inline currentColor component.

Results

11,757
Grants collected (output)
250
Open right now (output)
22.1%
Grants with a verified amount (measured)

The product is finished. Whether it's right is still unknown. What didn't work gets stated as plainly as what did.

  • 14 signups (measured) – 5 official bot accounts, 9 individuals. Most of the 9 are the founder or people they know.
  • 33 posts by people vs. 407 bot news posts (measured) – most of what fills the timeline is bot-posted news. That's the result of deciding to fill an empty feed with startup news.
  • ₩0 in revenue (measured). Monthly infrastructure cost is ₩0 too.
  • CSP is still Report-Only – it doesn't block violations, and with no collection endpoint it doesn't even observe them.

Where a person steps in

  • By itself – a Python crawler gathers grants daily, normalises them in seven steps and records a heartbeat on every run. It says so when nothing has arrived for 36 hours. Posting and reporting carry per-action rate limits.
  • By hand – on money, accuracy beat coverage. Where the rules are not sure, the figure stays empty and the reader is sent to the original (22.1%). Auto-hidden posts are deleted or restored by a person, and the admin tool re-checks permission on every server action, not once at page entry.
  • Never handed over – no model is called at runtime. Amounts and summaries are rule-based. Removing the place where it can be wrong costs less than correcting wrong answers.

Reflection

What held up

  • Leaving an unknown number blank. Amount coverage stopped at 22.1%. In exchange, every amount on screen matches the original. In turning public data into product information, the expensive mistake is a wrong value, not a missing one.
  • When an external dependency dies, the screen should live. Search, logos and amount matching all had their failure states designed first.
  • Building the design system afterwards. Extracted after the screens existed, it was an observation rather than a guess.
  • Data never collected can't leak. Choosing anonymity lowered the regulatory cost too, and a delete-account button was on the pre-launch checklist.

What failed

  • I assumed row-level security also controlled columns. It controls which rows; which columns is a job for grants. Every member's email could have gone through that gap.
  • I assumed building the community first would bring people. People arriving from search wanted grants, so community features came out and grants moved to the centre.
  • I assumed a "success" response meant a usable result. The favicon service returned blank icons as success. After that, logos were checked by hand.
  • I read inconsistent UX as a screen problem. Most of it was an information-structure problem: it only resolved once ownership sat in one place.

What I would test next

  • The integration hypothesis. Whether one timeline for finding and recording reduces drop-off can first be seen somewhere between 10 and 100 users.
  • Whether the search is worth it. The three-layer search needs an A/B against plain text search. There's no sample for it yet.
  • How to raise amount coverage. Going past 22.1% means adding sources beyond the single one, K-Startup.
  • Abuse handling. Rate limits, auto-hiding and blocking exist but have never met real traffic.
  • The debt – zero tests, a remote database that migrations alone can't reproduce, and a CSP stuck in Report-Only.