@officexapp/vidfarm-devcli 0.21.62 → 0.21.63

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/.agents/skills/dollarplatoon-skill/SKILL.md +163 -1174
  2. package/.agents/skills/dollarplatoon-skill/SOURCE.md +62 -0
  3. package/.agents/skills/dollarplatoon-skill/skill/clients.md +234 -0
  4. package/.agents/skills/dollarplatoon-skill/skill/feeds.md +326 -0
  5. package/.agents/skills/dollarplatoon-skill/skill/gigs.md +395 -0
  6. package/.agents/skills/dollarplatoon-skill/skill/gigworkers.md +324 -0
  7. package/.agents/skills/dollarplatoon-skill/skill/orders.md +573 -0
  8. package/.agents/skills/dollarplatoon-skill/skill/payouts.md +234 -0
  9. package/.agents/skills/dollarplatoon-skill/skill/platform.md +174 -0
  10. package/.agents/skills/dollarplatoon-skill/skill/prices.md +75 -0
  11. package/.agents/skills/dollarplatoon-skill/skill/pricing-and-tags.md +255 -0
  12. package/.agents/skills/dollarplatoon-skill/skill/proofs.md +555 -0
  13. package/.agents/skills/dollarplatoon-skill/skill/queue.md +404 -0
  14. package/.agents/skills/dollarplatoon-skill/skill/quickstart.md +191 -0
  15. package/.agents/skills/dollarplatoon-skill/skill/staging.md +178 -0
  16. package/.agents/skills/dollarplatoon-skill/skill/tasks.md +588 -0
  17. package/.agents/skills/dollarplatoon-skill/skill/web-pages.md +586 -0
  18. package/.agents/skills/vidfarm/SKILL.md +3 -3
  19. package/.agents/skills/vidfarm/references/core-workflows.md +39 -0
  20. package/SKILL.director.md +42 -3
  21. package/SKILL.md +3 -1
  22. package/clipper.md +20 -0
  23. package/dist/src/cli.js +50 -6
  24. package/dist/src/devcli/delivery-seal.js +119 -0
  25. package/dist/src/devcli/marketplace-console.js +1253 -0
  26. package/dist/src/devcli/marketplace-gigs.js +162 -16
  27. package/marketplace.md +275 -1
  28. package/package.json +24 -3
