`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
122 lines
7.6 KiB
Markdown
122 lines
7.6 KiB
Markdown
# 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.
|