Founding as a Developer — Validate Before You Build
An Ordered, Windowed Funnel and Channel Attribution
Goal
From the "Moanote" records, build a people-count funnel that keeps the order and the 14-day window, measure how much common mistakes inflate the numbers, then calculate and compare the CAC per channel with first-touch and last-touch attribution.
Why it matters
The funnel decides where people leak, and attribution and CAC decide where to spend money. If you count events or ignore the order and the window, the cells inflate, and if you silently change the attribution model, the CAC of the same channel changes. You must pin the definition down in code and make it recompute so that the team talks with the same numbers.
Materials and definitions
/opt/fixtures/founder/product/users.csvandevents.csv(the same as the metrics module),/opt/fixtures/founder/product/touches.csv(user_id,ts,channel, the touches before signup), and/opt/fixtures/founder/product/spend.csv(month,channel,spend_krwfor 2026-03 to 2026-05).- Exclude internal accounts (is_internal=1). You may compare times in UTC as they are (the window and the order are differences between times, so they are unrelated to the time zone).
- Funnel: signup → create_doc → share_doc → invite_sent → upgrade. Each step is reached by the first occurrence at or after (the same time included) the time the previous step was reached, and every step falls within 14×24 hours of the signup time (boundary included). Count by number of people. The result is five integers,
[가입, create_doc, share_doc, invite_sent, upgrade](the Korean word in the first slot means signup). - Paid converter: an external user who has an upgrade within the observation period. First touch = the line in touches.csv with the earliest ts for that person, last touch = the line with the latest.
- CAC (won) = the three-month sum of that channel's spend_krw ÷ the number of paid converters attributed to that channel, rounded down to an integer (
//). If zero people are attributed,null. - The five channels: organic_search · paid_search · social_ads · referral · content.
Steps
- In
/root/founder/funnel/counts.json, write{"events": 줄 수, "users": 사람 수}(the placeholders are the number of lines and the number of people) for each of the four funnel event types (create_doc, share_doc, invite_sent, upgrade), for external users, over the whole observation. - In
/root/founder/funnel/funnel.py, createfunnel(fix)— a list of five integers, as defined. - In
/root/founder/funnel/order.json, write the five cellsunorderedfor when the order is ignored (a step is reached if the earlier steps' events are all present regardless of order within the 14-day window) andorderedas defined. - In
/root/founder/funnel/window.json, write the five cellsno_windowfor when the window is removed (through the end of observation, keeping the order) andwindow_14d. - In
/root/founder/funnel/conversion.json, writestep_rates(cell i ÷ cell i−1, four values, rounded to four decimal places),overall(upgrade ÷ signup), andbiggest_drop(the name of the arrival step of the step with the lowest conversion rate, for example "share_doc"). - If
funnel(fix, channel=None)infunnel.pyreceives a channel, it produces the funnel from only the people whose last touch is that channel. In/root/founder/funnel/channel.json, write the funnels of the five channels. - In
/root/founder/funnel/attribution.json, write the number of paid converters per channel for each offirst_touchandlast_touch. - In
/root/founder/funnel/cac.json, write the CAC per channel for each offirst_touchandlast_touch, pluscheapest_paid_last(among channels whose cost is greater than 0, the channel with the lowest last-touch CAC) andrank_changed(true if the CAC ranking of channels that have costs differs between the two models).
Notes
- If you sort each person's (time, event) list and advance a step pointer one slot at a time, you get the ordered funnel.
- First and last touches come out in the right order even with a string comparison of times (
"2026-03-01T…Z") — because the format has a fixed length. - Common mistakes: writing the number of event lines as the number of people, ignoring the order, having no window, mixing first and last touch, and rounding the CAC.
The number of events and the number of people are different
In /root/founder/funnel/counts.json, write events (number of lines) and users (number of people) for create_doc, share_doc, invite_sent, and upgrade, for external users.
From the same event list, get len(list) and len(set(users)) side by side. Filter out internal accounts with users.csv.
A people-count funnel that keeps the order and the 14-day window
In /root/founder/funnel/funnel.py, create funnel(fix). Five integers: [signup, create_doc, share_doc, invite_sent, upgrade].
Sort each person's (time, event), and advance one cell when the event you are waiting for next appears after the time the previous step was reached. Discard events that occur more than 14×24 hours after signup.
Ignoring the order inflates the later cells
In /root/founder/funnel/order.json, write two funnels: ordered (as defined) and unordered (a step is reached if the earlier steps' events are all present within the 14-day window regardless of order).
Leave the window as it is and change only the reach judgment. For unordered, look only at whether the set of event types within the window contains all the earlier steps.
With no window, older people look like they convert more
In /root/founder/funnel/window.json, write two funnels: window_14d (as defined) and no_window (through the end of observation, keeping the order).
Leave the order judgment as it is and remove only the 14-day window.
Step conversion rates and the cell that leaks the most
In /root/founder/funnel/conversion.json, write step_rates (four values), overall, and biggest_drop (the name of the arrival step of the step with the lowest conversion rate).
step_rates[i] is funnel cell i+1 ÷ cell i. biggest_drop is the name of the step the lowest ratio "arrives at" (one of create_doc through upgrade).
The funnel by signup channel
If funnel(fix, channel=None) receives a channel, it produces the funnel from only the people whose last touch is that channel. In /root/founder/funnel/channel.json, write the funnels of the five channels.
Narrow only the list of signups by the channel of each person's latest-ts touch in touches.csv; the rest of the calculation stays the same.
Attribute conversions by first touch and last touch
In /root/founder/funnel/attribution.json, write the number of paid converters per channel for each of first_touch and last_touch.
A paid converter is an external user who has an upgrade within the observation period (not the 14-day window). The first touch is the channel with the earliest ts, and the last touch is the channel with the latest ts.
The attribution model changes the channel CAC
In /root/founder/funnel/cac.json, write the CAC per channel for first_touch and last_touch (rounded down to an integer, null if zero attributed), plus cheapest_paid_last and rank_changed.
CAC = three-month cost sum // number of attributed converters. Leave channels whose cost is 0 out of the ranking comparison. rank_changed is whether the order you get by sorting channels with costs by CAC ascending differs between the two models.