# Vipassana Matching

> Full Practice Buddy matching workflow for vipassana.cool — check submissions, research candidates, identify matches, send intro emails, and manage the waitlist

- Skill: `mediar-ai/vipassana-matching` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add mediar-ai/vipassana-matching`
- Raw SKILL.md: https://api.skillmd.com/api/skills/mediar-ai/vipassana-matching/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Research & Search
- Author: mediar-ai (https://skillmd.com/u/mediar-ai)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/mediar-ai/vipassana-matching

---


# Vipassana Practice Buddy Matching Skill

Trigger this skill when the user asks to:
- Do matching / run matching
- Check submissions or the waitlist
- Review who to match
- Send intro emails
- Manage the Practice Buddy waitlist
- Check inbound emails or replies from matched pairs

---

## Project Context

| Item | Value |
|------|-------|
| Site | vipassana.cool |
| Feature | Practice Buddy |
| Project root | `<project-root>` |
| DB | Neon Postgres (serverless) |
| Credentials | `.env.local` (see `DATABASE_URL`, `RESEND_API_KEY`) |
| Email from | `Matt from Vipassana.cool <matt@vipassana.cool>` |
| Admin dashboard | `https://vipassana.cool/admin/matching` |

> Matching is currently **manual**: a human reviews and decides who to match. The DB and email infrastructure is fully automated once the decision is made.

---

## Database Schema

### `waitlist_entries`

| Column | Type | Notes |
|--------|------|-------|
| `id` | uuid | Primary key |
| `email` | text | Unique |
| `name` | text | Full name |
| `status` | text | `pending` or `matched` |
| `timezone` | text | e.g. `America/New_York` |
| `city` | text | |
| `frequency` | text | e.g. `Once daily`, `Twice daily` |
| `morning_time` | text | e.g. `6:00 AM` |
| `evening_time` | text | e.g. `8:00 PM` |
| `session_duration` | text | e.g. `1 hour`, `30 minutes` |
| `is_old_student` | text | `Yes` / `No` |
| `is_goenka_tradition` | text | `Yes` / `No` |
| `has_maintained_practice` | text | e.g. `Regularly`, `Inconsistently` |
| `practice_length` | text | e.g. `3 years`, `6 months` |
| `days` | text | JSON array of days |
| `requested_match_id` | uuid | If they requested a specific person |
| `research_notes` | text | Claude's research findings |
| `created_at` | text | ISO timestamp |
| `updated_at` | text | ISO timestamp |

### `matches`

| Column | Type | Notes |
|--------|------|-------|
| `id` | uuid | Primary key |
| `person_a_id` | uuid | FK to waitlist_entries |
| `person_b_id` | uuid | FK to waitlist_entries |
| `status` | text | `pending`, `active`, or `ended` |
| `notes` | text | Optional notes |
| `created_at` | text | ISO timestamp |

### `vipassana_emails`

| Column | Type | Notes |
|--------|------|-------|
| `id` | uuid | Primary key |
| `resend_id` | text | Resend message ID |
| `direction` | text | `inbound` or `outbound` |
| `from_email` | text | |
| `to_email` | text | Comma-separated for outbound |
| `subject` | text | |
| `body_text` | text | Plain text body |
| `body_html` | text | HTML body |
| `status` | text | `sent`, `failed`, etc. |
| `created_at` | text | ISO timestamp |

---

## Node.js Query Patterns

Use the Neon serverless driver. Write queries to a temp script and run with `node`.

### Setup a query script

```bash
cat > /tmp/vipassana-query.mjs << 'EOF'
import { neon } from "@neondatabase/serverless";

const sql = neon(process.env.DATABASE_URL);

// Your query here
const rows = await sql`SELECT * FROM waitlist_entries WHERE status = 'pending' ORDER BY created_at DESC`;
console.log(JSON.stringify(rows, null, 2));
EOF

cd <project-root> && node --env-file=.env.local /tmp/vipassana-query.mjs
```

---

## Full Matching Workflow

### Step 1 - Check Pending Submissions

Query all pending waitlist entries and display their matching-relevant fields:

```javascript
const pending = await sql`
  SELECT
    id, name, email, timezone, city,
    frequency, session_duration,
    morning_time, evening_time,
    is_old_student, has_maintained_practice,
    practice_length, requested_match_id,
    research_notes, created_at
  FROM waitlist_entries
  WHERE status = 'pending'
  ORDER BY created_at ASC
`;
```

Display a human-readable summary: name, email, city/timezone, frequency, session duration, old student status, practice length, requested match.

---

### Step 2 - Check Existing Matches

See who is already matched to avoid double-matching:

```javascript
const matches = await sql`
  SELECT
    m.id, m.status, m.created_at,
    a.name AS person_a_name, a.email AS person_a_email,
    b.name AS person_b_name, b.email AS person_b_email
  FROM matches m
  JOIN waitlist_entries a ON a.id = m.person_a_id
  JOIN waitlist_entries b ON b.id = m.person_b_id
  ORDER BY m.created_at DESC
`;
```

---

### Step 3 - Check Inbound Emails

Look for replies from matched pairs:

```javascript
const inbound = await sql`
  SELECT id, from_email, to_email, subject, body_text, created_at
  FROM vipassana_emails
  WHERE direction = 'inbound'
  ORDER BY created_at DESC
  LIMIT 20
`;
```

If someone replied only to you (not reply-all), see Step 9 for the forward flow.

---

### Step 4 - Research Candidates

For each pending candidate, web search their name + email domain / city to find public info (LinkedIn, personal site, occupation, background). This makes the intro email more personal.

```
WebSearch: "Jane Smith" site:linkedin.com OR "Jane Smith" vipassana
```

