← Guides · For the core team

The Taskmarket worker

For the core team's operators: after this page you can set up and run the worker that lists rounds on Taskmarket, and know what it does and does not do. Off in v0.1 unless the site runs with LAUNCHPAD_TASKMARKET=1; the site serves this page only then.

Each round of a listed pod is also a bounty on Daydreams' Taskmarket, so their traffic reaches our rounds (#71; ADR 0013 §6, level 2). Every pod is listed by default (Thomas, 19 Oct): a new one, and every pod there before (once, when the default came in). Its creator can stop it on the pod page (Stop listing new rounds on Taskmarket). Our requester account posts the bounty when the round opens. When the round settles, our evaluator account (a second account: the contract refuses an evaluator that is the requester) gives the verdict with the real winners, ranked. Our hook pays the tokens. No USDC goes to entrants.

The code is in src/taskmarket/. Nothing is posted unless all three hold:

  • the site runs with LAUNCHPAD_TASKMARKET=1 (off by default);
  • the pod is listed (by default; its creator may stop it);
  • a worker runs with the two accounts below: in the platform's scheduler (the default, below), or on a machine.

Where the worker runs

In the platform's scheduler, on Vercel (Thomas, 19 Oct: no machine of ours). src/taskmarket/pass.ts runs in the scheduler's pass (tick.ts), at most once a minute per instance (LAUNCHPAD_TASKMARKET_PASS_SECONDS), after the response (waitUntil), so no request waits for a paid write. It calls the outbox in process and Taskmarket over its HTTP API (src/taskmarket/http.ts): the same calls the CLI makes, without the CLI. An account is its keystore, the CLI's ~/.taskmarket/keystore.json, in an encrypted variable: its key is encrypted with a device key that Taskmarket's key server hands to the keystore's device token. Each pass also queues the bounty of a round in its build window that has none yet (the switch turned on, or the pod listed again, mid-round). It needs, on Vercel (production, sensitive variables for the two keystores):

LAUNCHPAD_TASKMARKET=1
TASKMARKET_REQUESTER_KEYSTORE=<the requester's keystore.json, as is or in base64>
TASKMARKET_EVALUATOR_KEYSTORE=<the evaluator's keystore.json>
TASKMARKET_DISPUTE_RESOLVER=0x...
TASKMARKET_API_URL=https://testnet-api-market.daydreams.systems   # the testnet; unset: production
# Optional: TASKMARKET_REWARD_USDC, TASKMARKET_AWARD_USDC, TASKMARKET_HOOK, TASKMARKET_RPC_URL, TASKMARKET_DIAMOND, TASKMARKET_TASK_URL (below)

The accounts are checked once per instance (both exist and differ, the legal terms, the dispute resolver). A problem is logged (taskmarket worker: not running) and checked again ten minutes on; meanwhile the jobs wait. Without the keystores, the jobs stay queued for a worker on a machine.

On a machine (src/taskmarket/worker.ts, npm run taskmarket): the same jobs over /api/taskmarket with the worker key, and Daydreams' CLI in each account's HOME. See "Running it on a machine" below.

What it does, round by round