@@ -0,0 +1,588 @@
1
+ # Delivering tasks into a gig
2
+
3
+ How work gets from you into a worker's hands: the publisher webhook, inbound email, the
4
+ distribution modes, and the payload formats that serve humans and AI agents from one send.
5
+
6
+ ## Contents
7
+
8
+ - Routes
9
+ - The publisher webhook (preferred)
10
+ - Query params that shape a task
11
+ - Save a task as a draft
12
+ - Reserved and view-only tasks
13
+ - Comments on a task
14
+ - Payload format: JSON or HTML
15
+ - Dual-format HTML for humans AND agents
16
+ - How an agent should parse a task payload
17
+ - **Task escrow: funding a task before anyone works it**
18
+ - Inbound email
19
+ - Distribution modes
20
+ - Payload size limits
21
+ - When a task reaches nobody
22
+
23
+ ---
24
+
25
+ ## Routes
26
+
27
+ | Method | Path | Auth | Description |
28
+ |--------|------|------|-------------|
29
+ | POST | `/inbound/webhook/:gig_id?token=...` | Security token | Publisher task delivery |
30
+ | POST | `/inbound/webhook/:gig_id?token=...&draft=true` | Security token | Save a task without publishing it |
31
+ | POST | `/inbound/webhook/:gig_id?token=...&availability=reserved` | Security token | Publish a task no poll offers |
32
+ | POST | `/inbound/webhook/:gig_id?token=...&availability=view_only` | Security token | Publish a task nobody can claim |
33
+ | GET | `/gigs/:id/drafts` | Owner | List the gig's unpublished tasks |
34
+ | GET | `/gigs/:id/reserved` | Owner or member | What the gig is holding back from the queue |
35
+ | PATCH | `/gigs/:id/tasks/:msgId/draft` | Owner | Rewrite an unpublished task |
36
+ | POST | `/gigs/:id/tasks/:msgId/publish` | Owner | Publish a draft into the gig |
37
+ | PATCH | `/gigs/:id/tasks/:msgId/availability` | Owner | Open, reserve, or make it view-only |
38
+ | GET | `/gigs/:id/tasks/:msgId/comments` | Member | Read the task's comments |
39
+ | POST | `/gigs/:id/tasks/:msgId/comments` | Member | Comment, reply, or reply privately |
40
+ | DELETE | `/gigs/:id/tasks/:msgId/comments/:commentId` | Author or owner | Delete a comment |
41
+ | PATCH | `/gigs/:id/tasks/:msgId/comments-policy` | Owner | Who may read this task's comments |
42
+ | POST | `/inbound/email` | No | Inbound email hook (the mail provider calls this) |
43
+ | DELETE | `/gigs/:id/tasks/:taskId` | Owner | Delete a stored task (a draft included) |
44
+
45
+ There is also a no-login web form for humans at
46
+ `https://dollarplatoon.com/insert/{gig_id}?token={token}` — same destination, shareable and
47
+ framable. See [web-pages.md](https://dollarplatoon.com/skill/web-pages.md).
48
+
49
+ **Every row above assumes the gig owner is the one sending work.** On an `inbound_order` gig that
50
+ is false: the sender is an outside participant, most of these routes change hands or are refused,
51
+ and `GET /gigs/:id/drafts` answers the participant their own unsent orders. See
52
+ [orders.md](https://dollarplatoon.com/skill/orders.md).
53
+
54
+ ## The publisher webhook (preferred)
55
+
56
+ **Prefer the webhook over email for anything automated.** It is instant, it takes structured
57
+ data, you control the exact format, and it is the only path that can carry a task's price, tags,
58
+ priority, or assignee.
59
+
60
+ ```bash
61
+ curl -X POST "https://dollarplatoon.com/api/inbound/webhook/GIG_01HX...?token=abc123" \
62
+ -H "Content-Type: application/json" \
63
+ -d '{"task":"Comment on this thread","url":"https://reddit.com/r/example/comments/abc"}'
64
+ ```
65
+
66
+ ```json
67
+ → { "status": "forwarded", "targets": 3,
68
+ "message_ids": ["TASK_01HX...", "TASK_01HY...", "TASK_01HZ..."] }
69
+ ```
70
+
71
+ `message_ids` lists every task row the push created — one per recipient. `round_robin`, `random`
72
+ and `priority_weighted` pick a single mailbox, so they return one id plus a singular `message_id`;
73
+ `free_for_all` writes one copy per matching worker and returns them all.
74
+
75
+ Use those ids immediately to attach a private brief:
76
+ `PATCH /gigs/:id/tasks/:msgId/private-details`. See
77
+ [queue.md](https://dollarplatoon.com/skill/queue.md).
78
+
79
+ A wrong or missing `token` returns `403`.
80
+
81
+ ## Query params that shape a task
82
+
83
+ The request **body is the task payload**, so everything about the task rides on the query string.
84
+
85
+ | Param | Effect |
86
+ |---|---|
87
+ | `token` | The gig security token. Required. |
88
+ | `subject` | Sets the subject line. |
89
+ | `tags` | Comma-separated task tags: `tags=shortform,urgent`. Falls back to the gig's `default_task_tags`. |
90
+ | `price` | A number (`price=2.50`) or `price=tbd`. Omit for the gig price. |
91
+ | `priority` | Queue position, lower is polled sooner. Queue gigs only. Omit for 1000. |
92
+ | `assign_to` | A mailbox id or the worker's **account email** — hands the task to that one person. |
93
+ | `draft` | `draft=true` saves the task instead of delivering it. See below. |
94
+ | `draft_id` | With `draft=true`, overwrites that draft instead of making a new one. |
95
+ | `availability` | `reserved` keeps the task out of every poll; `view_only` makes it unclaimable. Queue gigs only. See below. |
96
+ | `reserved_for` | With `availability=reserved`: a mailbox id or account email. Only that worker polls or claims it. |
97
+
98
+ ```bash
99
+ POST /inbound/webhook/GIG_abc?token=...&price=2.50&tags=shortform,urgent&priority=0
100
+ POST /inbound/webhook/GIG_abc?token=...&price=500&assign_to=winner@example.com
101
+ ```
102
+
103
+ `?priority=` and `?assign_to=` are mutually exclusive — an assigned task has no queue position.
104
+ An assigned push returns a different shape:
105
+
106
+ ```json
107
+ { "status": "assigned", "message_id": "TASK_01KV...", "mailbox_id": "MBX_01KR5..." }
108
+ ```
109
+
110
+ Details on pricing and tags: [pricing-and-tags.md](https://dollarplatoon.com/skill/pricing-and-tags.md).
111
+ Details on assignment and ordering: [queue.md](https://dollarplatoon.com/skill/queue.md).
112
+
113
+ ## Save a task as a draft
114
+
115
+ Add `?draft=true` and the task is **saved, not delivered**. No worker can see it, no webhook
116
+ fires, and the gig's counters do not move. It is the same door and the same query string — only
117
+ the delivery is deferred.
118
+
119
+ ```bash
120
+ curl -X POST "https://dollarplatoon.com/api/inbound/webhook/GIG_abc?token=abc123&draft=true&price=2.50&tags=shortform" \
121
+ -H "Content-Type: text/plain" \
122
+ --data "Draft this brief later"
123
+ ```
124
+
125
+ ```json
126
+ → { "status": "draft", "message_id": "TASK_01KV...", "updated": false,
127
+ "draft_saved_at": "2026-08-21T10:04:00.000Z",
128
+ "note": "This task is a draft. No worker can see it until you publish it with POST /gigs/:id/tasks/:msgId/publish." }
129
+ ```
130
+
131
+ Save the same draft again with `&draft_id=TASK_01KV...`, and it is overwritten rather than
132
+ duplicated. Every save is a **full replace** of the form, so send every field the draft should
133
+ keep. Two fields are the exception, because they have their own routes and survive a save:
134
+ `private_details` and your alias for the task.
135
+
136
+ Then, as the gig owner:
137
+
138
+ | Do this | Route |
139
+ |---|---|
140
+ | List every draft, newest first | `GET /gigs/:id/drafts` |
141
+ | Rewrite one | `PATCH /gigs/:id/tasks/:msgId/draft` |
142
+ | Attach the private brief | `PATCH /gigs/:id/tasks/:msgId/private-details` |
143
+ | Publish it | `POST /gigs/:id/tasks/:msgId/publish` |
144
+ | Throw it away | `DELETE /gigs/:id/tasks/:taskId` |
145
+
146
+ `PATCH .../draft` takes the whole form in a JSON body — `payload`, `subject`, `price`, `tags`,
147
+ `assign_to`, `priority`, `json`, `private_details` — and validates every value exactly as the
148
+ webhook query string does, so a draft can never hold something that would be refused at publish.
149
+
150
+ Publishing runs the ordinary delivery path, so the answer is the ordinary one — `queued`,
151
+ `assigned` or `forwarded` — plus `draft_id` and `published: true`:
152
+
153
+ ```json
154
+ → { "status": "queued", "message_id": "TASK_01KW...", "priority": 1000,
155
+ "draft_id": "TASK_01KV...", "published": true }
156
+ ```
157
+
158
+ **The task gets a new id when it publishes.** Address the published task by the id in
159
+ `message_id`/`message_ids`, not by the draft id.
160
+
161
+ If nobody matched the task, it is **not** published and the draft is kept:
162
+
163
+ ```json
164
+ → { "status": "dropped", "reason": "no_matching_mailboxes",
165
+ "draft_id": "TASK_01KV...", "draft_kept": true }
166
+ ```
167
+
168
+ Fix its tags or its price and publish again. A draft cannot be assigned, recycled, reordered or
169
+ claimed — those routes answer `409` and tell you to publish it first.
170
+
171
+ In the web app: the **Insert Task** form has a *Save as Draft* button, and the gig owner's copy
172
+ of the form lists the gig's saved drafts to reopen, publish, or delete.
173
+
174
+ ## Reserved and view-only tasks
175
+
176
+ A queued task is offered to everybody: any member's poll can take it, and a claim link is spent
177
+ by the first person who opens it. Two other states say something different about the same task.
178
+
179
+ | `availability` | Polls | The task link | Use it for |
180
+ |---|---|---|---|
181
+ | `open` (default) | every member | first opener takes it | ordinary queue work |
182
+ | `reserved` | **nobody** | first opener takes it | handing tasks out one at a time by link |
183
+ | `reserved` + `reserved_for` | **only that worker** | **only that worker** | setting work aside for somebody |
184
+ | `view_only` | nobody | nobody can claim it | a brief to read, discuss, or bid on |
185
+
186
+ None of them is a draft. A draft is invisible; all three of these are published, listed, priced,
187
+ tagged, readable by every member, and open to comments.
188
+
189
+ ### Reserved — out of the queue, handed out by link
190
+
191
+ Reserving is the common case. The client stocks the gig with work but does not want it swept up
192
+ by whoever polls first — they send each task to the worker they have in mind.
193
+
194
+ ```bash
195
+ curl -X POST "https://dollarplatoon.com/api/inbound/webhook/GIG_abc?token=abc123&availability=reserved&price=25" \
196
+ -H "Content-Type: application/json" \
197
+ -d '{"task":"Edit this clip","url":"https://…"}'
198
+ ```
199
+
200
+ ```json
201
+ → { "status": "reserved", "message_id": "TASK_01KW...", "priority": 1000, "reserved_for": null,
202
+ "note": "Reserved. No poll will offer it — send the task link to whoever should take it." }
203
+ ```
204
+
205
+ Then share `https://dollarplatoon.com/claim/GIG_abc/TASK_01KW...` (add `?invite=` for somebody
206
+ who has not joined). No poll competes for it, so the link can sit in an inbox for a week.
207
+
208
+ ### Reserved for one worker — their poll, and only theirs
209
+
210
+ Add `reserved_for` (a mailbox id **or** the worker's account email) and the task waits on that
211
+ one worker's shelf. Their next poll delivers it; nobody else's poll ever sees it, and anybody
212
+ else opening the link is told `409 { "reason": "reserved_for_other" }`.
213
+
214
+ ```bash
215
+ curl -X POST ".../inbound/webhook/GIG_abc?token=abc123&availability=reserved&reserved_for=ana@example.com" \
216
+ -H "Content-Type: application/json" -d '{"task":"Ana'\''s usual Friday edit"}'
217
+ ```
218
+
219
+ **This is not assigning.** `?assign_to=` puts the task in the worker's hands right now and starts
220
+ its expiry clock; a reservation waits until they take it. Use `assign_to` for "do this now", and
221
+ `reserved_for` for "this is yours when you want it".
222
+
223
+ A worker's poll serves their reservations **first**, ahead of the shared queue, and ignores the
224
+ queue order and their own tag filter while doing it — the client named them for this task, so a
225
+ standing preference must not silently drop it. Each such task comes back with
226
+ `reserved_for_me: true`. What they cannot do is dodge a limit: reservations count against the
227
+ rate limit and the open-task cap exactly like any other claim.
228
+
229
+ If that worker **skips** the task, or the owner **recycles** it away from them, the name is
230
+ released and the task returns to the gig's unaddressed reservations. It never falls into the open
231
+ queue — the client held it back on purpose, and a skip is not permission to give it to anybody.
232
+
233
+ ### View only — nobody claims it
234
+
235
+ ```bash
236
+ curl -X POST ".../inbound/webhook/GIG_abc?token=abc123&availability=view_only&price=50&tags=bid" \
237
+ -H "Content-Type: text/plain" \
238
+ --data "Bid on this: 30s vertical edit, footage supplied. Comment with your price."
239
+ ```
240
+
241
+ | Route | On a view-only task |
242
+ |---|---|
243
+ | `POST /gigs/:id/queue/poll` | never returns it |
244
+ | `POST /gigs/:id/queue/:msgId/claim` | `409 { "reason": "view_only" }` |
245
+ | `POST /gigs/:id/queue/:msgId/decline` \| `.../violation` | `409 { "reason": "view_only" }` |
246
+ | `POST /gigs/:id/tasks/:msgId/assign` | `409 { "reason": "view_only" }` |
247
+ | `POST /gigs/:id/proofs` | `409 { "reason": "view_only" }` |
248
+ | `GET /gigs/:id/tasks/:msgId` | works, for any member of the gig |
249
+ | the comments routes | work |
250
+
251
+ ### Changing a task's availability
252
+
253
+ ```bash
254
+ curl -X PATCH "https://dollarplatoon.com/api/gigs/GIG_abc/tasks/TASK_01KW.../availability" \
255
+ -H "x-api-key: $KEY" -H "Content-Type: application/json" \
256
+ -d '{"availability": "reserved", "reserved_for": "ana@example.com"}'
257
+ ```
258
+
259
+ `{"availability": "open"}` puts it back in the queue. The task keeps its queue **position**
260
+ throughout, so open → reserved → open returns it to where it was rather than to the end of the
261
+ line. A task somebody is already holding answers `409 { "reason": "claimed" }` — recycle it
262
+ first. A `reserved_for` naming somebody with no mailbox in this gig answers `404`, immediately,
263
+ rather than leaving a task nobody will ever be offered.
264
+
265
+ Reserved and view-only tasks drop out of `GET /gigs/:id/queue` and the queue counts, because
266
+ neither is queued work. Two places still list them:
267
+
268
+ | Route | Shows |
269
+ |---|---|
270
+ | `GET /gigs/:id/reserved` | owner: every reservation in the gig. Worker: only the ones waiting for them |
271
+ | `GET /gigs/:id/dashboard` | owner: every task in the gig, whatever its state |
272
+
273
+ In the web app: the task pane in the gig dashboard has a **Who can take it** dropdown and, for a
274
+ reservation, a **Held for** picker. The share dialog beside them says what the link will do, and
275
+ writes `/task/:gig_id/:task_id` for a view-only task. Both it and the **⋯** copy buttons put a
276
+ gig invite on the link (`?invite=…`), so a worker who has not joined can still open the task.
277
+
278
+ ## Comments on a task
279
+
280
+ Every task carries a comment thread. Two levels: a comment, and replies under it.
281
+
282
+ ```bash
283
+ # read
284
+ curl "https://dollarplatoon.com/api/gigs/GIG_abc/tasks/TASK_01KW.../comments" -H "x-api-key: $KEY"
285
+
286
+ # write
287
+ curl -X POST "https://dollarplatoon.com/api/gigs/GIG_abc/tasks/TASK_01KW.../comments" \
288
+ -H "x-api-key: $KEY" -H "Content-Type: application/json" \
289
+ -d '{"body": "I can do this by Friday for $40."}'
290
+
291
+ # reply
292
+ -d '{"body": "Deal.", "parent_id": "TCOMMENT_01KW..."}'
293
+
294
+ # reply PRIVATELY — only the person you are replying to can read it
295
+ -d '{"body": "You won. Invite: https://…", "parent_id": "TCOMMENT_01KW...", "private": true}'
296
+ ```
297
+
298
+ **Who reads what** is set per task, and falls back to the gig's `default_comments_policy`, and
299
+ then to `private`:
300
+
301
+ | Policy | The gig owner | A member |
302
+ |---|---|---|
303
+ | `public` | reads and writes every thread | reads and writes every thread |
304
+ | `private` | reads and writes every thread | writes; reads **only the thread they started** |
305
+ | `off` | nothing | nothing |
306
+
307
+ `private` is the default because a queue is read by many workers at once: it lets you collect
308
+ twelve independent answers to the same task without handing the first one to the other eleven. A
309
+ comment the owner writes as a **root** is the exception — it is a prompt to the room, so every
310
+ member reads it, while each answer under it stays private to its author.
311
+
312
+ ```bash
313
+ # this task only
314
+ curl -X PATCH ".../gigs/GIG_abc/tasks/TASK_01KW.../comments-policy" \
315
+ -d '{"policy": "public"}' # or "private", "off", or null to follow the gig
316
+
317
+ # every task in the gig that has not chosen for itself
318
+ curl -X PATCH ".../gigs/GIG_abc" -d '{"default_comments_policy": "public"}'
319
+ ```
320
+
321
+ **A private reply has exactly two readers** — its author and the person it answers — whatever the
322
+ policy says, and **including the gig owner**, who cannot read one they are not part of.
323
+
324
+ ### The bidding round, end to end
325
+
326
+ That is what makes this work, and it is the pattern the whole feature is shaped around:
327
+
328
+ 1. Publish the task **`view_only`** and share its **read-only** link (`/task/:gig_id/:task_id`).
329
+ Nobody can take it, so the link is safe in a group chat or a feed.
330
+ 2. Set the comments **`public`**. Workers bid where everybody can see the offers.
331
+ 3. Pick a winner and **reserve the task for them**:
332
+ `PATCH .../availability {"availability":"reserved","reserved_for":"ana@example.com"}`.
333
+ 4. Send them the **claim** link as a **private reply**. Nobody else can read it, and nobody else
334
+ can use it — a losing bidder who somehow got the URL is told
335
+ `409 { "reason": "reserved_for_other" }`.
336
+
337
+ Reserve **before** you send the link, so it is already inert in anybody else's hands by the time
338
+ it exists. In the dashboard all of step 3 and 4 is one action: **⋯ → Give task to \<name\>** on
339
+ that worker's comment.
340
+
341
+ Comments read by the gig owner carry `author_mailbox_id`, which is what step 3 needs. No other
342
+ reader gets it: on a public bidding thread it would tell each bidder exactly who they are bidding
343
+ against.
344
+
345
+ The response is a flat list; every reply carries `thread_id`, and what comes back is already
346
+ filtered to what you may read.
347
+
348
+ ```json
349
+ → { "policy": "public", "can_post": true, "total": 3,
350
+ "comments": [
351
+ { "id": "TCOMMENT_01KW...", "thread_id": null, "body": "I can do this by Friday for $40.",
352
+ "author_display_name": "ana", "author_role": "gigworker", "is_mine": false,
353
+ "author_mailbox_id": "MBX_01KR5...",
354
+ "private": false, "deleted": false, "created_at": "2026-08-25T10:00:00.000Z" },
355
+ { "id": "TCOMMENT_01KX...", "thread_id": "TCOMMENT_01KW...", "body": "You won. Invite: https://…",
356
+ "author_role": "client", "is_mine": true, "private": true,
357
+ "private_to_display_name": "ana", "created_at": "2026-08-25T10:05:00.000Z" }
358
+ ] }
359
+ ```
360
+
361
+ Task reads carry `availability` and `reserved_for`. Owner-facing ones (the dashboard,
362
+ `GET /gigs/:id/tasks/:msgId` as the owner) add `comments_policy`, `comments_policy_source`
363
+ (`"task"` or `"gig"`), and `comment_count` — the number of comments in the **open**
364
+ conversation, which is why a private reply does not move it.
365
+
366
+ Bylines are **display names, never email addresses**, and `author_mailbox_id` is present only
367
+ when the gig owner is reading. A comment is at most 4000 characters, and a task holds at most
368
+ 1000 comments. Deleting one leaves a tombstone (`deleted: true`) so its replies
369
+ keep their place. A caller who has not joined the gig gets
370
+ `403 { "reason": "not_a_member" }` — the task page turns that into a join link.
371
+
372
+ Every comment that needs an answer writes a notification: see
373
+ [web-pages.md](https://dollarplatoon.com/skill/web-pages.md).
374
+
375
+ ## Payload format: JSON or HTML
376
+
377
+ The `Content-Type` header decides how the body is parsed.
378
+
379
+ | Your audience | Content-Type | Why |
380
+ |---|---|---|
381
+ | **Only AI agents** | `application/json` | Send structured JSON. Agents parse it natively. No HTML needed. |
382
+ | **Only humans** | `text/html` | Rich layout, click-to-copy fields, buttons. |
383
+ | **Mixed or unknown** | `text/html` | HTML with an embedded hidden JSON input, so one payload serves both. |
384
+
385
+ HTML and plain text are stored with `type: "email"` and rendered as formatted HTML in the web
386
+ app, exactly like a mail-sourced task.
387
+
388
+ **If your gig is 100% agents, just send JSON.** Do not add HTML you do not need.
389
+
390
+ ## Dual-format HTML for humans AND agents
391
+
392
+ When humans might be involved, design one payload that works for both. Four principles:
393
+
394
+ 1. **Readable layout** — headings and hierarchy, so a human understands the job at a glance.
395
+ 2. **Click-to-copy inputs** for anything they must copy:
396
+ `<input type="text" value="..." readonly onclick="this.select()">`.
397
+ 3. **Buttons that open in a new tab** for URLs they must visit:
398
+ `<a href="..." target="_blank" rel="noopener">`.
399
+ 4. **A hidden JSON input for agents** — always the same name, `agent_data`, so it is trivially
400
+ findable.
401
+
402
+ ```html
403
+ <div style="font-family: sans-serif; max-width: 600px;">
404
+ <h2>Post a comment on this Reddit thread</h2>
405
+
406
+ <p><strong>Thread URL:</strong></p>
407
+ <input type="text" value="https://reddit.com/r/example/comments/abc123"
408
+ readonly onclick="this.select()"
409
+ style="width:100%; padding:8px; border:1px solid #ccc; border-radius:4px; cursor:pointer;">
410
+
411
+ <p><strong>Comment text to post:</strong></p>
412
+ <input type="text" value="This product changed my workflow completely."
413
+ readonly onclick="this.select()"
414
+ style="width:100%; padding:8px; border:1px solid #ccc; border-radius:4px; cursor:pointer;">
415
+
416
+ <a href="https://reddit.com/r/example/comments/abc123" target="_blank" rel="noopener"
417
+ style="display:inline-block; padding:10px 20px; background:#0079d3; color:#fff;
418
+ text-decoration:none; border-radius:6px; font-weight:bold;">
419
+ Open Thread in New Tab
420
+ </a>
421
+
422
+ <p style="color:#888; font-size:12px;">After posting, submit a proof with a link to your comment.</p>
423
+
424
+ <!-- Structured JSON for AI agents — invisible to humans, trivial for agents to extract -->
425
+ <input type="hidden" name="agent_data" value='{"task_type":"reddit_comment","thread_url":"https://reddit.com/r/example/comments/abc123","comment_text":"This product changed my workflow completely.","proof_requirements":["comment_permalink"],"task_id":"task_001"}'>
426
+ </div>
427
+ ```
428
+
429
+ - The **human** sees a clean task with copy fields and a button. The hidden input is invisible.
430
+ - The **agent** reads `input[name="agent_data"]`, parses the `value` as JSON, and gets
431
+ `task_type`, `thread_url`, `comment_text`, `proof_requirements`, `task_id` with no HTML parsing.
432
+ - The **AI-assisted human** gets both from the same payload.
433
+
434
+ ## How an agent should parse a task payload
435
+
436
+ 1. If the payload is JSON (`type: "webhook"`), parse it directly — it is already structured.
437
+ 2. If it is HTML (`type: "email"`), look for `input[name="agent_data"]` and parse its `value`.
438
+ 3. If there is no `agent_data`, fall back to the visible text.
439
+ 4. For `task_identifier` when you submit the proof: on a **queue** gig use the polled task's `id`
440
+ (that is what claims it to you); otherwise use the `task_id` from the JSON or the task's
441
+ unique reference.
442
+
443
+ ## Task escrow: funding a task before anyone works it
444
+
445
+ **Off by default, and off on every gig that has ever existed.** Turn it on with
446
+ `PATCH /gigs/:id { "task_escrow": true }`.
447
+
448
+ Normally a gig holds one shared pot and a payout is measured against it at approval time — so a
449
+ worker cannot know, while they work, whether that pot will still cover them. With `task_escrow`
450
+ on, each task's USDC is deposited into the Treasury **against that task alone, at the moment the
451
+ task is created**, and `payoutFromDeposits` settles it. Nothing else on the gig can spend it.
452
+
453
+ ### What a worker sees
454
+
455
+ Every task read carries these when the gig escrows:
456
+
457
+ ```json
458
+ { "escrow_funded": true,
459
+ "escrow_amount": 2.75, // the deposit: the payout PLUS the fee charged on top
460
+ "escrow_fee_bps": 1000, // the fee rate snapshotted at deposit time
461
+ "escrowed_at": "2026-08-29T09:12:44.108Z",
462
+ "deposit_id": "0x9f2c…" } // the on-chain id — PUBLIC on purpose
463
+ ```
464
+
465
+ `escrow_amount` is **not** the wage. `price` is the wage; the two differ by the platform fee,
466
+ which this gig pays on top. `deposit_id` is exposed so a worker can call `deposits(<id>)` on the
467
+ Treasury and confirm the amount, the gig and the `Open` state **without trusting this API**. That
468
+ verification is the point of the feature — use it.
469
+
470
+ A task that predates the flag, or whose escrow was released, reports `"escrow_funded": false`
471
+ rather than omitting the field. On a gig with `task_escrow` off, none of these keys appear at all.
472
+
473
+ ### What the flag requires before it can be switched on
474
+
475
+ | Requirement | Why |
476
+ |---|---|
477
+ | `distribution` is `queue`, `round_robin`, `priority_weighted` or `random` | One task must produce exactly one payable row. `free_for_all` and `queue_solo` produce several; `inbound_proof` produces none |
478
+ | A `price` above zero | `$0` cannot be escrowed, and the gig price is what an unpriced task escrows |
479
+ | `allow_price_offers` is off | An offer moves the payout **after** the deposit is fixed. Too high reverts "Exceeds funded amount", far too low reverts "Residue too large", and both are permanent |
480
+ | `DEPOSIT_ID_SECRET` is configured | Answers `503` otherwise |
481
+
482
+ Not available on `inbound_order`, which already escrows every order against the buyer's deposit.
483
+
484
+ ### Side effects you are opting into
485
+
486
+ - **Creating a task spends money.** The deposit confirms *before* the task row is written, so an
487
+ unfunded task never reaches the queue — but a delivery can now fail because the owner's hot
488
+ wallet is short of USDC or gas. Keep it funded.
489
+ - **Inbound email is refused** (`status: "ignored"`, `reason: "task_escrow_gig"`). That door's
490
+ credential travels through third-party mail servers, and creating a task now spends real money.
491
+ - **Bulk deletes are refused** — `DELETE /gigs/:id/queue/all` and `/inbound/all`. Each escrowed
492
+ task needs its own on-chain refund, and forty of those will not fit in one request. Delete them
493
+ one at a time.
494
+ - **The price is pinned per task** the moment it is escrowed, even when it equals `gig.price`.
495
+ Editing `gig.price` afterwards affects future tasks only. `PATCH .../tasks/:msgId/price` and
496
+ `PATCH .../queue/prices` refuse an escrowed task (`reason: "task_escrowed"`).
497
+ - **You cannot turn the flag off, or change `distribution`, while any deposit is still open.**
498
+ Both would aim escrowed work at the legacy payout path, which reverts "Reserved" permanently.
499
+ Let the open tasks finish first.
500
+
501
+ ### When the escrow moves
502
+
503
+ The escrow **freezes as soon as a delivery exists** against the task. Before that it simply rides
504
+ along — expiry, recycle and reassignment make no on-chain call at all.
505
+
506
+ | What happens | The escrow |
507
+ |---|---|
508
+ | Task expires, is recycled, or is reassigned | Stays with the task. No chain call |
509
+ | Worker submits a proof | **Freezes.** The task can no longer be deleted |
510
+ | Client approves | Settles to the worker through the normal rollup |
511
+ | Client rejects | Stays. The task returns to the queue **still funded** |
512
+ | Owner deletes an unproven task | Released back to the owner's wallet |
513
+
514
+ A worker's **draft** proof does not freeze anything — the client has never seen it, so a worker
515
+ cannot park a funded task by starting a draft and never sending it.
516
+
517
+ ### What it costs, exactly
518
+
519
+ The escrow is `price + floor(price × fee_bps / 10000)`, computed in 6-decimal USDC units with the
520
+ same integer division the contract uses. The residue is therefore **exactly zero** for every price
521
+ and every fee rate — the deposit covers the payout and the fee with nothing left stranded.
522
+
523
+ ## Inbound email
524
+
525
+ Every gig has an address: `{gig_id}_{token}.dollar-platoon@fwd.zoomgtm.com`. Mail sent to it
526
+ becomes a task and is distributed normally.
527
+
528
+ Email is the fallback, not the recommendation. It has one hard limitation worth planning around:
529
+ **an email has nowhere to carry `?tags=`**, so on an email gig every task arrives untagged — and
530
+ a worker with any tag filter would receive nothing, forever. Set `default_task_tags` on the gig
531
+ so untagged arrivals get stamped. Email tasks also always land at the gig price; reprice them
532
+ afterwards with `PATCH /gigs/:id/tasks/:msgId/price`.
533
+
534
+ ## Distribution modes
535
+
536
+ Set on the gig as `distribution`.
537
+
538
+ | Mode | Behaviour |
539
+ |---|---|
540
+ | `round_robin` | Cursor-based fair rotation through active mailboxes. One recipient. |
541
+ | `random` | Uniform random pick. One recipient. |
542
+ | `priority_weighted` | Weighted by each mailbox's `priority` (1–10, higher gets more). One recipient. |
543
+ | `free_for_all` | Every active mailbox receives a copy. |
544
+ | `queue` | Stored in a shared queue. Workers poll and claim. Nothing is pushed. |
545
+ | `queue_solo` | Shared queue, but each worker takes their own private copy. **Cost is price × workers.** |
546
+ | `inbound_proof` | No tasks distributed at all. Workers submit proofs directly. |
547
+ | `inbound_order` | **Inverted.** An outside participant sends and funds each task; the gig owner does the work. |
548
+
549
+ The push modes (`round_robin`, `random`, `priority_weighted`, `free_for_all`) respect each
550
+ worker's standing mailbox filters. The queue modes are covered in
551
+ [queue.md](https://dollarplatoon.com/skill/queue.md).
552
+
553
+ `inbound_proof` is the one people overlook: with no tasks at all, the **proof is the submission**.
554
+ That makes the gig an application inbox, a bounty board, or a tip line — and the approval
555
+ `feedback` is where you answer the person.
556
+
557
+ > **`inbound_order` is not a variation on this page — it is the reverse of it.** The gig owner
558
+ > does not send tasks; participants do, and each one arrives with its own USDC deposit behind it.
559
+ > Almost everything above changes hands or is refused there: the webhook may only save drafts
560
+ > (`?draft=true`, and only for an identified buyer), `?price=` and `?assign_to=` are rejected,
561
+ > inbound email answers `{"status":"ignored"}`, and the publish, the draft edit, the availability
562
+ > switch, the recycle, the assign and every delete are closed with `409 reason: "inbound_order"`.
563
+ > Read [orders.md](https://dollarplatoon.com/skill/orders.md) instead of adapting this page.
564
+
565
+ ## Payload size limits
566
+
567
+ Bodies are stored in full — nothing is silently truncated.
568
+
569
+ - Over **2,000,000 characters** → `413` with
570
+ `{ "error": "Payload too large", "received_chars": N, "max_chars": 2000000 }`. An oversized task
571
+ fails loudly instead of arriving with its tail missing.
572
+ - Over **6,000 characters** → stored off the message row. List endpoints then return the first
573
+ 1,000 characters with `payload_truncated: true`; fetch the whole body with
574
+ `GET /gigs/:id/tasks/:msgId`.
575
+
576
+ ## When a task reaches nobody
577
+
578
+ If a task matches no mailbox's filters, it is **dropped** and the publisher is told:
579
+
580
+ ```json
581
+ { "status": "dropped", "reason": "no_matching_mailboxes",
582
+ "active_mailboxes": 7, "matched": 0,
583
+ "evaluated": { "tags": ["thumbnail"], "price": 1 } }
584
+ ```
585
+
586
+ This is deliberately distinct from `no_active_mailboxes` ("nobody is here"), and it counts
587
+ separately on the gig as `inbound_dropped_no_match`. The two need opposite fixes: recruit a
588
+ worker, versus fix your tag vocabulary.