Store findings in the `research_notes` column:

```javascript
await sql`
  UPDATE waitlist_entries
  SET research_notes = ${notes}, updated_at = ${new Date().toISOString()}
  WHERE id = ${id}
`;
```

---

### Step 5 - Identify Good Matches

Evaluate pending pairs against these criteria (in priority order):

| Criterion | Rule |
|-----------|------|
| Requested match | `requested_match_id` is set - always match these first |
| Timezone | Same timezone or +-3 hours offset preferred |
| Frequency | Same (Once daily / Twice daily) required |
| Session duration | Within +-15 minutes preferred |
| Old student | Both `is_old_student = 'Yes'` preferred |
| Morning time | Within +-2 hours preferred |

Hard to match - leave as pending, wait for future signups:
- Very short sessions (15 min) with no other 15-min sitters
- Unique/exotic timezones with no nearby match
- Non-old-students when all remaining pending are old students

---

### Step 6 - Create Match in DB

Once you've decided on a pair:

```javascript
const matchId = crypto.randomUUID();
const now = new Date().toISOString();

await sql`
  INSERT INTO matches (id, person_a_id, person_b_id, status, created_at)
  VALUES (${matchId}, ${personAId}, ${personBId}, 'pending', ${now})
`;

await sql`UPDATE waitlist_entries SET status = 'matched', updated_at = ${now} WHERE id = ${personAId}`;
await sql`UPDATE waitlist_entries SET status = 'matched', updated_at = ${now} WHERE id = ${personBId}`;
```

Or use the admin API:

```bash
curl -X POST https://vipassana.cool/api/admin/matches \
  -H "Authorization: Bearer $ADMIN_SECRET" \
  -H "Content-Type: application/json" \
  -d '{"personAId": "<uuid>", "personBId": "<uuid>", "sendEmail": false}'
```

Use `"sendEmail": false` if you want to send a custom email manually (Step 7).

---

### Step 7 - Send Intro Email via Resend

**Critical**: Set `reply_to` to both email addresses so "Reply All" reaches both people.

#### Resend curl pattern

```bash
curl -X POST https://api.resend.com/emails \
  -H "Authorization: Bearer $RESEND_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "Matt from Vipassana.cool <matt@vipassana.cool>",
    "to": ["person_a@example.com", "person_b@example.com"],
    "reply_to": ["person_a@example.com", "person_b@example.com"],
    "subject": "Your Practice Buddy match is here",
    "html": "<html>...</html>"
  }'
```

#### Email body guidelines

- Address both by first name: "Hi [Name A] and [Name B],"
- Mention shared traits (same timezone, same frequency, similar session length, both old students)
- Include city + practice history for each person (personalized from DB data)
- Use research notes to add a personal touch (occupation, mutual interest)
- Instruct: "Just reply all to this email to introduce yourselves"
- End with: "Be happy, Matt"
- Add P.S.: "I do a brief web search on name and email to write a more personal intro. Hope that's okay."

#### Rate limit

Resend allows 2 requests/sec max. When sending multiple emails back-to-back:

```bash
curl ... # pair 1
sleep 2
curl ... # pair 2
```

---

### Step 8 - Log Email to DB

After a successful Resend response, log the outbound email:

```javascript
const resendId = emailResult.data?.id || null;

await sql`
  INSERT INTO vipassana_emails (
    resend_id, direction, from_email, to_email,
    subject, body_html, status, created_at
  )
  VALUES (
    ${resendId}, 'outbound',
    'Matt from Vipassana.cool <matt@vipassana.cool>',
    ${[personA.email, personB.email].join(", ")},
    'Your Practice Buddy match is here',
    ${htmlBody}, 'sent', ${new Date().toISOString()}
  )
`;
```

---

### Step 9 - Handle Reply-All Failures

If someone replies to just you (not both), forward to bring both back into the thread:

```bash
curl -X POST https://api.resend.com/emails \
  -H "Authorization: Bearer $RESEND_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "Matt from Vipassana.cool <matt@vipassana.cool>",
    "to": ["person_a@example.com", "person_b@example.com"],
    "reply_to": ["person_a@example.com", "person_b@example.com"],
    "subject": "Re: Your Practice Buddy match is here",
    "html": "<p>Hey both - just looping you back together! Please use Reply All so you can stay in touch with each other directly.</p><p>Be happy,<br>Matt</p>"
  }'
```

---

## Admin API Reference

All endpoints require `Authorization: Bearer $ADMIN_SECRET` header.

| Method | Endpoint | Purpose |
|--------|----------|---------|
| GET | `/api/admin/waitlist` | All waitlist entries |
| PATCH | `/api/admin/waitlist` | Update `research_notes` for an entry |
| GET | `/api/admin/matches` | All matches with person details |
| POST | `/api/admin/matches` | Create a match (optionally send email) |
| PATCH | `/api/admin/matches/{id}` | Update match status |
| GET | `/api/admin/emails` | All emails (inbound + outbound) |

---

## Quick Start - Full Matching Session

1. Query pending entries (Step 1)
2. Query existing matches (Step 2)
3. Check inbound emails (Step 3)
4. Research any unresearched pending candidates (Step 4)
5. Present match recommendations to user with rationale
6. **Wait for user confirmation** before creating any match
7. For each confirmed pair:
   a. Create match in DB (Step 6) - use `sendEmail: false`
   b. Compose personalized intro email using research notes
   c. Send via Resend with `reply_to` set to both (Step 7) - remember `sleep 2`
   d. Log to `vipassana_emails` (Step 8)
8. Report summary: X matches created, X pending, X inbound replies reviewed

