`taskmaster/`: the factory's Whop coaching app rebuilt so the money is real. The old version switched to coaching when the member typed "Paid $X via ..." and its webhook handler only logged invoices. The character haggles; the deal desk decides. The character never sets a price: it calls one tool, quote(tier, price, reason), and dealdesk.js answers accept, counter or refuse. Four rules — the opening is the list price and the desk never quotes above it or walks a price back up; a quote under the floor is countered at the floor whatever reason came with it; a step down needs one of four reasons the member gave (scope, urgency, commitment, pay_now), each worth a slice of the anchor and each spendable once; three rounds, the opening included. Two refusals are about honesty and spend no round: a reason that reaches for the member's profile, following, employer, means or country, and invented scarcity or a fake deadline. The app decides the mode, never the model. One sheet, two prompts, and the server picks from whether Whop says the member has paid — a valid membership on prod_EZ7UkE4pkW04U or a place on a valid pack's seat list. Typing "Paid" changes nothing and a test proves it. Also: the sealed quote card the model can't edit, the share card, the deal log (openings, counters, final price, rounds, reasons, outcome, fee and net; never a word the member wrote, validated so prose can't get in), seat packs in membership metadata with rate-limited swaps and a buyer view of seats and usage but no chats, the signed payment webhook, the three experiments bucketed by user, and the Venice voice with one tool, a per-member limit and no uncensored default. Nothing created on Whop, as the card requires: the Whop client refuses every write unless constructed with live: true, and the 75 tests run against an injected fetch with simulated signed webhooks. What is missing, and why: - teahub.xyz is blocked by this container's network policy, so teahubxyz/dealdesk and its SKILL.md could not be read. The rules are implemented in taskmaster/src/dealdesk.js behind the same seam (openDeal / quote / agree / walkaway), so swapping the import in is a one-file change. Someone with network access to Teahub should reconcile the two. - docs.whop.com is blocked too, so four things stay unconfirmed and each is one line to correct: daily billing for the daily and checkins tiers (checkout() refuses to create a renewal tier until it is passed dailyBilling: "confirmed", so nothing is built on the assumption), the field that keeps an inline plan off the product's public page (HIDDEN in whop.js), the webhook's header names and whether the signed string is the body alone or a timestamp joined to it (both shapes verify; the names are constants), and how Whop's iOS rules treat a one-off checkout, which would change what these tiers may be priced at on a phone. - Where the code lives was the operator's call. It is a top-level taskmaster/ here, beside havre/, since GitHub access for this session is scoped to teahubxyz/time alone. Moving it to teahubxyz/taskmaster or a shared Whop-apps repo is a directory move; nothing outside taskmaster/ depends on it but the one test hook. - No workflow change is needed: CI's `python3 .github/time/test_time.py` step now runs the suite, as it already does for havre/. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014KUi5SZq4GBkdHQ2TdhZ3r Closes #66
taskmaster
Taskmaster is the factory's coaching character on Whop (app_M7BBH1Evw8sjL4, product prod_EZ7UkE4pkW04U). You
haggle for your price, pay through a real Whop checkout, and only then does it coach you.
The old version never actually took money: the character switched to coaching when the member typed "Paid $X via …", and the webhook handler only logged invoices. This is the rebuild. Two things carry it:
The character haggles; the deal desk decides. The character never sets a price. It calls one tool, quote(tier, price, reason), and src/dealdesk.js answers accept, counter or refuse against that tier's anchor, its floor and
the concession rules. This is OpenPoke's split between the voice and the workers, applied to price.
The app decides the mode, never the model. One character sheet, two prompts, and the server picks which to send
from one fact: does Whop say this member has paid? Typing "Paid" changes nothing, and test/mode.test.js proves it.
The haggle
Modelled on Poke's beta "bouncer", which sized you up and then made you negotiate your monthly price — it opened
high, gave ground, and people posted the price they got and asked whether anyone went lower, so the haggle was the
marketing too. Four rules, all in src/dealdesk.js:
| Rule | What it means |
|---|---|
| Anchor high | The opening is the tier's list price. The desk never quotes above it, and never walks a price back up. |
| Keep a real floor | A quote under the floor is countered at the floor, whatever reason came with it. |
| Earn every step | A step down needs one of four reasons the member gave — scope, urgency, commitment, pay_now — each worth a fixed slice of the anchor and each spendable once. No reason, no step. |
| Three rounds | Every answer that puts a number on the table is a round, the opening included. Then the standing price is the price. |
Two more, about honesty rather than money, and neither spends a round:
- Price on what they tell you, never on who they are. A reason that reaches for the member's profile, following, employer, means or country is refused. This is the one place Taskmaster deviates from Poke's bouncer.
- No invented scarcity and no fake deadlines. The haggle is a game with real rules, not pressure.
The agreed price becomes a quote card the model cannot edit — sealed with an HMAC over its fields, so a card that comes back through the model changed is worthless — and after a deal comes a share card ("I got Taskmaster down to $X. Anyone get it lower?").
Every deal is logged, closed or walked away from: the opening, each counter, the final price, the rounds, the
reasons and the outcome, and never a word the member wrote (src/deallog.js). That log is the price discovery;
once there are enough deals, the operator replaces the haggle with fixed tiers, as Poke did.
Tiers
Starting points from the old prompt, in src/tiers.js, which is the only file that knows a price.
| Tier | Opens at | Floor | Shape |
|---|---|---|---|
sprint — 1-hour sprint |
$100 | $80 | one-time, 14 days to use |
lite — 30-minute sprint |
$50 | $40 | one-time, 14 days |
pack3 — 3 sprints |
$300 | $250 | one-time, 90 days (17% off, not the 15% the old prompt claimed) |
daily — ongoing |
$10/day | $8/day | renewal, 1-day billing |
checkins — email check-ins |
$5/day | $4/day | renewal, 1-day billing |
seats5 / seats10 |
$100/seat | $80 / $72 per seat | one-time pack, 90 days |
Only one discount was ever published (the sprint's $100 against its $80 intro, 20% off), so that is the ratio the other individual floors take. Fees are in there too: a card charge plus orchestration is about $0.30 + 3.5%, which is 9.5% of a $5 ticket but 3.8% of a $100 one, so every logged deal carries what actually reaches the owner.
Orgs
A team lead haggles for a seat pack within a per-seat floor, then assigns seats by Whop username. A Whop checkout
has no quantity, so a pack is one membership and the seat list lives in that membership's metadata — no database.
Swaps are allowed but rate-limited. The buyer sees seats and usage, never anyone's chats: usage is counted in
days a seat was used, and assertNoChats in src/seats.js is what keeps a future field from adding them. An org
that already runs a whop can install Taskmaster there and buy the pack as the app's plan instead.
If a creator recommends Taskmaster, Whop's affiliate codes pay them: a checkout configuration takes an
affiliate_code and Whop pays the commission. There is no reseller cut here.
Layout
src/tiers.js what is sold, the anchors, the floors, the fees, the variant key
src/dealdesk.js the rules. The only thing that decides a price
src/cards.js the sealed quote card and the share card
src/deallog.js one line per deal, validated so prose cannot get in
src/mode.js haggle or coach, from Whop's answer; the two prompts
src/seats.js seat packs in membership metadata, and the buyer's view
src/whop.js the Whop client: checkouts with variant reuse, memberships, metadata
src/webhook.js the signed payment webhook, the only thing that unlocks coach mode
src/experiments.js the three experiments, bucketed by user
src/voice.js Venice, the one tool, the rate limit, no asterisk narration
src/server.js the routes
src/index.js where app secrets become dependencies
npm test runs it all: no dependencies, no network, no Whop.
Secrets
Set with whop apps secrets set. None has a default that would let the app run half-configured: see the list at
the top of src/index.js. The persona — the character sheet and the two prompts — belongs to the operator and
stays out of this repo; what is here is a personality-free skeleton so a fresh install and the tests run.
The voice runs on Venice, as the old app did. The old code defaulted to venice-uncensored; there is no uncensored
default here and model() refuses one outright. Name the model in TASKMASTER_MODEL. The sheet is plain text
rather than a Venice character, so it is portable to another model unchanged.
v1 leaves out the old image and voice modes.
Not done here
Card #66 touches nothing on Whop: no checkout configuration, no variant, no deploy, no secret, no base_url
change, until the operator approves the Whop previews. src/whop.js therefore refuses every write unless it is
constructed with live: true (TASKMASTER_WHOP_LIVE=on), and the tests run against an injected fetch.
Four things need confirming against Whop's docs before this runs for real, and each is one line to correct:
- Daily billing for the
dailyandcheckinstiers. Whop's membership API mentions daily billing, so it looks supported, butcheckout()refuses a renewal tier until it is passeddailyBilling: "confirmed". - How an inline plan is kept off the product's public page. The field is in
HIDDENinsrc/whop.js. - The webhook's header names and signed string. Both shapes are verified (a timestamp joined to the body, or
the body alone); the header names are constants at the top of
src/webhook.js. - iOS. Whop's iOS rules may treat a one-off checkout differently from a subscription, which would change what these tiers may be priced at on a phone.
The factory's own deal desk lives at https://teahub.xyz/teahubxyz/dealdesk and the card asks for it to be
imported rather than rebuilt. That host is not reachable from the build container, so the rules are implemented in
src/dealdesk.js behind the same seam — every caller goes through openDeal / quote / agree / walkaway, so
swapping the import is a one-file change.