Find an accountability partner, rewritten as code
Find an accountability partner: as a scheduled job, not a checklist
Every other guide on this topic treats the word "find" as a quality problem. Find someone whose goals match yours. Find someone reliable. Find someone outside your friends and family. On this site the verb "find" points at something smaller and more concrete. It is a GET handler at src/app/api/auto-match/route.ts. It runs every 30 minutes. It excludes any candidate pair whose session times sit more than 60 minutes apart on a UTC clock. What is left, it sorts by four keys and writes a match row for. The search is not something you do. It is something that has already happened to you while you were doing laundry.
What every other guide tells you to do
Run the same search and you get a predictable set of pages. Flow Club, Tony Robbins, LifeHack, Melissa Gratias, the Columbia GSAS student affairs PDF, the Focusmate blog, Theresa Cifali, Indeed, Develop Good Habits. They all give you the same shape of answer. First, decide your goal. Second, pick qualities to look for in a candidate (reliability, honesty, shared values, similar goals). Third, avoid close friends and family, because they turn into distractions. Fourth, have a kickoff conversation to align. Fifth, commit to check-in cadence. Sixth, keep going.
That advice is not wrong. It is also not an implementation. It never tells you how to actually be inside a pool where a partner can be produced for you; it only tells you how to evaluate one once you are somehow already talking to them. The "find" step, the one the search query asks about, is left as an exercise. This page is the exercise, worked out as code.
What actually runs when you sign up
Five kinds of row flow into a single cron. One kind of row flows out, plus three side effects (email, activity log, shared Meet URL). The hub is the GET handler; the arrows are the order the code takes.
inputs → auto-match cron → outputs
Nothing on the left side of the diagram is "a quality." Every input is a row, a field, or a status. The hub does not invite you to a vibe check. It evaluates predicates and writes rows.
The 60-minute UTC window, in code
The single most consequential line in the find action is the hard filter on line 164. It reads the circular UTC-minute distance between two sitters' session times and drops the pair if that distance exceeds 60. A wrap-around case (23:30 UTC vs 00:10 UTC) is handled by the 1440 - d branch of timeDiff, because 1440 is the number of minutes in a day.
Sixty minutes is not a wellness claim. It is the operator's bet on what time granularity still lets two adults in different timezones overlap meaningfully on a daily sit. A 30-minute window would starve the pool. A 120-minute window would pair someone who sits at 06:00 with someone who sits at 08:00 and call it a daily partnership, which is not what a daily partnership is. The number is a compromise that the runtime exposes plainly, where a guide on a different site would soften it into "find someone whose schedule works with yours."
What the sorter breaks ties on
Once the 60-minute filter passes, viable pairs are sorted by four keys. The ordering is strict lexicographic: each key is only consulted if all keys above it tie. There are no weights, no coefficients, no learned scoring function. The source is the clearest form of the argument.
One tick, end to end
This is what happens at :00 and :30 of every hour on the production server. Eight steps, evaluated in order, with a hard budget of one Postgres transaction per side effect.
Tick fires at :00 or :30
vercel.json line 13 holds the schedule '*/30 * * * *'. Vercel's scheduler calls GET /api/auto-match?live=true every 30 minutes, authenticated by the CRON_SECRET bearer token.
Pool is loaded from Postgres
One query over waitlist_entries, filtered to status in ('pending', 'ready') and unsubscribed = false. Ready rows sort first inside the query itself.
Eligibility gates run
status = 'ready' (bypasses every other check), or contact_count = 0 AND age > 24h, or contact_count between 1 and 9 AND last terminal match > 7d ago. contact_count >= 10 is excluded outright.
Slots are built
One morning slot per eligible person, plus a second evening slot if frequency is 'Twice a day' and evening_utc resolves. A twice-a-day sitter becomes two independent candidates in the pool.
Pairs are enumerated and filtered
Every unordered slot pair is evaluated. Pairs with a circular UTC minute distance above 60 are dropped. Pairs whose two people have any prior match row are also dropped.
Pairs are sorted lexicographically
Four keys, evaluated in order: readyScore (2/1/0), bothOld (true/false), sessionMatch (true/false), smallest UTC diff. No weights. No learned coefficients. Just comparisons.
Greedy non-overlapping pick
Walk the sorted list; write a pair if neither session slot is already used; mark both slots used. A person with two slots can land in two rows, never the same slot in two.
Match row is written
createMatchWithTokens or createMatch inserts a row into matches. A confirmation email with a token link is dispatched to either or both partners. The cron goes quiet until the next tick.
What the cron looks like from the operator's shell
Triggered manually with a bearer token, the handler prints a compact summary of what the pool looked like and what it wrote. Numbers below are illustrative for one tick on a typical weekday; the production handler emits a structured JSON response with identical fields.
The find action, in four numbers
If the whole argument of this page collapses to integers, these are the four that survive. They are literal constants in vercel.json and in the handler, not rhetorical flourishes.
None of these numbers is tunable per user. They apply to the whole pool, every tick, uniformly. The find action is not personalized. It is a single scheduled job with four hard-coded parameters. Personalization, if it exists anywhere, happens in the form fields you submit, which the handler then reads but does not argue with.
Every parameter the cron honors, as chips
Read left to right. Any of these tokens can be grepped directly in the repo to verify that the behavior of the find action is what this page says it is.
Checklist-style guides vs. the cron
| Feature | Top ranking guides (checklist find) | vipassana.cool (scheduled find) |
|---|---|---|
| What 'find' means | A checklist you apply to friends, coworkers, and strangers until one of them says yes. | A GET handler at src/app/api/auto-match/route.ts that runs on cron '*/30 * * * *' and writes a match row. |
| Who does the search work | You do. Interviews, vetting, goal-alignment calls. | The cron does. You do not see a single candidate before a row is written. |
| How long the search takes | Indefinite. Most guides suggest weeks of looking. | At most 30 minutes between ticks once you are eligible. |
| What 'viable' means | Shared values, compatible schedule, similar goals, reliability. | timeDiff(slotA.utcMinutes, slotB.utcMinutes) <= 60, and no prior match between the two. |
| Tie-breaker when two candidates both qualify | Gut feel. Most articles do not specify. | readyScore -> bothOld -> sessionMatch -> smallest UTC diff. |
| Failure mode | You never get around to actually asking anyone. | The pool is too thin this tick. You stay pending. Next tick is 30 minutes away. |
| Serial no-shows | No defined stop condition. | contact_count >= 10 skips you; the operator takes it from there manually. |
| Cost to you | Days of search time, ongoing social negotiation. | One 2-minute form. Everything else is cron cost, borne by the server. |
“The contact_count = 1 retry path requires at least 7 days between an expired match and the next eligibility tick. A ghosted pair cools off; it does not immediately re-enter the pool and rematch into another fragile row.”
src/app/api/auto-match/route.ts, lines 92 to 101
Why the cron is symmetric and the checklist is not
The general-advice guides teach the find action asymmetrically by default. You pick a partner. You assess them. You decide whether they meet your bar. The implicit model is that one side of the future relation (the reader) is doing the evaluating and the other side (the candidate) is being evaluated. Most of the checklist content is about how to evaluate better.
The cron cannot do that. It does not know which of the two rows in a candidate pair is "the reader". It evaluates both sides against each other on the same four symmetric keys. It is not picking a candidate for a specific user. It is picking two rows out of one pool, and either row could equally well be described as the user or as the match. The asymmetry the checklist depends on does not exist in the data model, so it does not exist in the outcome.
The consequence is that the product can offer the find action for free. A paid search would need to assign a client and a service; somebody pays, somebody gets paid. A scheduled symmetric match does not. Every participant stands in the same relation to the cron, and the cron takes no money from anyone. The scheduled form is the only form compatible with the dana tradition the service orbits, in which the 10-day courses at dhamma.org are taught by authorized assistant teachers who are not paid and who do not charge.
The whole page, skimmable
- Search intent: what does "find an accountability partner" mean operationally.
- Common answer: a checklist of qualities and places to look.
- Product answer: a cron at
src/app/api/auto-match/route.ts, scheduled by vercel.json at*/30 * * * *. - Hard filter: pairs whose UTC-minute distance exceeds 60 are dropped, every tick.
- Sort: readyScore, bothOld, sessionMatch, smallest UTC diff, strictly lexicographic.
- Gates: 24h cool-off, 7-day retry, contact_count < 2.
- Your effort: one waitlist form, two minutes. The find work is the server's.
- Not for: readers who want a paid coach, curated qualities, or a search they can steer. A checklist-style product does those jobs. This one does not.
Two numbers, spring-animated
The cron interval is 0 minutes. The UTC distance ceiling on any viable pair is 0 minutes. Between those two integers sits the entire shape of the find action on this site.
Want to see what one tick of the cron looks like for your schedule?
Book a short call and we will walk through what the pool looks like at the next :00 or :30, given your timezone and your session time, and how it would sort around you.
Frequently asked questions
What does 'find an accountability partner' actually mean on this site?
It means being inserted into a waitlist_entries row, then waiting until the next tick of a cron. The cron is a GET handler at src/app/api/auto-match/route.ts. Its schedule is declared in vercel.json on the line '*/30 * * * *', which is every 30 minutes, on the hour and on the half. At each tick the handler loads every pending and ready row from the database, builds one session slot per morning sit and a second slot per evening sit for twice-a-day sitters, and iterates all unordered pairs of slots. For each pair it runs a circular UTC-minute distance calculation and drops the pair if the distance exceeds 60. What is left is the eligible pool for this tick. If your signup was in that pool and ended up on a matched row, 'finding' just happened to you. You did not do it. The scheduled job did.
Where is the actual filter in the code?
src/app/api/auto-match/route.ts, line 164. The line reads `if (diff > 60) continue;` where diff is the result of `timeDiff(sa.utcMinutes, sb.utcMinutes)` two lines earlier. timeDiff is defined on line 49 as `Math.min(Math.abs(a - b), 1440 - d)`, which is the circular distance in minutes between two points on a 24-hour clock. In plain terms, two sitters whose session times differ by up to one hour (measured the short way around the clock) are considered viable. Any pair further apart than that is silently discarded for this tick and will not be revisited until one or both of them changes their time or the eligible pool shifts.
What happens when no partner shows up in the current tick?
Nothing visible. The cron finishes, writes its admin summary email for me, and goes quiet. There is no notification to you that no viable pair was found, because the absence of a match is not really a fact about you. It is a fact about the global pool at that instant. You stay in status pending and remain a candidate for every subsequent tick. The system does not expire or downweight you for not being matched on a given run, and it does not keep asking you questions to improve your profile. The only thing that changes your shape in the pool is an update you make to your own form (timezone, session duration, sit time) or the contact_count incrementing because you were matched once and that match reached a terminal state.
What are the eligibility rules for 'being findable' at all?
Three cases, all visible at src/app/api/auto-match/route.ts lines 78 to 105. First, if your status is 'ready' you bypass the rest of this filter — motivated users go straight into the eligible pool. Otherwise, if your contact_count is 10 or more, you are skipped. Ten prior matches that did not stick is enough evidence for the system to stop trying, and at that point the operator (Matt) takes over manually or not at all. Second, if your contact_count is 0, you must have been signed up for longer than 24 hours before you enter the eligible pool. The cool-off period is there so you do not get paired during the first tick after you sign up, before any human has confirmed the signup is real. Third, if your contact_count is between 1 and 9, your previous match must have expired, ended, or been declined, and at least 7 days must have passed since that row reached its terminal status.
How are viable pairs sorted once the 60-minute filter passes?
Lines 183 to 189. Four keys, evaluated in this order. Key one, readyScore: if both partners have status 'ready' that pair gets a readyScore of 2, if one is ready it gets 1, if neither is it gets 0. Higher readyScore sorts first. Key two, bothOld: a boolean true if both are marked 'Yes' on is_old_student. True sorts before false. Key three, sessionMatch: a boolean true if both picked the same session_duration (30, 45, 60, 90, 120 minutes). Key four, smallest UTC diff: the tie-breaker, lowest first. The scorer does not invent weights. It is a strict lexicographic sort over four comparisons, and every comparison is a function of both candidates rather than a property of one.
Can two people be matched to each other twice by the system?
Not automatically. Line 241 calls getPriorMatchedIds for one side of every candidate pair and skips the pair if the two of them have ever been matched before. Line 160 also runs a blockedPairs guard that includes any match row whose status is not in the expired, declined, or ended set, or which had either side confirmed at any point. Together those two checks make the find action forward-only: each individual partnership is visited once. If a pair ghosts, both sides go back into the pool but will be paired with fresh candidates, never with each other again, unless a human overrides the matching on the admin page at /admin/matching.
How does a twice-a-day sitter fit into the slot model?
As two separate SessionSlots in the same tick. Lines 107 to 122 build a morning slot for every eligible person and add a second evening slot if their frequency is 'Twice a day' and the evening_utc field resolves to a number. The greedy picker at lines 192 to 203 treats the two slots independently and marks each one used via a composite key '<personId>:<session>'. The practical effect is that a twice-a-day sitter can pick up two different partners in the same tick, one for the morning sit and one for the evening sit, which also means the same person can appear in two matched rows with two different buddies running at two different local times.
Why does this page present finding as a scheduled job rather than a quality-of-person question?
Because the runtime does not know about qualities. The WaitlistEntry type, defined in src/lib/db.ts, holds timezone, frequency, session_duration, morning_time, evening_time, is_old_student, status, contact_count, and pass_count. None of those fields encodes motivation, discipline, personality fit, values alignment, or any of the categories the top guides spend their word count on. The matcher cannot filter on things the data model does not carry. Any page that promised to help you find a partner by optimizing for qualities would be lying about what the system here can see. The honest description is the one the code supports: overlapping time slots, similar practice traits, a finite pool, a fixed schedule.
What is the one number that captures how much work 'finding' costs you?
Two minutes, once, to fill out the waitlist form. After that the act of finding is entirely the cron's responsibility, on its own schedule, and your involvement in it is zero until the confirmation email lands in your inbox. You do not vet candidates. You do not interview them. You do not send messages. You do not review profiles. The product's explicit bet is that the friction cost of finding is 100% dominated by the form and 0% dominated by search, and that the search should be automated away so the surviving cost is just showing up to sit. A paid coach is a different product with different economics. This one is calibrated around 'the cron does the search for free, in the dana tradition the service inherits.'
What stays off this page and goes to an authorized teacher instead?
Anything operational about the practice itself. How to sit, how to work with a difficulty on the cushion, how to interpret a bodily experience, how to structure your daily practice, what to notice, how to handle doubt. Those questions belong at dhamma.org, inside a 10-day residential course, with an authorized assistant teacher in the S.N. Goenka tradition. This page and the matcher it describes handle logistics and peer pairing only. The scheduled job finds you a human whose slots overlap with yours. Everything about what happens once the two of you open the Meet URL at your sit time is the tradition's to guide, not this site's.
Comments (••)
Leave a comment to see what others are saying.Public and anonymous. No signup.