# Frenday launch runbook — the tweet week (2026-09-17)

> Renamed **เฟรนเดย์ / Frenday** on 2026-09-18, public host **`frenday.xyz`** (a temporary TLD; `paiduay.6326638.xyz` 301s to it). "Paiduay" survives below only as the internal identifier — the folder, the tenant slug, `PAIDUAY_*` env names, routes — and in the dated "found live" notes, which record what was true on their date.

**Scope.** One operator (James), one shared VPS, LINE as the channel, no company. Wave 1 is built (spec §9.58, `apps/paiduay/docs/MVP-BRIEF.md`); this is now to the first ฿499. Facts checked live at 18:20 ICT today.

## 1. Day 0 — before the tweet

**Found live:** `paiduay.6326638.xyz/c/new`, `/report`, `/me` return **404**. The image was rebuilt at 18:18 ICT but the container dates from 09-10 — `docker compose up -d` never ran, so Wave 1 is not live. Server `.env` has `pk_test`/`sk_test` (Clerk **dev** instance), `PAIDUAY_SUPPORT_EMAIL` set, `PAIDUAY_PROMPTPAY_ID` **unset**, no prefilled login password.

| # | Item | Do | Proof |
|---|---|---|---|
| 0 | Deploy Wave 1 | `ssh james@92.118.206.89 'cd ~/apps/ai-new-business/repo && git pull && deploy/deploy.sh'` (build → migrate 0004 outbox → `up -d`) | `/c/new` → 302 to `/sign-in`; `/report` → 200 |
| 1 | Domain — **done 2026-09-18: `frenday.xyz`** | Taken and live: Cloudflare zone, `A @ → 92.118.206.89`, Caddy block with `secure_headers` (`deploy/Caddyfile`), `NEXT_PUBLIC_SITE_URL` + `PUBLIC_ORIGIN_SUFFIX` (`deploy/compose.yaml`), `paiduay.6326638.xyz` 301s here — commit `ddd796a`. `.xyz` is a **temporary** TLD: which name James keeps (`frenday.co`, `frenday.app`, or another) is still open, and the move costs a second Clerk domain migration, so decide it before item 2. Path for that move = `docs/paiduay/plans/public-flip.md` §2 steps 1–6. | phone on cellular opens `https://frenday.xyz/c/mint`; `https://paiduay.6326638.xyz/` → 301 |
| 2 | Clerk production instance | `apps/paiduay/docs/CLERK-SETUP.md` steps 1–9. It binds to the domain (CNAMEs `clerk.`, `accounts.`, `clkmail.`) — that is why 1 comes first. `pk_live`/`sk_live` into the server `.env` **before** `deploy.sh` (the publishable key is inlined at build). Sign-ups open, phone off, session 30/90 days. | `/sign-up` shows no "Development mode" banner |
| 3 | LINE as a sign-in button | Clerk → Social connections → LINE. Dev credentials work today (consent screen says "Clerk"); for the real thing a LINE Login channel named เฟรนเดย์ (the consent screen prints it), callback pasted into Clerk. Email scope needs LINE's permission form — leave email optional for social. | LINE button first on `/sign-in` |
| 4 | `PAIDUAY_PROMPTPAY_ID` | Your personal PromptPay (phone or citizen id). Until set, `/c/<handle>/pro` prints "ยังไม่เปิดรับชำระ" and nobody can pay. | QR renders; your bank app shows ฿499 and **your legal name** (§5) |
| 5 | Support email | Already set on the server; it prints on `/policy` and in the receptionist grounding (`lib/manifest.ts`). Make sure it is an inbox you open daily. | send yourself one |
| 6 | Cast | Pending in `apps/paiduay/lib/screens.ts`: Martin (editor), Troy (done), Todd (`/me`), Skinner (`/sign-in`), Krabappel (`/sign-up`), Itchy (`/report`), Kirk (`/pro`), Wiggum (`/admin/pro`). Say yes or rename. | — |
| 7 | Whole-flow test on your own phone, on cellular | real-email sign-up → `/c/new` → handle → camera-roll photo (iPhone: confirm a HEIC-origin photo arrives as JPEG; raw HEIC is refused) → line, price, LINE → publish → tap LINE → `/kit` → `/pro` scan ฿499 to yourself, slip → `/admin/pro` approve → night template unlocks → `/report` → buzz → `/admin/report` hide → 404 → `/admin/card` → published. | every buzz arrives; nothing prints "not open" |
| 8 | Rollback in one line | Fastest: in `deploy/Caddyfile` paiduay block `respond "ปิดชั่วคราว" 503` + `docker compose up -d --force-recreate caddy`. DNS: PATCH the apex and wildcard records of `6326638.xyz` to `100.71.64.33` (the value is in each record's Cloudflare comment) — tailnet-only within TTL 300. | — |

**What a temporary host still costs.** `frenday.xyz` retires the worst of it — `6326638.xyz` was a number domain that "reads as a scam in a cold DM" (the plans index), and LINE and X printed it under her photo. What remains is that `.xyz` is not the name James intends to keep: a Clerk production instance binds to the host, so moving means a second domain migration and a second round of DNS; and every seller's bio link breaks the day you move (a 301 helps the web, not the bios). Tweet on `frenday.xyz` if you like, but stay on the dev Clerk instance until the keeper name is bought (§7 risk 3) — never a production instance on a temporary host.

## 2. The tweet

Register: James speaking, ผม/ครับ, the sellers' own words (รับงานเพื่อนเที่ยว, ทักไลน์). Honest by construction: free, no deposit ever, her own photo, LINE is the button. No urgency, no counts. `{url}` = the public origin (host from Day 0 item 1). All ≤ 280 characters with the temp host filled in.

**A — plain**
```
ถึงคนที่รับงานเพื่อนเที่ยวทุกคนครับ
ผมทำเว็บให้คุณมีหน้าของตัวเอง รูปของคุณ ราคา กติกา ปุ่ม LINE ที่ทักถึงคุณตรง ๆ
ทำเองบนมือถือ 3 นาที ฟรี ไม่มีมัดจำ ไม่หักอะไร
{url}
```

**B — no middleman**
```
รับงานเพื่อนเที่ยวอยู่ใช่ไหมครับ
ทำหน้าของตัวเองได้เอง รูปจริงของคุณ ราคา กติกา แล้วปุ่ม LINE ปุ่มเดียว
ฟรี ไม่ต้องโอนอะไร ไม่มีคนกลาง ลูกค้าทักคุณ จ่ายคุณเอง ลบเมื่อไหร่ก็ได้
{url}
```

**C — the repeat-typing pain**
```
โพสต์รับงานเพื่อนเที่ยวแล้วต้องพิมพ์ราคากับกติกาซ้ำทุกโพสต์ไหมครับ
ผมทำเว็บให้คุณมีหน้าเดียวที่มีครบ พร้อมรูปสำหรับโพสต์ทุกขนาด
รูปของคุณเอง ฟรี ไม่มีมัดจำ ไม่หักอะไร
{url}
```

**EN**
```
To Thai freelancers who take friend-for-hire bookings: I built a small site where you make your own page in three minutes. Your own photo, your price, your rules, one LINE button that reaches you. Free. No deposit, ever. I take nothing.
{url}
```

**Pinned reply** (the demo card; demo people are labelled on the card itself)
```
หน้าตัวอย่างครับ คนในรูปเป็นตัวอย่าง ไม่ใช่คนจริง
{url}/c/mint
ทำของตัวเอง {url}/c/new
ติดตรงไหน DM ผมได้เลย
```
```
A sample page. The person in the photo is a demo, not a real seller.
{url}/c/mint
Make yours at {url}/c/new
Stuck? DM me.
```

## 3. The DM playbook

A seller replies or DMs. Three messages, then the three answers. Longer cold-outreach scripts (`cold`, `deliver`, `followup`, `scam`) live in `docs/paiduay/content/card-captions.md` — the `scam` one is the source of the answer below.

**1 · hello**
```
สวัสดีครับ ขอบคุณที่ทักมา ผมชื่อ James เป็นคนทำเว็บนี้เองครับ
```
```
Hi, thanks for writing. I'm James, I build the site myself.
```

**2 · here is how**
```
เข้า {url}/c/new สมัครด้วยอีเมลหรือ LINE เลือกชื่อลิงก์ อัปรูปของคุณ ใส่ราคา กติกา แล้ว LINE ID
กดเผยแพร่ จะได้ลิงก์กับรูปสำหรับโพสต์ทุกขนาด ใช้เวลาประมาณ 3 นาทีครับ
```
```
Go to {url}/c/new, sign up with email or LINE, pick your link name, upload your photo, add your price, rules and LINE ID.
Tap publish and you get your link plus post images in every size. About three minutes.
```

**3 · here is your link** (when she sends the handle)
```
หน้าของคุณอยู่ที่ {url}/c/{handle} รูปสำหรับโพสต์อยู่ที่ {url}/c/{handle}/kit
ลองกดปุ่ม LINE บนหน้าดูก่อนโพสต์นะครับ อะไรไม่ตรงแก้เองได้ที่ {url}/c/{handle}/edit
```
```
Your page is at {url}/c/{handle}; post images at {url}/c/{handle}/kit.
Tap the LINE button once before you post. Anything off, edit it yourself at {url}/c/{handle}/edit.
```

**"Is this a scam?"**
```
ถามแบบนี้ถูกแล้วครับ
เว็บนี้คือหน้าเดียว มีรูปคุณ ราคา กติกา ปุ่ม LINE ที่ไปหาคุณตรง ๆ แค่นั้น
ไม่ขอรหัสผ่านไลน์ ไม่ขอเลขบัญชี ไม่ขอให้โอนอะไร ผมไม่ได้อยู่ระหว่างคุณกับลูกค้า
อยากลบเมื่อไหร่ บอกคำเดียว ลบให้ทันที
```
```
You should ask.
It is one page: your photo, price, rules, a LINE button that goes straight to you. That is all.
I never ask for your LINE password, a bank account or a transfer. I am not between you and your customers.
Want it gone? Say so and it is gone.
```

**"That page is not me"**
```
ขอบคุณที่บอกครับ ผมซ่อนหน้านั้นก่อนเลยภายใน 24 ชม. แล้วค่อยตรวจ
ช่วยส่งข้อความนี้จากบัญชีที่หน้านั้นลิงก์ไป หรือแจ้งที่ {url}/report ก็ได้ครับ
```
```
Thank you for telling me. I hide that page first, within 24 hours, and check afterwards.
Please send this from the account the page links to, or use {url}/report.
```

**"Can I use my AI image?"**
```
ใช้ไม่ได้ครับ หน้าบนเฟรนเดย์ต้องเป็นรูปจริงของคุณเท่านั้น ระบบไม่แต่งหน้าและไม่สร้างรูปให้
ลูกค้าจ่ายเพราะเชื่อว่าเจอคนในรูป ถ้ารูปไม่ใช่คุณ ทุกอย่างพังตั้งแต่ทักครั้งแรก
```
```
No. A Frenday page carries your real photo only; nothing retouches the face or generates one.
Customers pay because they trust they will meet the person in the photo. If it is not you, it breaks at the first message.
```

## 4. The operator's daily loop — 15 minutes

| Minute | Where | Do |
|---|---|---|
| 0–5 | `/admin/report` (Better Auth login) | Open reports, newest first. **`not_me` → Hide first** (button on the report; card `hidden` → 404, kit and OG with it), ask the claimant to prove the account, restore from `/admin/card` when the original owner answers. `scam`/`conduct` → read, ask if a contact was left, close. SLA printed on `/report`: read within one working day Mon–Fri 09–18, "not me" hidden within 24 h — keep it or change `content/copy-safety.ts`. |
| 5–10 | `/admin/pro` (Wiggum) | Each slip: **amount ฿499.00**, **date** after the request time, **receiver** = your name. Approve → `tier: pro`, `proUntil` = +12 months, she gets the `pro.approved` buzz path and the night templates unlock. Decline needs a reason she reads: "amount is ฿49 not ฿499" · "slip date is before the request" · "receiver is not Frenday" · "image unreadable, send again" · "same slip sent twice". She can resend. |
| 10–13 | Your phone (n8n) | Buzzes today: `card.published` (open the card, read it — this is the content review), `report.created`, `pro.requested`, `pro.approved`, and the meetup request/accept/decline/cancel events. Payloads carry handle and first name, never a customer contact. |
| 13–15 | `/admin/card` | A card that crosses the line (`docs/paiduay/plans/trust-safety.md` threat 4) — แฟนเช่า/ฟีลแฟน/GFE, after 21:00, โรงแรม/คอนโด/ห้องส่วนตัว, 18+, นวด — open the record → transition **hidden**. No second chance: Prostitution Act §7 exposure is ours if we knowingly host it. There is no delete; hidden is removal. One LINE line to her if she left an id: "หน้าของคุณถูกซ่อนเพราะข้อความผิดกติกา". |

## 5. Money handling

**PromptPay realities.** The QR is your personal PromptPay with ฿499 pinned; the slip is her proof and yours. Her bank app shows the **receiver's registered name** — your legal name, not "เฟรนเดย์". `content/copy-pro.ts` prints "พร้อมเพย์ เฟรนเดย์" under the QR; a QR with no name behind it is a scam tell (CONVERSION-RESEARCH §2), so add "ชื่อผู้รับที่แสดงในแอปธนาคาร: <your name>" before the first slip (small copy work order). Receipts: no tax invoice without VAT registration; the slip plus "โปรเปิดแล้ว ใช้ได้ถึง …" on her Pro page is the receipt. Refund = a manual transfer, request set declined with the reason.

**Tax, in one honest paragraph (not advice).** Money paid to you for your own service is your assessable personal income: declare it on the year's PND 90 (and mid-year PND 94 if treated as §40(8) income), keep the `pro_request` records as the ledger; no VAT until ฿1.8M a year; no withholding when individuals pay you. Open a company when a PSP or LINE OA verification wants a juristic person, when the booking fee (`docs/paiduay/plans/payments.md` §2) is about to start, or when Pro revenue is steady around ฿30–50k a month — before that it costs more than it protects. The PDPA controller can be a named person; `/policy` should carry that name.

**The price as she reads it.** ฿499 once, 12 months, no auto-renew, no card, no subscription. Every kit template including night versions, one handle change; "featured on browse" is said to not exist yet. The card stays free. Exactly what `copy-pro.ts` says, and enough to sell.

## 6. First-week metrics

Cheap, honest, from `kernel.records` (tenant slug `paiduay`; seeded demo cards have no `ownerUserId`, so they are excluded). Run with any `psql` using the server `.env` Postgres values — a read is fine with the superuser; never use it to prove isolation.

```sql
select
  count(*) filter (where type='card' and data->>'ownerUserId' is not null) as cards_created,
  count(*) filter (where type='card' and data->>'ownerUserId' is not null and state='published') as cards_published,
  count(*) filter (where type='card' and data->>'ownerUserId' is not null and state='published'
                    and coalesce(data->'photo'->>'src','') <> '') as published_with_photo,
  count(*) filter (where type='pro_request') as pro_requests,
  count(*) filter (where type='pro_request' and state='approved') as pro_approved,
  count(*) filter (where type='report') as reports,
  count(*) filter (where type='report' and state='open') as reports_open
from kernel.records
where tenant_id = (select id from kernel.tenants where slug='paiduay')
  and created_at >= now() - interval '7 days';
```

Per day, for the curve:

```sql
select (created_at at time zone 'Asia/Bangkok')::date as day, type, state, count(*)
from kernel.records
where tenant_id = (select id from kernel.tenants where slug='paiduay')
  and created_at >= now() - interval '7 days'
group by 1,2,3 order by 1,2,3;
```

Not measurable yet: LINE-button taps (the analytics plan). Do not guess them.

**The two numbers that decide.** `published_with_photo` and `pro_approved`. Proposed reading: ≥ 10 published-with-photo in seven days means the tweet and the editor work; < 3 means the door is wrong (copy, sign-up friction, the host), not the market — fix the door before tweeting again. `pro_approved` ≥ 1 is first revenue; 0 with ≥ 10 cards is a price-or-what-Pro-buys question, answered by asking three of those sellers on LINE, not by building.

## 7. Risks and responses

| Risk | What happens | Response |
|---|---|---|
| **Impersonation report** | Someone reports `not_me` on a real seller, or a real person finds her photo on a stranger's card | Hide first (24 h clock), verify second: proof = a DM from the linked account. Restore from `/admin/card`. The report record is the written log MDES expects (CCA §15). |
| **The tweet goes wide** | Box: 4 vCPU, 12 GB (9.4 GB free), 9 containers — paiduay, caddy, tuaton-web, Luna api + frontend, demo, clinic, cameo, barber. Pages are cheap; photo uploads (sharp), kit and OG renders (Satori) and `/api/chat` (tokens, 2M/day budget) are the CPU. | Turn off in this order: (1) `docker compose stop demo clinic barber cameo` — no product harm; (2) `respond /api/chat* 503` in the paiduay Caddy block; (3) turn Cloudflare orange for OG/kit caching — then add `trusted_proxies` or the per-IP limiters see one IP (public-flip §2); (4) `respond "ปิดชั่วคราว" 503` on the block. Never touch Luna or TuaTon — not yours. |
| **Tweeting on the Clerk dev instance** | A hard user cap Clerk prints on the instance page (historically 500), a "Development mode" banner on `/sign-up`, LINE consent in Clerk's name, mail from clerk.com, no production licence. Users do not migrate: after the switch each seller signs up again with a **new** `userId`, so her card no longer lists under `/me`. | Switch before the tweet (Day 0 item 2). If already tweeted: the operator re-binds `ownerUserId` from `/admin/card` after she signs up again; five minutes a seller. |
| **Demo apps on the public box** | `demo.`/`clinic.`/`barber.`/`cameo.6326638.xyz` are reachable; §9.58 left "does the prefilled password stay" open | Stop the four containers for launch week (also risk 2 step 1), or `basic_auth` in Caddy as Luna has. |
| **Mass card creation** | **No rate limit at `/c/new`.** `startCard` checks Clerk session, handle regex, handle free — nothing else; no per-owner cap; photos size-limited per file only. Clerk's sign-up bot check is the only gate (`/report` and `/api/chat` are limited; cards are not). | Watch `cards_created` daily; squatted handles → hidden from `/admin/card`; `df -h` on `paiduay-uploads`. If it starts: Clerk → Restrictions → block disposable emails, then a one-hour work order — 5 cards per owner in `createCardIn` and a per-IP bucket on `startCard` reusing `TokenBucket` from `lib/rate-limit.ts`. Not before. |

## 8. What only James decides

1. The keeper domain: `frenday.xyz` is live and launch-ready, but temporary — which name to buy (`frenday.co`, `frenday.app`, or another) and whether it happens before Clerk production (§1, `public-flip.md` §1).
2. Clerk production before the tweet — or knowingly tweet on dev and re-bind sellers by hand later.
3. Which PromptPay id, and whether your legal name is printed under the QR (recommended: yes).
4. The support inbox you will actually read, and the SLA hours as printed (Mon–Fri 09–18).
5. The cast: Martin, Troy, Todd, Skinner, Krabappel, Itchy, Kirk, Wiggum.
6. Demo apps during launch week: stop, password, or leave.
7. Hide-first on "not me" (it will occasionally hide a real seller for a day) and zero-tolerance removal for rule breaks — both are what the copy already promises.
8. The two thresholds in §6 (10 cards, 1 Pro) — or your own.
9. Whether the `/c/new` cap ships before the tweet (one hour) or waits for a signal.
10. When a company exists (§5), and who is named as PDPA controller until then.