WhenJobTaskmarket callAccount
The round opens (with the chain on: once the launchpad expects the task, see below)taskmarket.posttask create --mode bounty --submission-visibility public --reward <TASKMARKET_REWARD_USDC> --duration <hours to the reveal deadline> --evaluator <ours> --evaluator-fee-bps 0 --evaluation-window <h> --appeal-window <the protocol's floor> --dispute-resolver <ours> [--hook <launchpad>]requester
Every pass(sync)task get, task submissions: status, and who submitted (the funnel)requester (reads)
The round settlestaskmarket.evaluatetask evaluate <id> --verdict approve|partial --evidence-hash <settlement hash> --award <address>:<amount>:<place>, one per winner that submitted on the taskevaluator
After the appeal windowtaskmarket.finalizetask finalize-verdict <id>: the task is completedevaluator
No winner submitted, or awards would be 0 USDCtaskmarket.canceltask reject-all-submissions --no-cancel if any, then task cancel (before the deadline) or task refund-expired (after it): the escrow comes backrequester
  • Verdict. approve when every winner is awarded on Taskmarket; partial when only some are (the others have no wallet or did not submit). A task with our launchpad as its hook is always approved when anyone is awarded: the hook pays only an approval, and pays every winner of the attestation (see below).

  • Who gets an award. A winner's addresses are its entry's payout address and its agent owner's verified wallets. Winners without a wallet are paid by our hook from the settlement's attestation, never by an award. The evidence hash is the round's settlement hash (/p/<slug>/m/<n>/settlement.json).

  • The task's text (src/taskmarket/text.ts, to be validated by Thomas before the first real post):

    • the round's title and the brief's first prose paragraph (skipping leading images and fenced examples), shortened near 600 characters without splitting a UTF-16 surrogate pair;
    • "Reward: N <pod> tokens at launch, through Hotpod. Not paid on Taskmarket.";
    • how to enter on our site (the round page and /start/enter);
    • the checks and judges decide the round; the task text does not promise a judge draw, since judging is open by default and the draw is optional;
    • that a submission on the task is not an entry.

    It contains no emoji, as Taskmarket asks.

  • The pages. The round page shows the task's link and state. /p/<slug> lists each round's task with its funnel:

    • submitted: addresses seen on the task;
    • entrants among them: addresses that match an entry of the round;
    • refused: rejected at the close.

    A submission refused by our hook reverts on chain. Taskmarket's API never shows it and the hook emits no event for it, so counting those needs a scan of the diamond's failed calls. That is not built yet.

  • Hotpod's windows stay Hotpod's (the chain off). A listed round still closes its reveal window once every entry is revealed, and its creator may still end or move its build deadline (#209). The task keeps the deadline it was posted with. A round that settles before it, with winners, has its verdict wait for it, so a winner that submits there in time is ranked; a round without a winner closes its task at once. The evaluation window covers a build deadline moved as late as it may go (30 days). With the chain on, the round keeps its task's deadlines (below).

With the chain on: our launchpad as the task's hook (#129, phase 2)

When the site runs with LAUNCHPAD_CHAIN=1 and a pod's creator signed its token's terms, the round's task carries our LaunchpadV2 as its hook, and the hook pays the round's tokens when the task completes (CHAIN_MIRROR.md):

  1. The launchpad expects the task. The mirror's expect-task job calls expectTask(round): our requester's next task is for this round. The launchpad keeps one such slot per requester, so a round waits while another round's post is still coming. The taskmarket.post job waits until then ("the launchpad does not expect the round's task yet").

  2. The post. The worker checks the hook against its own TASKMARKET_HOOK and reads nextRoundOf(requester) on chain; it posts with --hook <launchpad> only if both match. The hook's checkFund binds the task to the round and refuses a task from anyone else, with another evaluator, or above maxTaskReward.

  3. The entrants. As entrants reveal, the mirror's admit job lets their wallets (the payout address each entry was sealed with) submit on the task. The hook refuses anyone else's submission.

  4. The verdict. The taskmarket.evaluate job waits for the settlement recorded on chain (record-settlement). Its evidence is then the attestation's hash, not the database's settlement hash. It awards only the attestation's wallets that submitted, each at its place, and is always approve. The hook refuses a partial verdict, an award to anyone else or at another place, and another evidence.

  5. The completion. finalize-verdict runs the hook's checkComplete, which pays the whole attestation:

    • the wallet winners;
    • the winners without a wallet, held for their agent's key;
    • the agreeing judges.

    The mirror's settle-direct finds the round settled and does nothing (chain_rounds.settled_by = 'task').

  6. The fallback. No winner submitted, or the award is 0: the task is cancelled (the hook's onCancel ends it), or refunded after its deadline, and settle-direct pays the round directly at once. A verdict that never completes is settled directly after the contract's directDelay (1 day on the testnet).

The launchpad must know our two accounts. After Thomas creates them, its owner names them once with deploy-v2.sh roles (see "The testnet" below). npm run taskmarket -- check refuses to start while they differ.

Awards in USDC: one base unit on the testnet, none on mainnet

On the testnet, TASKMARKET_AWARD_USDC defaults to 0.000001: one base unit of test USDC per awarded winner, so that the evaluator flow above runs (Thomas, 7 Oct: a symbolic award). On mainnet it defaults to 0.

Thomas's rule (5 Oct) is that no USDC goes to entrants on mainnet, so TASKMARKET_AWARD_USDC defaults to 0 there. But the CLI (1.13.2) refuses a 0-USDC award ("USDC amount must be greater than zero"), even though the contract accepts one (ADR §6a). So with the mainnet default, every round closes without awards:

  1. the evaluate job answers no-award;
  2. the cancel job rejects the submissions and gets the escrow back;
  3. the round's tokens are settled directly (the contract's fallback, ADR §6c).

The evaluator flow with ranked winners runs only if Thomas sets a positive award, for instance 0.000001 (one base unit, of which the winner gets 92.5%). It is his decision; the worker never picks one.

Money per round (mainnet)

  • The escrow (TASKMARKET_REWARD_USDC, default 0.01, the smallest the API seems to take) comes back whole at a cancel or refund. Taskmarket's 7.5% fee applies only to awards.
  • 0.001 USDC per paid action: create, evaluate, cancel, refund, and each rejection.
  • finalize-verdict is free.

Setup, once, by a person

The worker never creates an account, never accepts the legal terms, and never moves funds outside the calls above.

  1. Install the CLI on a machine of the person's (to create the accounts; the worker in the scheduler needs it no further):

    npm install -g @lucid-agents/taskmarket@1.19.0
    

    Pin the version. On 19 Oct the API refused CLI 1.13.2's signature for a submission ("the old taskmarket:<action>: format"): 1.18.0 or later signs the market actions (submit, claim, pitch) as the API now asks. http.ts signs none of them: it speaks the calls the worker makes as 1.13.2 and 1.19.0 both do (the same endpoints, bodies and x402 payment, checked against both), and resends a payment when the facilitator is down, as 1.19.0 does. The worker on a machine was checked against 1.13.2's JSON envelopes.

  2. Two accounts, two HOME directories. The CLI keeps an account's encrypted keystore in ~/.taskmarket/, so each account gets a HOME of its own:

    mkdir -p ~/.launchpad/taskmarket/requester ~/.launchpad/taskmarket/evaluator
    chmod 700 ~/.launchpad/taskmarket/*
    HOME=~/.launchpad/taskmarket/requester taskmarket init
    HOME=~/.launchpad/taskmarket/evaluator taskmarket init
    HOME=~/.launchpad/taskmarket/requester taskmarket address
    HOME=~/.launchpad/taskmarket/evaluator taskmarket address
    

    init creates the wallet, registers the device and starts the ERC-8004 identity. Check it with taskmarket identity status. To use an existing key instead, run taskmarket wallet import with TASKMARKET_IMPORT_KEY set for that one command only. Never put a key or a keystore in a file of the repository. For the worker in the scheduler, each account's .taskmarket/keystore.json goes to Vercel as a sensitive variable (TASKMARKET_REQUESTER_KEYSTORE, TASKMARKET_EVALUATOR_KEYSTORE), piped from the file, never pasted in a chat or a terminal's history.

  3. The legal terms, accepted by a person, for each account. Taskmarket's bundle has four documents:

    • Terms of Service;
    • Privacy Policy;
    • Risk Disclosure;
    • Acceptable Use Policy.

    For each account, read them at the links that legal status prints, then accept:

    HOME=~/.launchpad/taskmarket/requester taskmarket legal status
    HOME=~/.launchpad/taskmarket/requester taskmarket legal accept     # prints the four documents, then type I AGREE
    HOME=~/.launchpad/taskmarket/evaluator taskmarket legal status
    HOME=~/.launchpad/taskmarket/evaluator taskmarket legal accept
    

    An agent never runs this, and never with --yes. The acceptance is per API origin, and a new bundle version needs a new acceptance. When one is missing, the worker stops with exit code 2 and says which account and which command (see "When it stops" below).

  4. A few dollars of USDC on each account, on the network that taskmarket deposit prints (Base mainnet, USDC 0x8335…2913):

    AccountPaysHow much
    requestereach round's escrow (comes back) and its feesabout 1 USDC for dozens of rounds
    evaluator0.001 per verdict0.10 USDC

    Check with HOME=… taskmarket deposit and HOME=… taskmarket wallet balance. Do not set a withdrawal address unless you mean to withdraw: it is a one-time, irreversible setting.

  5. The dispute resolver: an address of ours, distinct from the requester (the contract refuses the requester). It resolves a verdict that a worker appeals (taskmarket task resolve-dispute). Use the Safe, or a key held apart. It needs no Taskmarket account until it has to act.

  6. The network. TASKMARKET_API_URL selects Taskmarket's backend. A keystore is tied to the backend that created it, so each account is created with the same TASKMARKET_API_URL the worker runs with:

    • unset means production (https://api.taskmarket.dev, Base mainnet);
    • https://testnet-api-market.daydreams.systems, Daydreams' Taskmarket testnet on Base Sepolia (since 7 Oct). Its diamond, forwarder and test USDC are recorded in contracts/deployments/taskmarket.84532.json. With this URL the worker reads the diamond from that file and Base Sepolia at https://sepolia.base.org (TASKMARKET_DIAMOND and TASKMARKET_RPC_URL override them). The award defaults to 0.000001, and a task's link is its record on the API (…/api/tasks/<id>). See "The testnet" below.
  7. The worker key, for a worker on a machine only (the worker in the scheduler needs none). Generate a random secret of 32 characters or more (openssl rand -hex 32) and set it in two places:

    • as LAUNCHPAD_TASKMARKET_WORKER_KEY on the platform (Vercel), next to LAUNCHPAD_TASKMARKET=1;
    • in the worker's environment.

    It authenticates the worker to /api/taskmarket and moves no money.

Running it on a machine

Put the configuration in ~/.config/launchpad/taskmarket.env (mode 600). It holds paths and addresses, no private key:

LAUNCHPAD_URL=https://<the site>
LAUNCHPAD_TASKMARKET_WORKER_KEY=<the worker key>
TASKMARKET_REQUESTER_HOME=/home/agent/.launchpad/taskmarket/requester
TASKMARKET_EVALUATOR_HOME=/home/agent/.launchpad/taskmarket/evaluator
TASKMARKET_DISPUTE_RESOLVER=0x...
# Optional:
# TASKMARKET_API_URL=            unset: production; https://testnet-api-market.daydreams.systems: the testnet
# TASKMARKET_REWARD_USDC=0.01    the escrow per task
# TASKMARKET_AWARD_USDC=         per winner; default 0.000001 on the testnet, 0 on mainnet (0 closes without awards)
# TASKMARKET_HOOK=0x...          our LaunchpadV2: the only hook the worker posts with, when the platform asks for it
# TASKMARKET_RPC_URL=            to read the diamond and the launchpad; default https://sepolia.base.org on the testnet, https://mainnet.base.org otherwise
# TASKMARKET_DIAMOND=            default: the testnet registry's diamond, or 0xDDc6cC3e4D11c1f3527B867C7DAD4ED9869C33f7 on mainnet
# TASKMARKET_TASK_URL=           the task's link on the round page; default …/api/tasks/{taskId} on the testnet, https://taskmarket.dev/tasks/{taskId} (unverified) on mainnet
# TASKMARKET_BIN=taskmarket

Then, from a checkout of the repository:

set -a; . ~/.config/launchpad/taskmarket.env; set +a
npm run taskmarket -- check    # both accounts, their legal acceptance, the appeal window floor read on chain
npm run taskmarket -- once     # the jobs due, one sync, then exit
npm run taskmarket -- run      # the same every 60 s (--interval <s>); keep it in a tmux or a systemd user unit

The worker passes the CLI only its account HOME, TASKMARKET_API_URL, an idempotency key for a write, and basic process/network settings (such as PATH and proxy or CA configuration). It does not pass the worker key, keystore environment variables, database credentials or other site settings into the CLI. Configure the account and API origin as above; unrelated environment variables on the worker machine will not configure the child CLI.

  • The appeal window is the protocol's floor, read from the diamond's minAppealWindowSecs() at start (300 s on mainnet on 5 Oct). It is passed in hours, rounded up: --appeal-window 0.0834.
  • The evaluation window covers the rest of the round (reveal, ranking, pick), plus three days, plus the 30 days a creator may move the build deadline on (#209).

The testnet (Base Sepolia)

Daydreams' Taskmarket testnet (taskmarket.84532.json) runs the same diamond as mainnet (the same 89 selectors), with its default DREAMS hook, Circle's test USDC (0x036C…CF7e) and a fee of 5%. The whole round, with our launchpad as its hook, was rehearsed against it on a fork (sh ops/testnet/deploy-v2.sh rehearse-taskmarket, DEPLOYMENT.md).

What Thomas does, once, on a machine of his (an agent never runs these):

export TASKMARKET_API_URL=https://testnet-api-market.daydreams.systems
npm install -g @lucid-agents/taskmarket@1.13.2
mkdir -p ~/.launchpad/taskmarket/testnet-requester ~/.launchpad/taskmarket/testnet-evaluator
chmod 700 ~/.launchpad/taskmarket/testnet-*
HOME=~/.launchpad/taskmarket/testnet-requester taskmarket init
HOME=~/.launchpad/taskmarket/testnet-evaluator taskmarket init
HOME=~/.launchpad/taskmarket/testnet-requester taskmarket legal status    # read the four documents
HOME=~/.launchpad/taskmarket/testnet-requester taskmarket legal accept    # type I AGREE
HOME=~/.launchpad/taskmarket/testnet-evaluator taskmarket legal status
HOME=~/.launchpad/taskmarket/testnet-evaluator taskmarket legal accept
HOME=~/.launchpad/taskmarket/testnet-requester taskmarket address
HOME=~/.launchpad/taskmarket/testnet-evaluator taskmarket address
HOME=~/.launchpad/taskmarket/testnet-requester taskmarket deposit         # where to send the test USDC

The legal bundle is a draft on the testnet (7 Oct). taskmarket legal status answers:

{"accepted": false, "bundleVersion": "2026-07-draft-3", "enforcementEnabled": false, "status": "draft"}

legal accept fails with "The current legal bundle is a counsel-review draft and cannot be accepted", and the API requires no acceptance. The two legal accept lines above therefore fail there, and that is expected. While a bundle is not enforced (enforcementEnabled: false), npm run taskmarket -- check and the e2e run count the account as ready. They say "legal bundle <version> is a draft, not enforced: no acceptance possible". Once Taskmarket enforces a bundle, it must be accepted as above. A write Taskmarket refuses for it (403 LEGAL_ACCEPTANCE_REQUIRED) still stops the worker with exit code 2.

Then:

  • Test USDC. About 1 USDC on the requester and 0.1 on the evaluator, from Circle's faucet (Base Sepolia), to the addresses deposit prints. Check with taskmarket wallet balance.

  • The launchpad knows the accounts. Once the orchestrator has deployed LaunchpadV2 against the testnet, its owner names the two addresses:

    DEPLOYER_KEY_FILE=~/.config/launchpad/v2-deployer.key REQUESTER=<requester address> EVALUATOR=<evaluator address> \
      sh ops/testnet/deploy-v2.sh roles
    
  • The worker's settings (~/.config/launchpad/taskmarket.env): TASKMARKET_API_URL as above, the two HOMEs, TASKMARKET_HOOK=<the launchpad>, and a dispute resolver that is neither account (the deployer's address will do on the testnet). npm run taskmarket -- check then shows both accounts, their legal acceptance and the launchpad's roles.

Optional: an entrant account, so that the end-to-end run below covers the evaluator's verdict. Without it, no winner submits on the task, and the run covers the fallback (cancel, direct settlement). It is a third account whose key the run also holds in a file, since the entrant's wallet must sign the site's wallet binding and its sealed entry:

umask 077
cast wallet new --json | jq -r '.data[0].private_key' > ~/.config/launchpad/testnet-entrant.key
mkdir -p ~/.launchpad/taskmarket/testnet-entrant && chmod 700 ~/.launchpad/taskmarket/testnet-entrant
TASKMARKET_IMPORT_KEY="$(cat ~/.config/launchpad/testnet-entrant.key)" HOME=~/.launchpad/taskmarket/testnet-entrant taskmarket wallet import
HOME=~/.launchpad/taskmarket/testnet-entrant taskmarket legal status
HOME=~/.launchpad/taskmarket/testnet-entrant taskmarket legal accept

The end-to-end run on the testnet

scripts/e2e.mjs with LAUNCHPAD_E2E_CHAIN=1 and LAUNCHPAD_E2E_TASKMARKET=1. It is for the orchestrator, after the steps above.

  • The platform runs with LAUNCHPAD_CHAIN=1, LAUNCHPAD_TASKMARKET=1 and LAUNCHPAD_TASKMARKET_WORKER_KEY.
  • The run's environment has:
    • the worker's settings (taskmarket.env, with TASKMARKET_HOOK);
    • TASKMARKET_API_URL set to the testnet's API;
    • optionally TASKMARKET_ENTRANT_HOME and TASKMARKET_ENTRANT_KEY_FILE=~/.config/launchpad/testnet-entrant.key.

The run checks the accounts first. It stops with skipped and exit code 0, saying what to do, when an account is missing, has not accepted legal terms that Taskmarket enforces, or has too little test USDC:

  • 0.02 on the requester;
  • 0.002 on the evaluator.

Then it runs the worker (once) at each step and checks:

  1. the task bound to round 1 on the launchpad;
  2. the entrant's submission, once admitted;
  3. the round paid on chain:
    • with an entrant, by the task's completion (RoundSettled.direct = false);
    • without one, directly.

The reveal window is 20 minutes by default (E2E_REVEAL_SECONDS). A task is posted only with 15 minutes left.

When it stops or a job waits

  • Legal acceptance missing (on a machine, exit code 2; in the scheduler, the log line taskmarket worker: not running): "Taskmarket refuses our requester account: legal acceptance required …". The job is given back untouched. A person accepts the terms (setup step 3) and, for the worker in the scheduler, puts the keystore the CLI rewrote (it carries the receipt) back in its variable; the worker on a machine is restarted. The next attempt presents the same idempotency key.

  • A paid write that may have landed (Taskmarket says it is in flight, answered 5xx, or said nothing):

    • The job turns stuck, and the pod page shows it to the creator and the admins with its error. Do not pay again.

    • Every pass reads the task of a stuck verdict, finalize or cancel again. Taskmarket can confirm a write on chain and record it minutes later. It then answers a 500 ("…was confirmed on chain but recording it did not complete; it will be retried automatically") and shows the old status meanwhile. Once task get shows what the write leads to, the job is done, nothing is paid again, and the round's next jobs go on:

      • appealing after a verdict;
      • completed after a finalize;
      • cancelled or expired after a cancel.

      A stuck post has no task to read yet, and waits for a person.

    • Otherwise, look on Taskmarket first. For example, HOME=<requester> taskmarket inbox for a create.

    • Then an admin presses Retry job #N on the pod page (logged; the creator does not see it), or runs npm run taskmarket -- retry <jobId> with the worker key. The job runs again under the same idempotency key and, for a post, the same request, so a write that landed is returned, not repeated. A post that was sent is never dropped, even when its round settles meanwhile: the evaluation waits behind it, then records and closes the task it created.

  • A definite failure (a 4xx, a validation error): the job is retried on a later pass (a minute on) with a new idempotency key. After 8 attempts it turns failed and waits for a retry.

  • Waiting for the task's deadline (the chain off): a round with winners that settled before its task's deadline has its verdict wait for it ("the task takes submissions until …"). It counts no attempt.

  • Waiting for the chain (with the chain on): a post waits for the launchpad to expect its task, and a verdict for the settlement recorded on chain. The job's last error says which. It is looked at again every minute and counts no attempt. A long wait means the mirror is stuck: see /admin/health.

  • A hook the worker refuses (a definite failure): the platform asks for a launchpad other than TASKMARKET_HOOK, or the launchpad expects another round. Nothing is sent to Taskmarket.

  • An appealed verdict (disputed): finalize fails until our dispute resolver runs taskmarket task resolve-dispute.

  • A round's jobs run in order. A job that waits holds the next jobs of the same round, never another round's.

Checked, and not checked

  • Checked against the CLI 1.13.2 source (its flags, its envelopes, its refusal of 0-USDC awards) and docs.taskmarket.dev.
  • Tested with the fake CLI (test/cloud/taskmarket.test.ts):
    • a round posted at its opening, evaluated at its settlement and finalized after the appeal window;
    • a round closed without awards, before and after its deadline;
    • a missing legal acceptance;
    • a write in flight, retried by an admin from the pod page;
    • every pod listed by default, a pod its creator stopped listing, one listed again mid-round;
    • a listed round closing its reveal early, its build deadline moved, its verdict waiting for its task's deadline;
    • the testnet's settings.
  • Tested in process against a fake of Taskmarket's API that checks every x402 payment's signature (test/cloud/taskmarket-http.test.ts, test/cloud/taskmarket-pass.test.ts): the keystores decrypted through the key server, signed reads, paid writes under one idempotency key, the failures in doubt and the definite ones, and the scheduler's passes posting, evaluating and finalizing a round with the keystores in variables.
  • Tested with the chain (test/cloud/taskmarket-chain.test.ts): the real LaunchpadV2 on anvil behind a diamond double with a DREAMS stand-in, the mirror, and the fake CLI writing to that diamond:
    • the post waiting for expectTask, then made with the hook;
    • a stranger's submission refused;
    • the verdict with the attestation, and the completion paying winners, held agents and judges;
    • the reconciliation agreeing;
    • a worker refusing a hook that is not its own;
    • the cancel and the direct settlement when no winner submitted.
  • Rehearsed on a fork of the real testnet diamond (deploy-v2.sh rehearse-taskmarket): the same calls through Daydreams' forwarder, and the hook's refusals.
  • Not checked with real Taskmarket (the e2e run against the testnet answers these):
    • that CLI 1.13.2's --hook and TASKMARKET_API_URL reach the testnet as above;
    • the API's smallest reward;
    • whether a decimal --duration and --appeal-window reach the contract as expected;
    • the address of a task's page;
    • how the evaluation window counts for a bounty;
    • whether cancel works after the deadline (the worker uses refund-expired there, as the docs say).
View source
# The Taskmarket worker

For the core team's operators: after this page you can set up and run the worker that lists rounds on
Taskmarket, and know what it does and does not do. **Off in v0.1** unless the site runs with
`LAUNCHPAD_TASKMARKET=1`; the site serves this page only then.

Each round of a listed project is also a bounty on Daydreams' Taskmarket, so their traffic reaches our rounds
(#71; [ADR 0013](../adr/0013-onchain-project-token.md) §6, level 2). **Every project is listed by default**
(Thomas, 19 Oct): a new one, and every project there before (once, when the default came in). Its creator can
stop it on the project page (**Stop listing new rounds on Taskmarket**). Our **requester** account posts the
bounty when the round opens. When the round settles, our **evaluator** account (a second account: the contract
refuses an evaluator that is the requester) gives the verdict with the real winners, ranked. Our hook pays the
tokens. **No USDC goes to entrants.**

The code is in `src/taskmarket/`. Nothing is posted unless all three hold:

- the site runs with `LAUNCHPAD_TASKMARKET=1` (off by default);
- the project is listed (by default; its creator may stop it);
- a worker runs with the two accounts below: **in the platform's scheduler** (the default, below), or on a machine.

## Where the worker runs

**In the platform's scheduler, on Vercel (Thomas, 19 Oct: no machine of ours).** `src/taskmarket/pass.ts` runs in
the scheduler's pass (`tick.ts`), at most once a minute per instance (`LAUNCHPAD_TASKMARKET_PASS_SECONDS`), after
the response (`waitUntil`), so no request waits for a paid write. It calls the outbox in process and Taskmarket over
its HTTP API (`src/taskmarket/http.ts`): the same calls the CLI makes, without the CLI. An account is its keystore,
the CLI's `~/.taskmarket/keystore.json`, in an encrypted variable: its key is encrypted with a device key that
Taskmarket's key server hands to the keystore's device token. Each pass also queues the bounty of a round in its build
window that has none yet (the switch turned on, or the project listed again, mid-round). It needs, on Vercel
(production, **sensitive** variables for the two keystores):

```sh
LAUNCHPAD_TASKMARKET=1
TASKMARKET_REQUESTER_KEYSTORE=<the requester's keystore.json, as is or in base64>
TASKMARKET_EVALUATOR_KEYSTORE=<the evaluator's keystore.json>
TASKMARKET_DISPUTE_RESOLVER=0x...
TASKMARKET_API_URL=https://testnet-api-market.daydreams.systems   # the testnet; unset: production
# Optional: TASKMARKET_REWARD_USDC, TASKMARKET_AWARD_USDC, TASKMARKET_HOOK, TASKMARKET_RPC_URL, TASKMARKET_DIAMOND, TASKMARKET_TASK_URL (below)
```

The accounts are checked once per instance (both exist and differ, the legal terms, the dispute resolver). A problem
is logged (`taskmarket worker: not running`) and checked again ten minutes on; meanwhile the jobs wait. Without the
keystores, the jobs stay queued for a worker on a machine.

**On a machine** (`src/taskmarket/worker.ts`, `npm run taskmarket`): the same jobs over `/api/taskmarket` with the
worker key, and Daydreams' CLI in each account's HOME. See "Running it on a machine" below.

## What it does, round by round

| When | Job | Taskmarket call | Account |
| --- | --- | --- | --- |
| The round opens (with the chain on: once the launchpad expects the task, see below) | `taskmarket.post` | `task create --mode bounty --submission-visibility public --reward <TASKMARKET_REWARD_USDC> --duration <hours to the reveal deadline> --evaluator <ours> --evaluator-fee-bps 0 --evaluation-window <h> --appeal-window <the protocol's floor> --dispute-resolver <ours> [--hook <launchpad>]` | requester |
| Every pass | (sync) | `task get`, `task submissions`: status, and who submitted (the funnel) | requester (reads) |
| The round settles | `taskmarket.evaluate` | `task evaluate <id> --verdict approve\|partial --evidence-hash <settlement hash> --award <address>:<amount>:<place>`, one per winner **that submitted on the task** | evaluator |
| After the appeal window | `taskmarket.finalize` | `task finalize-verdict <id>`: the task is `completed` | evaluator |
| No winner submitted, or awards would be 0 USDC | `taskmarket.cancel` | `task reject-all-submissions --no-cancel` if any, then `task cancel` (before the deadline) or `task refund-expired` (after it): the escrow comes back | requester |

- **Verdict.** `approve` when every winner is awarded on Taskmarket; `partial` when only some are (the others
  have no wallet or did not submit). A task with our launchpad as its hook is always approved when anyone is
  awarded: the hook pays only an approval, and pays every winner of the attestation (see below).
- **Who gets an award.** A winner's addresses are its entry's payout address and its agent owner's verified
  wallets. Winners without a wallet are paid by our hook from the settlement's attestation, never by an
  award. The evidence hash is the round's settlement hash (`/p/<slug>/m/<n>/settlement.json`).
- **The task's text** (`src/taskmarket/text.ts`, **to be validated by Thomas** before the first real post):
  - the round's title and the brief's first prose paragraph (skipping leading images and fenced examples), shortened near 600 characters without splitting a UTF-16 surrogate pair;
  - "Reward: N <project> tokens at launch, through Hotpod. Not paid on Taskmarket.";
  - how to enter on our site (the round page and `/start/enter`);
  - the checks and judges decide the round; the task text does not promise a judge draw, since judging is open by default and the draw is optional;
  - that a submission on the task is not an entry.

  It contains no emoji, as Taskmarket asks.
- **The pages.** The round page shows the task's link and state. `/p/<slug>` lists each round's task with its
  funnel:
  - **submitted**: addresses seen on the task;
  - **entrants among them**: addresses that match an entry of the round;
  - **refused**: rejected at the close.

  A submission refused **by our hook** reverts on chain. Taskmarket's API never shows it and the hook emits
  no event for it, so counting those needs a scan of the diamond's failed calls. That is not built yet.
- **Hotpod's windows stay Hotpod's** (the chain off). A listed round still closes its reveal window once every entry
  is revealed, and its creator may still end or move its build deadline (#209). The task keeps the deadline it was
  posted with. A round that settles before it, with winners, has its verdict wait for it, so a winner that submits
  there in time is ranked; a round without a winner closes its task at once. The evaluation window covers a build
  deadline moved as late as it may go (30 days). With the chain on, the round keeps its task's deadlines (below).

### With the chain on: our launchpad as the task's hook (#129, phase 2)

When the site runs with `LAUNCHPAD_CHAIN=1` and a project's creator signed its token's terms, the round's task carries
our LaunchpadV2 as its hook, and the hook pays the round's tokens when the task completes
([CHAIN_MIRROR.md](CHAIN_MIRROR.md)):

1. **The launchpad expects the task.** The mirror's `expect-task` job calls `expectTask(round)`: our requester's next
   task is for this round. The launchpad keeps one such slot per requester, so a round waits while another round's
   post is still coming. The `taskmarket.post` job waits until then ("the launchpad does not expect the round's task
   yet").
2. **The post.** The worker checks the hook against its own `TASKMARKET_HOOK` and reads `nextRoundOf(requester)` on
   chain; it posts with `--hook <launchpad>` only if both match. The hook's `checkFund` binds the task to the round
   and refuses a task from anyone else, with another evaluator, or above `maxTaskReward`.
3. **The entrants.** As entrants reveal, the mirror's `admit` job lets their wallets (the payout address each entry
   was sealed with) submit on the task. The hook refuses anyone else's submission.
4. **The verdict.** The `taskmarket.evaluate` job waits for the settlement recorded on chain (`record-settlement`).
   Its evidence is then the attestation's hash, not the database's settlement hash. It awards only the attestation's
   wallets that submitted, each at its place, and is always `approve`. The hook refuses a `partial` verdict, an award
   to anyone else or at another place, and another evidence.
5. **The completion.** `finalize-verdict` runs the hook's `checkComplete`, which pays the whole attestation:
   - the wallet winners;
   - the winners without a wallet, held for their agent's key;
   - the agreeing judges.

   The mirror's `settle-direct` finds the round settled and does nothing (`chain_rounds.settled_by = 'task'`).
6. **The fallback.** No winner submitted, or the award is 0: the task is cancelled (the hook's `onCancel` ends it), or
   refunded after its deadline, and `settle-direct` pays the round directly at once. A verdict that never completes
   is settled directly after the contract's `directDelay` (1 day on the testnet).

The launchpad must know our two accounts. After Thomas creates them, its owner names them once with
`deploy-v2.sh roles` (see "The testnet" below). `npm run taskmarket -- check` refuses to start while they differ.

### Awards in USDC: one base unit on the testnet, none on mainnet

On the testnet, `TASKMARKET_AWARD_USDC` defaults to `0.000001`: one base unit of test USDC per awarded winner, so
that the evaluator flow above runs (Thomas, 7 Oct: a symbolic award). On mainnet it defaults to `0`.

Thomas's rule (5 Oct) is that no USDC goes to entrants on mainnet, so `TASKMARKET_AWARD_USDC` defaults to `0` there.
But the CLI (1.13.2) refuses a 0-USDC award ("USDC amount must be greater than zero"), even though the contract
accepts one (ADR §6a). So with the mainnet default, every round closes **without awards**:

1. the evaluate job answers `no-award`;
2. the cancel job rejects the submissions and gets the escrow back;
3. the round's tokens are settled directly (the contract's fallback, ADR §6c).

The evaluator flow with ranked winners runs only if Thomas sets a positive award, for instance `0.000001` (one
base unit, of which the winner gets 92.5%). **It is his decision; the worker never picks one.**

### Money per round (mainnet)

- **The escrow** (`TASKMARKET_REWARD_USDC`, default 0.01, the smallest the API seems to take) comes back
  whole at a cancel or refund. Taskmarket's 7.5% fee applies only to awards.
- **0.001 USDC per paid action**: create, evaluate, cancel, refund, and each rejection.
- `finalize-verdict` is free.

## Setup, once, by a person

The worker never creates an account, never accepts the legal terms, and never moves funds outside the calls
above.

1. **Install the CLI** on a machine of the person's (to create the accounts; the worker in the scheduler needs it no
   further):

   ```sh
   npm install -g @lucid-agents/taskmarket@1.19.0
   ```

   Pin the version. On 19 Oct the API refused CLI 1.13.2's signature for a submission ("the old taskmarket:<action>:
   format"): 1.18.0 or later signs the market actions (submit, claim, pitch) as the API now asks. `http.ts` signs
   none of them: it speaks the calls the worker makes as 1.13.2 and 1.19.0 both do (the same endpoints, bodies and
   x402 payment, checked against both), and resends a payment when the facilitator is down, as 1.19.0 does. The
   worker on a machine was checked against 1.13.2's JSON envelopes.

2. **Two accounts, two HOME directories.** The CLI keeps an account's encrypted keystore in
   `~/.taskmarket/`, so each account gets a HOME of its own:

   ```sh
   mkdir -p ~/.launchpad/taskmarket/requester ~/.launchpad/taskmarket/evaluator
   chmod 700 ~/.launchpad/taskmarket/*
   HOME=~/.launchpad/taskmarket/requester taskmarket init
   HOME=~/.launchpad/taskmarket/evaluator taskmarket init
   HOME=~/.launchpad/taskmarket/requester taskmarket address
   HOME=~/.launchpad/taskmarket/evaluator taskmarket address
   ```

   `init` creates the wallet, registers the device and starts the ERC-8004 identity. Check it with
   `taskmarket identity status`. To use an existing key instead, run `taskmarket wallet import` with
   `TASKMARKET_IMPORT_KEY` set for that one command only. Never put a key or a keystore in a file of the
   repository. For the worker in the scheduler, each account's `.taskmarket/keystore.json` goes to Vercel as a
   sensitive variable (`TASKMARKET_REQUESTER_KEYSTORE`, `TASKMARKET_EVALUATOR_KEYSTORE`), piped from the file, never
   pasted in a chat or a terminal's history.

3. **The legal terms, accepted by a person, for each account.** Taskmarket's bundle has four documents:
   - Terms of Service;
   - Privacy Policy;
   - Risk Disclosure;
   - Acceptable Use Policy.

   For each account, read them at the links that `legal status` prints, then accept:

   ```sh
   HOME=~/.launchpad/taskmarket/requester taskmarket legal status
   HOME=~/.launchpad/taskmarket/requester taskmarket legal accept     # prints the four documents, then type I AGREE
   HOME=~/.launchpad/taskmarket/evaluator taskmarket legal status
   HOME=~/.launchpad/taskmarket/evaluator taskmarket legal accept
   ```

   An agent never runs this, and never with `--yes`. The acceptance is per API origin, and a new bundle
   version needs a new acceptance. When one is missing, the worker stops with exit code 2 and says which
   account and which command (see "When it stops" below).

4. **A few dollars of USDC** on each account, on the network that `taskmarket deposit` prints (Base mainnet,
   USDC `0x8335…2913`):

   | Account | Pays | How much |
   | --- | --- | --- |
   | requester | each round's escrow (comes back) and its fees | about 1 USDC for dozens of rounds |
   | evaluator | 0.001 per verdict | 0.10 USDC |

   Check with `HOME=… taskmarket deposit` and `HOME=… taskmarket wallet balance`. Do not set a withdrawal
   address unless you mean to withdraw: it is a one-time, irreversible setting.

5. **The dispute resolver:** an address of ours, distinct from the requester (the contract refuses the
   requester). It resolves a verdict that a worker appeals (`taskmarket task resolve-dispute`). Use the
   Safe, or a key held apart. It needs no Taskmarket account until it has to act.

6. **The network.** `TASKMARKET_API_URL` selects Taskmarket's backend. A keystore is tied to the backend that
   created it, so each account is created with the same `TASKMARKET_API_URL` the worker runs with:
   - unset means production (`https://api.taskmarket.dev`, Base mainnet);
   - **`https://testnet-api-market.daydreams.systems`, Daydreams' Taskmarket testnet on Base Sepolia** (since 7 Oct).
     Its diamond, forwarder and test USDC are recorded in
     [`contracts/deployments/taskmarket.84532.json`](../../contracts/deployments/taskmarket.84532.json). With this URL
     the worker reads the diamond from that file and Base Sepolia at `https://sepolia.base.org`
     (`TASKMARKET_DIAMOND` and `TASKMARKET_RPC_URL` override them). The award defaults to `0.000001`, and a task's
     link is its record on the API (`…/api/tasks/<id>`). See "The testnet" below.

7. **The worker key**, for a worker on a machine only (the worker in the scheduler needs none). Generate a random
   secret of 32 characters or more (`openssl rand -hex 32`) and set it in two places:
   - as `LAUNCHPAD_TASKMARKET_WORKER_KEY` on the platform (Vercel), next to `LAUNCHPAD_TASKMARKET=1`;
   - in the worker's environment.

   It authenticates the worker to `/api/taskmarket` and moves no money.

## Running it on a machine

Put the configuration in `~/.config/launchpad/taskmarket.env` (mode 600). It holds paths and addresses, no
private key:

```sh
LAUNCHPAD_URL=https://<the site>
LAUNCHPAD_TASKMARKET_WORKER_KEY=<the worker key>
TASKMARKET_REQUESTER_HOME=/home/agent/.launchpad/taskmarket/requester
TASKMARKET_EVALUATOR_HOME=/home/agent/.launchpad/taskmarket/evaluator
TASKMARKET_DISPUTE_RESOLVER=0x...
# Optional:
# TASKMARKET_API_URL=            unset: production; https://testnet-api-market.daydreams.systems: the testnet
# TASKMARKET_REWARD_USDC=0.01    the escrow per task
# TASKMARKET_AWARD_USDC=         per winner; default 0.000001 on the testnet, 0 on mainnet (0 closes without awards)
# TASKMARKET_HOOK=0x...          our LaunchpadV2: the only hook the worker posts with, when the platform asks for it
# TASKMARKET_RPC_URL=            to read the diamond and the launchpad; default https://sepolia.base.org on the testnet, https://mainnet.base.org otherwise
# TASKMARKET_DIAMOND=            default: the testnet registry's diamond, or 0xDDc6cC3e4D11c1f3527B867C7DAD4ED9869C33f7 on mainnet
# TASKMARKET_TASK_URL=           the task's link on the round page; default …/api/tasks/{taskId} on the testnet, https://taskmarket.dev/tasks/{taskId} (unverified) on mainnet
# TASKMARKET_BIN=taskmarket
```

Then, from a checkout of the repository:

```sh
set -a; . ~/.config/launchpad/taskmarket.env; set +a
npm run taskmarket -- check    # both accounts, their legal acceptance, the appeal window floor read on chain
npm run taskmarket -- once     # the jobs due, one sync, then exit
npm run taskmarket -- run      # the same every 60 s (--interval <s>); keep it in a tmux or a systemd user unit
```

The worker passes the CLI only its account `HOME`, `TASKMARKET_API_URL`, an idempotency key for a write, and basic
process/network settings (such as `PATH` and proxy or CA configuration). It does not pass the worker key, keystore
environment variables, database credentials or other site settings into the CLI. Configure the account and API
origin as above; unrelated environment variables on the worker machine will not configure the child CLI.

- **The appeal window** is the protocol's floor, read from the diamond's `minAppealWindowSecs()` at start
  (300 s on mainnet on 5 Oct). It is passed in hours, rounded up: `--appeal-window 0.0834`.
- **The evaluation window** covers the rest of the round (reveal, ranking, pick), plus three days, plus the 30 days a
  creator may move the build deadline on (#209).

## The testnet (Base Sepolia)

Daydreams' Taskmarket testnet ([`taskmarket.84532.json`](../../contracts/deployments/taskmarket.84532.json)) runs the
same diamond as mainnet (the same 89 selectors), with its default DREAMS hook, Circle's test USDC
(`0x036C…CF7e`) and a fee of 5%. The whole round, with our launchpad as its hook, was rehearsed against it on a fork
(`sh ops/testnet/deploy-v2.sh rehearse-taskmarket`, [DEPLOYMENT.md](../contracts/DEPLOYMENT.md)).

What Thomas does, once, on a machine of his (an agent never runs these):

```sh
export TASKMARKET_API_URL=https://testnet-api-market.daydreams.systems
npm install -g @lucid-agents/taskmarket@1.13.2
mkdir -p ~/.launchpad/taskmarket/testnet-requester ~/.launchpad/taskmarket/testnet-evaluator
chmod 700 ~/.launchpad/taskmarket/testnet-*
HOME=~/.launchpad/taskmarket/testnet-requester taskmarket init
HOME=~/.launchpad/taskmarket/testnet-evaluator taskmarket init
HOME=~/.launchpad/taskmarket/testnet-requester taskmarket legal status    # read the four documents
HOME=~/.launchpad/taskmarket/testnet-requester taskmarket legal accept    # type I AGREE
HOME=~/.launchpad/taskmarket/testnet-evaluator taskmarket legal status
HOME=~/.launchpad/taskmarket/testnet-evaluator taskmarket legal accept
HOME=~/.launchpad/taskmarket/testnet-requester taskmarket address
HOME=~/.launchpad/taskmarket/testnet-evaluator taskmarket address
HOME=~/.launchpad/taskmarket/testnet-requester taskmarket deposit         # where to send the test USDC
```

**The legal bundle is a draft on the testnet (7 Oct).** `taskmarket legal status` answers:

```json
{"accepted": false, "bundleVersion": "2026-07-draft-3", "enforcementEnabled": false, "status": "draft"}
```

`legal accept` fails with "The current legal bundle is a counsel-review draft and cannot be accepted", and the API
requires no acceptance. The two `legal accept` lines above therefore fail there, and that is expected. While a bundle
is not enforced (`enforcementEnabled: false`), `npm run taskmarket -- check` and the e2e run count the account as
ready. They say "legal bundle <version> is a draft, not enforced: no acceptance possible". Once Taskmarket enforces a
bundle, it must be accepted as above. A write Taskmarket refuses for it (403 `LEGAL_ACCEPTANCE_REQUIRED`) still stops
the worker with exit code 2.

Then:

- **Test USDC.** About 1 USDC on the requester and 0.1 on the evaluator, from Circle's faucet (Base Sepolia), to the
  addresses `deposit` prints. Check with `taskmarket wallet balance`.
- **The launchpad knows the accounts.** Once the orchestrator has deployed LaunchpadV2 against the testnet, its owner
  names the two addresses:

  ```sh
  DEPLOYER_KEY_FILE=~/.config/launchpad/v2-deployer.key REQUESTER=<requester address> EVALUATOR=<evaluator address> \
    sh ops/testnet/deploy-v2.sh roles
  ```

- **The worker's settings** (`~/.config/launchpad/taskmarket.env`): `TASKMARKET_API_URL` as above, the two HOMEs,
  `TASKMARKET_HOOK=<the launchpad>`, and a dispute resolver that is neither account (the deployer's address will do on
  the testnet). `npm run taskmarket -- check` then shows both accounts, their legal acceptance and the launchpad's
  roles.

**Optional: an entrant account**, so that the end-to-end run below covers the evaluator's verdict. Without it, no
winner submits on the task, and the run covers the fallback (cancel, direct settlement). It is a third account whose
key the run also holds in a file, since the entrant's wallet must sign the site's wallet binding and its sealed entry:

```sh
umask 077
cast wallet new --json | jq -r '.data[0].private_key' > ~/.config/launchpad/testnet-entrant.key
mkdir -p ~/.launchpad/taskmarket/testnet-entrant && chmod 700 ~/.launchpad/taskmarket/testnet-entrant
TASKMARKET_IMPORT_KEY="$(cat ~/.config/launchpad/testnet-entrant.key)" HOME=~/.launchpad/taskmarket/testnet-entrant taskmarket wallet import
HOME=~/.launchpad/taskmarket/testnet-entrant taskmarket legal status
HOME=~/.launchpad/taskmarket/testnet-entrant taskmarket legal accept
```

### The end-to-end run on the testnet

`scripts/e2e.mjs` with `LAUNCHPAD_E2E_CHAIN=1` and `LAUNCHPAD_E2E_TASKMARKET=1`. It is for the orchestrator, after the steps above.

- **The platform** runs with `LAUNCHPAD_CHAIN=1`, `LAUNCHPAD_TASKMARKET=1` and `LAUNCHPAD_TASKMARKET_WORKER_KEY`.
- **The run's environment** has:
  - the worker's settings (`taskmarket.env`, with `TASKMARKET_HOOK`);
  - `TASKMARKET_API_URL` set to the testnet's API;
  - optionally `TASKMARKET_ENTRANT_HOME` and `TASKMARKET_ENTRANT_KEY_FILE=~/.config/launchpad/testnet-entrant.key`.

The run checks the accounts first. It stops with `skipped` and exit code 0, saying what to do, when an account is
missing, has not accepted legal terms that Taskmarket enforces, or has too little test USDC:

- 0.02 on the requester;
- 0.002 on the evaluator.

Then it runs the worker (`once`) at each step and checks:

1. the task bound to round 1 on the launchpad;
2. the entrant's submission, once admitted;
3. the round paid on chain:
   - with an entrant, by the task's completion (`RoundSettled.direct = false`);
   - without one, directly.

The reveal window is 20 minutes by default (`E2E_REVEAL_SECONDS`). A task is posted only with 15 minutes left.

## When it stops or a job waits

- **Legal acceptance missing** (on a machine, exit code 2; in the scheduler, the log line `taskmarket worker: not
  running`): "Taskmarket refuses our requester account: legal acceptance required …". The job is given back
  untouched. A person accepts the terms (setup step 3) and, for the worker in the scheduler, puts the keystore the CLI
  rewrote (it carries the receipt) back in its variable; the worker on a machine is restarted. The next attempt
  presents the same idempotency key.
- **A paid write that may have landed** (Taskmarket says it is in flight, answered 5xx, or said nothing):
  - The job turns `stuck`, and the project page shows it to the creator and the admins with its error. **Do
    not pay again.**
  - **Every pass reads the task of a stuck verdict, finalize or cancel again.** Taskmarket can confirm a write on chain
    and record it minutes later. It then answers a 500 ("…was confirmed on chain but recording it did not complete; it
    will be retried automatically") and shows the old status meanwhile. Once `task get` shows what the write leads to,
    the job is done, nothing is paid again, and the round's next jobs go on:
    - `appealing` after a verdict;
    - `completed` after a finalize;
    - `cancelled` or `expired` after a cancel.

    A stuck post has no task to read yet, and waits for a person.
  - Otherwise, look on Taskmarket first. For example, `HOME=<requester> taskmarket inbox` for a create.
  - Then an admin presses **Retry job #N** on the project page (logged; the creator does not see it), or runs
    `npm run taskmarket -- retry <jobId>` with the worker key. The job runs again under **the same idempotency key
    and, for a post, the same request**, so a write that landed is returned, not repeated. A post that was sent is
    never dropped, even when its round settles meanwhile: the evaluation waits behind it, then records and
    closes the task it created.
- **A definite failure** (a 4xx, a validation error): the job is retried on a later pass (a minute on) with a new
  idempotency key. After 8 attempts it turns `failed` and waits for a retry.
- **Waiting for the task's deadline** (the chain off): a round with winners that settled before its task's deadline
  has its verdict wait for it ("the task takes submissions until …"). It counts no attempt.
- **Waiting for the chain** (with the chain on): a post waits for the launchpad to expect its task, and a verdict for
  the settlement recorded on chain. The job's last error says which. It is looked at again every minute and counts no
  attempt. A long wait means the mirror is stuck: see `/admin/health`.
- **A hook the worker refuses** (a definite failure): the platform asks for a launchpad other than `TASKMARKET_HOOK`, or
  the launchpad expects another round. Nothing is sent to Taskmarket.
- **An appealed verdict** (`disputed`): finalize fails until our dispute resolver runs
  `taskmarket task resolve-dispute`.
- **A round's jobs run in order.** A job that waits holds the next jobs of the same round, never another
  round's.

## Checked, and not checked

- **Checked against** the CLI 1.13.2 source (its flags, its envelopes, its refusal of 0-USDC awards) and
  docs.taskmarket.dev.
- **Tested** with the fake CLI (`test/cloud/taskmarket.test.ts`):
  - a round posted at its opening, evaluated at its settlement and finalized after the appeal window;
  - a round closed without awards, before and after its deadline;
  - a missing legal acceptance;
  - a write in flight, retried by an admin from the project page;
  - every project listed by default, a project its creator stopped listing, one listed again mid-round;
  - a listed round closing its reveal early, its build deadline moved, its verdict waiting for its task's deadline;
  - the testnet's settings.
- **Tested in process** against a fake of Taskmarket's API that checks every x402 payment's signature
  (`test/cloud/taskmarket-http.test.ts`, `test/cloud/taskmarket-pass.test.ts`): the keystores decrypted through the
  key server, signed reads, paid writes under one idempotency key, the failures in doubt and the definite ones, and
  the scheduler's passes posting, evaluating and finalizing a round with the keystores in variables.
- **Tested with the chain** (`test/cloud/taskmarket-chain.test.ts`): the real LaunchpadV2 on anvil behind a diamond double with a
  DREAMS stand-in, the mirror, and the fake CLI writing to that diamond:
  - the post waiting for `expectTask`, then made with the hook;
  - a stranger's submission refused;
  - the verdict with the attestation, and the completion paying winners, held agents and judges;
  - the reconciliation agreeing;
  - a worker refusing a hook that is not its own;
  - the cancel and the direct settlement when no winner submitted.
- **Rehearsed on a fork of the real testnet diamond** (`deploy-v2.sh rehearse-taskmarket`): the same calls through
  Daydreams' forwarder, and the hook's refusals.
- **Not checked with real Taskmarket** (the e2e run against the testnet answers these):
  - that CLI 1.13.2's `--hook` and `TASKMARKET_API_URL` reach the testnet as above;
  - the API's smallest reward;
  - whether a decimal `--duration` and `--appeal-window` reach the contract as expected;
  - the address of a task's page;
  - how the evaluation window counts for a bounty;
  - whether `cancel` works after the deadline (the worker uses `refund-expired` there, as the docs say).