Files
time/taskmaster/README.md
T
Claude a1ea50f816
ci / test (pull_request) Successful in 47s
heartbeat / tick (pull_request_review) Successful in 12s
Taskmaster reroll: haggle to a real Whop checkout, then coach
`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
2026-10-20 14:15:26 +00:00

7.6 KiB

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 daily and checkins tiers. Whop's membership API mentions daily billing, so it looks supported, but checkout() refuses a renewal tier until it is passed dailyBilling: "confirmed".
  • How an inline plan is kept off the product's public page. The field is in HIDDEN in src/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.