Finding, as survival of a filter sequence
Finding an accountability partner is surviving eight gates in one cron tick
Most writing on this topic treats "finding" as a reader-side sequence: shortlist candidates, check their qualities, book a kickoff, negotiate cadence. That framing assumes the reader can see people and choose. On this site, the reader never sees a candidate. The reader is a row, and "finding" is the state that row reaches when eight specific filter gates all let it through at the same tick of the cron. Six gates apply to the row alone, two apply to the pair. The entire pipeline lives in one file: src/app/api/auto-match/route.ts.
What the other guides leave out
The pages that already cover this topic are, almost uniformly, procedural advice for a reader who will assemble a partnership from scratch. Decide a goal. List qualities. Post on a community. Run a kickoff call. Negotiate cadence and check-in format. None of that advice is wrong, but none of it is an implementation. None of it describes a running system with a schedule, a data model, and a set of conditions a candidate pair either satisfies or does not.
On this site there is an implementation. It lives in one file and runs every thirty minutes. The file has a finite number of conditions that a row must pass to become findable, and a finite number of conditions that a pair must pass to become a match. The honest description of "finding an accountability partner" here is the description of those conditions. That is what this page is.
The gate sequence, in one horizontal read
Eight boxes. Six row-level, two pair-level. A row that stops at any box produces no match this tick; a row that clears the last box is a row the cron writes a matches entry for and sends an email about.
gate sequence (auto-match/route.ts)
SELECT
status + unsubscribe predicates
cc < 10
serial-ghoster cap
age > 24h
first-match cool-off
7d since expire
retry cool-off
toUtcTime
slot must resolve
blockedPairs
no prior confirmed/live pair
diff <= 60
circular UTC window
match
row written, email queued
Gates in bento form
Each card names the gate, the line or line range inside src/app/api/auto-match/route.ts, and the predicate. Every number and condition can be grepped in the repo on the date this page was published.
Gate 1, 2 — DB predicates
Line 74 of route.ts runs `status IN ('pending', 'ready') AND unsubscribed = false`. Every other row is invisible to the pipeline for the rest of the tick.
Gate 3 — contact-count cap
Line 84. A row with contact_count >= 10 is dropped with no further checks. Ten prior matches that did not stick is enough evidence for the cron to stop trying. Status 'ready' bypasses this gate.
Gate 4 — 24h cool-off
Lines 88 to 90. A row at contact_count = 0 must be older than DAY_MS = 24 * 60 * 60 * 1000 before it enters the eligible list. Fresh signups wait one day.
Gate 5 — 7d retry cool-off
Lines 91 to 102. A row at contact_count between 1 and 9 must have a prior match in status 'expired', 'declined', or 'ended' older than 7 days. Otherwise it stays out of the pool.
Gate 6 — UTC slot resolves
Lines 108 to 122. toUtcTime(morning_time, timezone) must return an HH:MM string. If it returns null, no slot is created, and the row cannot be paired this tick.
Gate 7 — no blockedPair
Line 160. The pair is dropped if the two IDs are in blockedPairs, which is every prior match that had any confirmation or is not in ('expired', 'declined', 'ended').
Gate 8 — 60-min circular window
Line 164. `if (diff > 60) continue;` where diff is the shortest distance on a 24-hour clock. 23:45 UTC and 00:20 UTC are 35 apart and pass.
Gates 1 through 5, in code
The first five gates all operate on a single row at a time. Two are SQL predicates on the initial SELECT. Three are JavaScript conditions inside the eligibility loop starting at line 80. The snippet below concatenates them verbatim, without reordering.
Gates 7 and 8, in code
These two gates do not run on the row alone. They run on a candidate pair of session slots. The enumeration that feeds them is a double loop over every pair of surviving slots, filtered by the two conditions below.
A simulated tick, gate by gate
What the cron log looks like if the counts were printed inline. The numbers below are illustrative of a typical tick shape; the shape itself (the sequence of gates, the proportion discarded by the 60-minute window) is the real thing.
One row, passing all eight gates
A candidate's checklist when the row is findable this tick. The sequence is the one the runtime walks, in order. A fail at any line short-circuits the rest.
sample findable row — tick at :30
- status = 'pending' — gate 1 passed
- unsubscribed = false — gate 2 passed
- contact_count = 0 — gate 3 passed (below cap of 10)
- created 2 days ago — gate 4 passed (> 24h)
- no prior expire on file — gate 5 not applicable at cc = 0
- toUtcTime('06:00', 'America/Chicago') = '12:00' — gate 6 passed
- no blockedPair against any survivor — gate 7 passed
- closest surviving slot is 45 min away — gate 8 passed
Same row, four days earlier, failing at gate 5
Four days before the tick above, the same row had a recent expire on file, and gate 5 (the 7-day retry cool-off) was still active. The runtime never reaches gate 6 or beyond, because eligibility is short-circuited. The row is invisible to the matcher until the 7-day mark elapses.
sample non-findable row — 4 days earlier
- status = 'pending' — gate 1 passed
- unsubscribed = false — gate 2 passed
- contact_count = 1 — gate 3 passed (below cap of 10)
- last expire was 3 days ago — gate 5 failed (7d cool-off still active)
- (pipeline does not run gates 6 through 8; row is already out)
Eight gates, one timeline
Each step is an operation inside the cron, in the order the runtime runs it. The file name and line range for every one is named under the gates-in-bento section above.
Gate 1 — status is eligible
The SELECT on line 74 reads only rows whose status is 'pending' or 'ready'. A row at 'matched', 'contacted', 'engaged', 'paused', or any custom admin state is not in the candidate list at all.
Gate 2 — not unsubscribed
The same SELECT also requires unsubscribed = false. Unsubscribes are soft-deletes; the row is preserved for audit but made invisible to the matcher.
Gate 3 — contact_count below 10
Line 84 drops any row whose contact_count has reached 10. Ten prior matches that did not stick is the cron's evidence that automated matching is not going to work for this row. Manual operator override at /admin/matching can still pair it. Status 'ready' bypasses the cap entirely.
Gate 4 — first-match cool-off of 24 hours
Lines 88 to 90 compute now - createdAt in milliseconds and compare against DAY_MS. A row at contact_count = 0 that is less than one day old never enters the eligible pool. The delay is deliberate: it gives operators time to observe a suspicious signup before it reaches a real human.
Gate 5 — retry cool-off of 7 days
Lines 91 to 102 read the row's most recent match in status 'expired', 'declined', or 'ended' and require that row to be older than 7 * DAY_MS. No prior terminal match means no retry; a recent one means wait. The 7-day floor is the operator's calibration for how long a fresh-start feels fresh.
Gate 6 — UTC slot resolves
Lines 108 to 122 call toUtcTime(morning_time, timezone) and push a SessionSlot only if the result parses into a number. A timezone string the converter does not recognize yields null and the row contributes zero slots to the pair enumeration.
Gate 7 — no blocked pair between these two
Line 160 checks the sorted concatenation of the two person IDs against blockedPairs, a set built from every match row that was ever confirmed on either side or is not in a terminal ('expired', 'declined', 'ended') state. The cron never re-proposes a partnership that got any positive signal the first time.
Gate 8 — pair inside the 60-minute circular window
Line 164 reads `if (diff > 60) continue;`, where diff is the shortest distance between the two slot's UTC minutes on a 24-hour clock. The circular metric lets 23:45 UTC pair with 00:20 UTC. A pair beyond 60 minutes is silently discarded for this tick.
The same tick as a sequence diagram
Three actors: the cron handler, the Postgres database, and the match writer. The arrows show what the handler asks for and how the gates become self-calls on the handler after the data comes back.
tick lifecycle, gate-annotated
The constants that define "findable"
Four numbers carry the entire gate sequence. Everything else is structure.
All four come out of the same file, plus the schedule on vercel.json. None are tunable at runtime. Changing any of them requires a deploy.
Every gate, as a chip
A scrolling reference. Every token below points to a specific condition or constant in the source tree.
Common advice about finding vs. gate-filtered finding on this site
| Feature | Common advice (reader-driven) | vipassana.cool (gate-filtered) |
|---|---|---|
| Shape of the work | Reader-side action: shortlist, outreach, interview, commit. | Runtime-side filter: row passes or fails 8 gates in one tick. |
| What disqualifies a candidate | Subjective: 'bad chemistry', 'unreliable', 'wrong goals'. | Eight named predicates at specific line numbers. No judgement calls. |
| Latency between effort and outcome | Days to weeks; depends on how many people you can interview. | At most 30 minutes per tick; rows are re-evaluated every :00 and :30. |
| Authority to decide | The reader chooses who passes. | The cron chooses; the reader cannot see the pool. |
| Explicit retry policy | Implicit: 'keep looking', no stop condition. | At most 10 tries per row (contact_count cap), 7-day wait between each. |
| How 'findable' is defined | 'A good match for me' — evaluator-dependent. | Survives eight filter gates this tick. Boolean. Tick-local. |
| Cost | Coach retainers, paid communities, or DIY time. | Free. Cron runs on the operator's Vercel account. |
“Six row-level, two pair-level. All eight live in src/app/api/auto-match/route.ts. A row is 'findable' only when the cron walks it through all eight without hitting a continue or a null.”
src/app/api/auto-match/route.ts, lines 74, 81, 88-90, 91-101, 108-122, 160, 164
Why naming the gates matters
The common advice on this topic is not written against a pipeline. It is written as if a reader could form a pool, inspect it, and decide. If that model were true for this site, listing qualities to look for would be the right shape of a page. It is not true for this site. The reader never sees the pool, and the pool never sees the reader as a reader; the reader is a row, observed only through the fields the data model carries.
Naming the gates makes the service honest. It says out loud what the matcher can and cannot do. The matcher cannot filter on motivation, discipline, or affinity, because those are not fields on WaitlistEntry. It can filter on time-of-sit, timezone, prior contact history, and whether two rows have already been tried together. So that is what it filters on, and that is what this page describes. The reader who wants anything beyond the eight predicates is looking for a different product.
Anything about how to practice (how to sit, what to notice, how to work with a difficulty on the cushion) is outside the matcher and outside this page. Those questions go to an authorized assistant teacher at a 10-day residential course in the S.N. Goenka tradition, at dhamma.org. This site handles peer-pairing logistics after a reader has completed such a course, and the page you are reading describes the scheduler that does that pairing.
The whole argument, skimmable
- The question: what does "finding an accountability partner" resolve to on this site.
- Common answer: a reader- side sequence of shortlisting, interviewing, and negotiating.
- This site's answer: a row that cleared eight predicates in one cron tick.
- The gates: status, unsubscribe, contact cap, 24h cool-off, 7d retry, UTC slot, blockedPairs, 60-minute circular window.
- Where they live:
src/app/api/auto-match/route.ts, lines 74, 81, 88-90, 91-101, 108-122, 160, 164. - How often it runs: every thirty minutes, on :00 and :30.
- Not for: readers who want to inspect or pick candidates, or who want guarantees on matching latency.
Two numbers, spring-animated
The pipeline has 0 filter gates between a fresh signup and a written match. It re-evaluates every row in the pool every 0 minutes. A row is findable at a given tick only when all eight gates let it through at the same moment. Every other shape of "finding" is a different product.
Want to walk the gate sequence for your own row?
Book a short call and we will trace what each of the eight gates would return for a signup at your timezone and session times.
Frequently asked questions
What does 'finding an accountability partner' actually resolve to on this site?
It resolves to a waitlist_entries row reaching the bottom of a specific function without being filtered out. The function is the GET handler at src/app/api/auto-match/route.ts, lines 54 to 476. On each tick the handler loads rows, filters them, builds session slots, enumerates pairs, drops ineligible pairs, sorts, and writes matches. 'Finding' is the successful traversal of that pipeline for your row, at the tick where a compatible other row happens to survive the same filters. It is not a search you perform. It is a survival state your row is in, measured once every thirty minutes by the cron declared in vercel.json.
How many filter gates stand between a signup and a match, and where are they?
Eight. The first two are in the SQL of line 74: status must be in ('pending', 'ready'), and unsubscribed must be false. The third is the contact_count >= 10 check at line 84, which removes rows that have already been matched ten times without sticking; status 'ready' bypasses this. The fourth is the 24-hour cool-off at lines 88 to 90, which holds a fresh signup out of the pool for one day. The fifth is the 7-day retry cool-off at lines 91 to 102 for rows at contact_count between 1 and 9. The sixth is the UTC slot build at lines 108 to 122, which silently drops a row if toUtcTime cannot resolve its morning_time plus timezone into an HH:MM string. The seventh is the blockedPairs guard at line 160, which drops the pair when the two people have ever been in a confirmed or still-active match together. The eighth is the 60-minute hard filter at line 164: if timeDiff(a, b) is greater than 60 UTC minutes on the circular clock, the pair is discarded.
Why count the database predicates as gates too?
Because nothing outside the rows those predicates return can be 'found'. The SELECT on line 74 is the first thing the runtime does, and it picks a strict subset: status IN ('pending', 'ready'), unsubscribed = false. Rows with status 'matched', 'contacted', 'engaged', 'paused', or any other value are invisible to the cron. Rows with unsubscribed = true are invisible. Those are not cosmetic filters. They are load-bearing: the set the handler sees is the set the entire matching pipeline operates on. Counting them as gates one and two is the honest way to describe what it takes to be a findable row.
What does the 60-minute UTC window actually look like in code?
Line 49 defines timeDiff as `const d = Math.abs(a - b); return Math.min(d, 1440 - d);` where 1440 is the number of minutes in a day. That returns the shortest circular distance between two clock minutes. Line 164 reads `if (diff > 60) continue;`, which skips the pair. The practical effect is that 06:00 UTC and 07:00 UTC are 60 apart (pass), 06:00 and 07:01 are 61 apart (fail), and 23:30 and 00:15 are 45 apart (pass, because the distance wraps around midnight). This is why the page frames the window as circular rather than linear: a sitter at 23:45 UTC is still findable with a sitter at 00:30 UTC.
What is the prior-pair guard doing that prior-match history does not already cover?
The blockedPairs set at lines 129 to 138 is built from every row in the matches table where at least one side confirmed the match, or the status is not in ('expired', 'declined', 'ended'). Then line 160 drops any candidate pair whose two IDs are in that set. The reason this is separate from 'same two people were matched before' is that it treats cold-bilateral-decline matches as unblocking: if neither side confirmed and the match ended, the pair can be retried against fresh conditions, but if either side confirmed at any point, or if the row is still live in any state, the same two will never be paired again automatically. The cron never reuses a partnership that got any real signal.
What does surviving all eight gates at the same tick feel like from the reader's side?
An email. The email is the only evidence the pipeline runs at all, because no UI shows the gate state in real time. If both rows happened to be 'ready' at the same tick, the intro email with a Google Meet link arrives. If one or both were 'pending' instead, a confirmation email arrives asking for bilateral opt-in before the Meet is created. Every other tick where your row did not survive all eight gates produces nothing for you. The cron writes its admin summary to the operator's inbox and goes quiet. Absence of an email is not a signal about you; it is a signal that the gates did not all align this tick.
Why call finding the 'inverse' of a gate sequence rather than the output of one?
Because most of the gates are exclusion predicates. contact_count >= 10 removes you. 24-hour cool-off removes you. Unsubscribed removes you. blockedPairs removes the pair. The predicate form is not 'you are findable because X' but 'you are findable unless X'. Writing it that way matches the code: if you list the reasons a row does not survive a tick, you have almost completely described the matcher. Frame the same thing positively and you would write nine conjoined conditions, which reads worse and describes the same behavior.
What happens to a row that clears gates 1 through 6 but no partner exists inside the 60-minute window this tick?
It stays pending and tries again on the next tick, 30 minutes later. The row is eligible, has a UTC slot, and has no blockedPair against any survivor in the current pool; it is just that nobody else's UTC minute is within 60 of yours at this instant. A twice-a-day sitter contributes two slots and so has two separate chances per tick. A new signup landing five minutes later could shift the pool inside the next tick. The row has no TTL and no escalating penalty. It is either matched on a future tick, or the reader updates their times and changes which distances are in range.
Can I read the current gate results for my own row?
The admin page at /admin/matching surfaces the relevant fields and flow states for operators, but the raw cron has no per-row report. A transparent proxy is scripts/best-pending-pairs.mjs, which applies most of the same gates outside the request loop and prints every non-overlapping viable pair it finds in the pool at the moment it runs. Running that script on 2026-04-23 in the production repo produced 23 viable pairs. That script is good enough to show what 'your row survived the gates this tick' looks like without instrumenting the cron itself.
Does this page teach meditation technique?
No. Nothing on this page is technique instruction. The S.N. Goenka tradition reserves transmission of the technique for authorized assistant teachers inside 10-day residential courses at dhamma.org. This site handles peer-pairing logistics only, and this page describes the filter gates in its scheduler. Any question about how to sit, how to work with a sensation, how to handle difficulty on the cushion, or how to structure daily practice goes to an authorized assistant teacher at a 10-day course, not here.
Comments (••)
Leave a comment to see what others are saying.Public and anonymous. No signup.