@officexapp/vidfarm-devcli 0.21.61 → 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 (29) 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 +28 -3
  29. package/public/assets/homepage-client-app.js +13 -13
@@ -0,0 +1,555 @@
1
+ # Proofs — submitting, reviewing, and sharing
2
+
3
+ A proof is the evidence a task was done. It is what triggers payment.
4
+
5
+ ## Contents
6
+
7
+ - Routes
8
+ - Submit a proof
9
+ - Drafts — save before sending, or take a proof back
10
+ - The proof lifecycle
11
+ - Review a proof
12
+ - Rejection tags and what they cost
13
+ - Report an auto-approved proof
14
+ - Private aliases
15
+ - Share links — submitting without an account
16
+ - Uploading files
17
+
18
+ ---
19
+
20
+ ## Routes
21
+
22
+ | Method | Path | Auth | Description |
23
+ |--------|------|------|-------------|
24
+ | POST | `/gigs/:id/proofs` | Worker | Submit a proof (`draft: true` saves without sending) |
25
+ | GET | `/gigs/:id/proofs` | Yes | List proofs, filterable by status |
26
+ | GET | `/gigs/:id/proofs/:proof_id` | Owner or submitter | One proof |
27
+ | PATCH | `/gigs/:id/proofs/:proof_id/draft` | Submitter | Edit an unsent draft |
28
+ | POST | `/gigs/:id/proofs/:proof_id/submit` | Submitter | Send a draft to the client |
29
+ | POST | `/gigs/:id/proofs/:proof_id/withdraw` | Submitter | Take a pending proof back to draft |
30
+ | DELETE | `/gigs/:id/proofs/:proof_id` | Submitter | Delete a draft |
31
+ | PATCH | `/gigs/:id/proofs/:proof_id` | Owner | Approve or reject |
32
+ | POST | `/gigs/:id/proofs/:proof_id/report` | Owner | Flag an auto-approved proof |
33
+ | PATCH | `/gigs/:id/proofs/:proof_id/tags` | Owner or submitter | Retag |
34
+ | PATCH | `/gigs/:id/proofs/:proof_id/alias` | Owner or submitter | Your own private title |
35
+ | POST | `/upload/presign` | Yes | Presigned S3 upload URL |
36
+
37
+ > **The "Owner" column reads differently on an order machine.** On an `inbound_order` gig the gig
38
+ > owner is the *vendor being judged*, so the verdict, the report and the proof reads belong to the
39
+ > **participant who paid**. The withdraw route is refused outright. Each difference is noted
40
+ > inline below and set out in full in
41
+ > [orders.md](https://dollarplatoon.com/skill/orders.md).
42
+
43
+ ## Submit a proof
44
+
45
+ ```json
46
+ POST /gigs/:id/proofs
47
+ {
48
+ "mailbox_id": "MBX_01HX...",
49
+ "task_identifier": "TASK_01HX...",
50
+ "proofs": ["https://reddit.com/r/...", "https://s3.../screenshot.png"],
51
+ "tags": ["batch_7"],
52
+ "private_note": "Licence key: ABC-123-XYZ",
53
+ "asking_price": 50
54
+ }
55
+ ```
56
+
57
+ ```json
58
+ → { "proof": { "id": "PROOF_01HX...", "status": "pending", "timeout_at": "...",
59
+ "locked_price": 2.5, "price_pending": false,
60
+ "private_note_locked": true },
61
+ "warning": "Warning: gig available funds are less than the task price" }
62
+ ```
63
+
64
+ **`task_identifier` is the field that gets this wrong.** It links the proof to the task it
65
+ fulfils:
66
+
67
+ | Gig type | What to send |
68
+ |---|---|
69
+ | `queue` | The polled task's `id`. The server uses it to atomically claim the queue item to you. |
70
+ | `queue_solo` | The `id` of **your private copy** — never `source_task_id`, which is shared and will be rejected. |
71
+ | Push modes, `inbound_proof` | The task's unique reference: a URL, a ticket id, the publisher's `task_id`. |
72
+
73
+ **Never use the subject line.** Subjects are not unique; collisions cause duplicate-submission
74
+ `409`s and missed payouts.
75
+
76
+ **Include verifiable evidence.** URLs, screenshots, permalinks — anything the client can check
77
+ independently. Unverifiable proofs get rejected.
78
+
79
+ **Price is locked here.** `locked_price` comes from the task's own price when it has one and from
80
+ the gig price otherwise. `locked_price: null` with `price_pending: true` means the task was TBD
81
+ and the amount is set at approval — it is not `$0`, and it settles at the gig price if the client
82
+ never names one. See [pricing-and-tags.md](https://dollarplatoon.com/skill/pricing-and-tags.md).
83
+
84
+ ## Name your own price
85
+
86
+ A gig with `allow_price_offers: true` lets the worker quote their own number:
87
+
88
+ ```json
89
+ POST /gigs/:id/proofs { ..., "asking_price": 50 }
90
+
91
+ → { "proof": { "id": "PROOF_01HX...", "locked_price": 10, "asking_price": 50 } }
92
+ ```
93
+
94
+ **An ask is a quote, never a payout.** `locked_price` still holds the task or gig price. The
95
+ client accepts by approving with `amount` equal to `asking_price`; the proof then records
96
+ `price_source: "worker"`. The client can also approve at the gig price, approve at a third
97
+ number, or reject the proof.
98
+
99
+ **Silence pays the gig price.** A review that times out settles at `locked_price`, so an ask can
100
+ never be won by a client who stops answering. Bulk approval in the dashboard does the same.
101
+
102
+ On a gig without the flag, `asking_price` returns `400`. Set it with
103
+ `PATCH /gigs/:id { "allow_price_offers": true }`.
104
+
105
+ The `warning` field appears when the gig's `available_funds` is below the task price. The proof is
106
+ still accepted and can still be approved, but it cannot be paid until the client deposits more.
107
+
108
+ ## Drafts — save before sending, or take a proof back
109
+
110
+ `draft` is a proof the client has not been shown. Two things put a proof there, and they are the
111
+ same state afterwards:
112
+
113
+ - **Save before sending** — `POST /gigs/:id/proofs` with `"draft": true`
114
+ - **Withdraw** — `POST /gigs/:id/proofs/:proof_id/withdraw` on a proof still `pending`
115
+
116
+ ```json
117
+ POST /gigs/:id/proofs
118
+ { "mailbox_id": "MBX_01HX...", "task_identifier": "TASK_01HX...",
119
+ "proofs": ["work in progress"], "draft": true }
120
+
121
+ → { "proof": { "id": "PROOF_01HX...", "status": "draft", "timeout_at": null },
122
+ "warning": "This is a draft. The client cannot see it until you POST .../submit." }
123
+ ```
124
+
125
+ **A draft claims its task, exactly like a submission.** That is the point — two workers cannot
126
+ both draft the same task and then both try to send it. It also means the task counts against your
127
+ `max_open_tasks` cap and your rate-limit window while the draft sits there.
128
+
129
+ **Nothing happens to a draft on its own.** No review clock, no auto-approval, no `proof_webhook`,
130
+ no payout, and it is not counted in `proofs_submitted`. The client cannot see it at all: it is
131
+ filtered out of their dashboard, their proof list, and `GET /gigs/:id/proofs/:proof_id` returns
132
+ `404` to them.
133
+
134
+ ### Editing and sending
135
+
136
+ ```json
137
+ PATCH /gigs/:id/proofs/:proof_id/draft
138
+ { "proofs": ["https://..."], "private_note": "Licence key: ABC-123", "asking_price": 50 }
139
+
140
+ → { "success": true, "status": "draft", "updated": ["proofs", "private_note", "asking_price"] }
141
+ ```
142
+
143
+ Send only the fields you are changing. `private_note` and `asking_price` accept `null` to clear.
144
+ `task_identifier` cannot be changed — delete the draft and start again against the other task.
145
+
146
+ ```json
147
+ POST /gigs/:id/proofs/:proof_id/submit
148
+
149
+ → { "success": true, "status": "pending", "timeout_at": "2026-08-22T...",
150
+ "locked_price": 2.5, "resubmitted": false }
151
+ ```
152
+
153
+ **The price is snapshotted at submit, not when the draft was saved.** A draft can sit for days
154
+ while the client reprices the task, so the number you agree to is the one showing when you send.
155
+ The client may also reprice a task freely while it carries only a draft.
156
+
157
+ ### Withdrawing
158
+
159
+ ```json
160
+ POST /gigs/:id/proofs/:proof_id/withdraw
161
+
162
+ → { "success": true, "status": "draft", "withdrawn_from": "pending" }
163
+ ```
164
+
165
+ **The web app calls this button "Undo".** It is the same endpoint — the label avoids reading as a
166
+ withdrawal of money.
167
+
168
+ **Only a `pending` proof can be withdrawn.** Once the client has approved or rejected it, the
169
+ verdict is theirs — withdrawing a rejection would erase it from your record. Anything already
170
+ reviewed returns `409`.
171
+
172
+ **Resending restarts the review clock from zero.** The client gets a full `review_timeout` window
173
+ on work they are seeing for the first time. The webhook fires again with `"resubmitted": true`,
174
+ but `proofs_submitted` is credited only once, on the first send, and only the first send writes
175
+ a `proof_submitted` event.
176
+
177
+ ### Deleting
178
+
179
+ ```json
180
+ DELETE /gigs/:id/proofs/:proof_id // drafts only; a submitted proof is a permanent record
181
+
182
+ → { "success": true, "id": "PROOF_01HX...", "deleted": true }
183
+ ```
184
+
185
+ Deleting frees the `task_identifier` so a fresh proof can be started against it. Your claim on
186
+ the task is left alone — abandon it and `task_timeout` recycles it as normal.
187
+
188
+ **A draft is discarded for you when the task leaves you.** Declining it, reporting it, an owner
189
+ recycling or reassigning it, and the expiry sweep all delete your draft, because otherwise it
190
+ would block the next worker's proof against that task.
191
+
192
+ ## The proof lifecycle
193
+
194
+ ```
195
+ draft (no clock, invisible to the client — POST with draft:true, or withdraw a pending proof)
196
+ ├─ deleted (worker action, or the task leaves them)
197
+ └─ submit ─┐
198
+
199
+ submitted (locked_price snapshot, timeout_at set)
200
+ ├─ draft (worker withdraws; pending only, and only before a payout)
201
+ ├─ approved (client action) → rolled up → paid on-chain → paid_out_at set
202
+ ├─ timeout_approved (cron, after review_timeout) → same rollup path
203
+ ├─ rejected (needs a rejection_tag; returns the task to be done again by default)
204
+ │ approved ⇄ rejected: the client may change the verdict until a rollup carries the proof,
205
+ │ and never once the rejection returned the task
206
+ └─ reported (owner flags a timeout-approved proof; excluded from payouts)
207
+ ```
208
+
209
+ A proof from a `tbd` task carries `locked_price: null` until the amount is fixed. That happens at
210
+ exactly one of three moments, whichever comes first: `PATCH .../tasks/:msgId/price`,
211
+ `PATCH .../proofs/:proof_id` with an `amount`, or the review timeout — which settles at the gig
212
+ price. After that the number never moves again.
213
+
214
+ ## Review a proof
215
+
216
+ ```json
217
+ PATCH /gigs/:id/proofs/:proof_id
218
+
219
+ { "action": "approve", "feedback": "Great work!" }
220
+ { "action": "approve", "amount": 7.50 } // a TBD task
221
+ { "action": "approve", "amount": 50 } // accept a worker's ask
222
+ { "action": "reject", "rejection_tag": "incomplete", "feedback": "Screenshot doesn't match" }
223
+
224
+ → { "success": true, "status": "approved", "locked_price": 7.5, "price_source": "review" }
225
+ ```
226
+
227
+ **Review promptly.** Silence is approval: a proof auto-approves after the gig's `review_timeout`
228
+ (default 48 hours). That rule exists to protect workers from a client who disappears.
229
+
230
+ **On an order machine the reviewer is the participant who paid, not the gig owner.** The owner is
231
+ the vendor who did the work, and this route refuses them — the authority is resolved from the
232
+ task's buyer. `review_timeout` is also the vendor's only structural protection there, which is why
233
+ that mode forbids `-1` (manual review, no auto-approval) and anything under an hour.
234
+
235
+ **`feedback` is stored on either verdict and the gigworker reads it.** On a rejection it explains
236
+ the problem. On an approval it is your reply to the submission — which is what makes an
237
+ `inbound_proof` gig work as an application inbox: the proof *is* the application, and the approval
238
+ carries the answer.
239
+
240
+ ```json
241
+ { "action": "approve",
242
+ "feedback": "You are good. Assigned to team abc — join the groupchat: https://..." }
243
+ ```
244
+
245
+ Maximum 4000 characters. Omit it, or send `""` or `null`, to leave no note.
246
+
247
+ `amount` is accepted on a proof whose `locked_price` is still `null`, and on a proof that carries
248
+ an `asking_price`. Sending it for any other already-priced proof returns `409` — the price a
249
+ worker agreed to at submission cannot be lowered at review. A worker's own ask reopens the
250
+ number because they, not the client, named it.
251
+
252
+ **Automate it** by setting `proof_webhook_url` on the gig: every submission is POSTed there, so
253
+ your own validator or AI agent can check the work before you look at it.
254
+
255
+ ## A rejection returns the task by default
256
+
257
+ Rejecting a proof sends its task back out to be done again, unless you say otherwise. The work
258
+ you asked for is still work you want; before this, a rejected task was closed for good and
259
+ nobody — not another worker, not you — could ever pick it up.
260
+
261
+ ```json
262
+ PATCH /gigs/:id/proofs/:proof_id
263
+
264
+ { "action": "reject", "rejection_tag": "incomplete" } // task goes back out
265
+ { "action": "reject", "rejection_tag": "not_selected", "requeue": false } // task closes too
266
+
267
+ → { "success": true, "status": "rejected", "requeued": true, "task_identifier": "TASK_..." }
268
+ ```
269
+
270
+ **`requeue` defaults to `true`.** Existing automation that rejects proofs now returns tasks
271
+ without asking. Send `"requeue": false` wherever a rejection is meant to end the task — hiring
272
+ gigs that reject every applicant but one are the usual case.
273
+
274
+ Where the task actually lands depends on the gig's distribution:
275
+
276
+ | Distribution | Where the task goes |
277
+ |---|---|
278
+ | `queue` | back into the queue at its own position, this worker excluded |
279
+ | `queue_solo` | this worker's copy is dropped, the claim slot returns to the template |
280
+ | assigned task | back to you as `UNASSIGNED`, brief intact — never into a shared queue |
281
+ | `push` | offered to another mailbox that accepts it |
282
+
283
+ **A returned rejection is final.** Another worker may already hold that task, so approving the
284
+ old proof afterwards would pay twice for one task. A second `PATCH` on it returns `409`:
285
+
286
+ ```json
287
+ { "error": "This rejection returned the task to be done again, so its verdict can no longer change",
288
+ "status": "rejected", "task_released_at": "2026-08-21T10:00:00.000Z" }
289
+ ```
290
+
291
+ Read the proof to check before you ask — the owner's copy carries `task_released_at`, and
292
+ `can_revise` is `false` with `revision_blocked_by_requeue: true`.
293
+
294
+ **When there is nothing to return**, the rejection still stands and `requeued` comes back
295
+ `false` with a reason. That happens on `push` and `inbound_proof` gigs where the
296
+ `task_identifier` is free-form — a URL, a ticket number — and names no task row:
297
+
298
+ ```json
299
+ → { "success": true, "status": "rejected", "requeued": false,
300
+ "requeue_error": "This gig's task identifier does not name a task that can be returned" }
301
+ ```
302
+
303
+ Nothing is lost in that case: the proof keeps its undo, exactly as a `requeue: false` rejection
304
+ does.
305
+
306
+ **One live proof per task.** A task may collect several rejected attempts over its life, but
307
+ only one proof owns it at a time. Every other rule follows from that — a released attempt no
308
+ longer blocks a new submission, no longer stops you recycling or repricing or editing the task,
309
+ and no longer counts against the worker's open-task cap.
310
+
311
+ ## Change a verdict (undo an accidental approve or reject)
312
+
313
+ Send the same `PATCH` again with the other verdict. There is no separate undo endpoint.
314
+
315
+ ```json
316
+ PATCH /gigs/:id/proofs/:proof_id
317
+
318
+ { "action": "approve", "feedback": "Rejected this by mistake — approved." } // was rejected
319
+ { "action": "reject", "rejection_tag": "incomplete" } // was approved
320
+
321
+ → { "success": true, "status": "approved", "revised": true, "previous_status": "rejected" }
322
+ ```
323
+
324
+ **The window closes when a payout picks the proof up.** The daily cron rolls approved proofs into
325
+ a payout, and sweeps rejected ones into a $0 rollup so they stop being reconsidered. Once either
326
+ happens the verdict is final and a second `PATCH` returns `409`:
327
+
328
+ ```json
329
+ { "error": "This proof was already settled in a payout and its verdict can no longer change",
330
+ "status": "approved", "rollup_id": "ROLLUP_...", "rollup_status": "paid" }
331
+ ```
332
+
333
+ So an undo is same-day work. To check before you ask, read the single proof — the owner's copy
334
+ carries `can_revise` on any `approved` or `rejected` proof:
335
+
336
+ ```json
337
+ GET /gigs/:id/proofs/:proof_id
338
+ → { "status": "rejected", "can_revise": true, "revision_blocked_by_rollup_id": null }
339
+ ```
340
+
341
+ A rollup is not the only thing that closes the window. A rejection that returned its task is
342
+ final from the moment it was made — see "A rejection returns the task by default" above.
343
+
344
+ What a revision changes:
345
+
346
+ - `feedback` is replaced by what you send now, and **cleared if you omit it**. The old note
347
+ belonged to the verdict you are replacing.
348
+ - `rejection_tag` is cleared when you change to `approve`, and required when you change to
349
+ `reject`.
350
+ - Every verdict writes its own event and none are overwritten, so a revision is visible as a
351
+ revision. A reader wanting one verdict per proof takes the latest by timestamp.
352
+ - `locked_price` follows the normal rule: `amount` is accepted only on a proof still priced
353
+ `null` or carrying an `asking_price`.
354
+
355
+ `timeout_approved` cannot be revised. The cron approved it, not you — dispute it with
356
+ `POST .../report` instead.
357
+
358
+ ## Rejection tags and what they cost
359
+
360
+ A `rejection_tag` is required when rejecting. It is recorded on the rejection's event and is
361
+ the reason anyone reading the ledger sees. It carries no score — there is no score — so the
362
+ column below is about what each tag MEANS, not what it costs:
363
+
364
+ | Tag | Weight | Use it for |
365
+ |---|---|---|
366
+ | `fake_proof` | 5× | Fabricated evidence |
367
+ | `duplicate` | 3× | Work already submitted |
368
+ | `incomplete` | 2× | Half done |
369
+ | `unresponsive` | 2× | Claimed and abandoned |
370
+ | `low_quality` | 1× | Done, but badly |
371
+ | `other` | 1× | Anything else |
372
+ | `not_selected` | **0 — excluded from scoring entirely** | You hired somebody else |
373
+
374
+ **`not_selected` is the one that costs nothing.** It is not counted, not weighted, and not added
375
+ to the denominator. Use it to close out applicants you did not pick, so a worker can apply for ten
376
+ jobs, lose nine, and carry no penalty.
377
+
378
+ Do not reach for `not_selected` to soften a genuine quality problem. The ledger is the only
379
+ signal this platform publishes, and mislabelling bad work removes the one thing everyone else
380
+ depends on.
381
+
382
+ ## Report an auto-approved proof
383
+
384
+ ```json
385
+ POST /gigs/:id/proofs/:proof_id/report → { "success": true, "status": "reported" }
386
+ ```
387
+
388
+ Works only on `timeout_approved` proofs — the ones that were approved because you missed the
389
+ window. Reported proofs are excluded from rollups and will not be paid.
390
+
391
+ **On an order machine `report` works in both directions**, and the row records which
392
+ (`reported_by: "client" | "gigworker"`). It has to: the clock approving a delivery nobody reviewed
393
+ is precisely the buyer's problem, and reporting is their only remedy when the timeout ruled
394
+ against them. An owner-only report there would point the remedy at the wrong party.
395
+
396
+ ## Private delivery (`private_note`)
397
+
398
+ `proofs` is visible to the client the moment you submit. `private_note` is not. It is the
399
+ escrowed half of the delivery — the licence key, the password, the download link — and the
400
+ client cannot read it until they have actually paid for that proof.
401
+
402
+ ```json
403
+ POST /gigs/:id/proofs { ..., "private_note": "Licence key: ABC-123-XYZ" }
404
+ POST /public/submit-proof { ..., "private_note": "Licence key: ABC-123-XYZ" }
405
+ ```
406
+
407
+ Optional, at most 8000 characters, on both submit routes. A blank string means "none".
408
+
409
+ Every proof response carries two fields for it, and the three states are readable from
410
+ `private_note` **alone** — you never have to cross-check the boolean to know where you stand:
411
+
412
+ | `private_note` | `private_note_locked` | Meaning |
413
+ |---|---|---|
414
+ | `""` | `false` | The worker attached no note. Nothing is coming, now or ever. |
415
+ | `null` | `true` | A note exists and is being withheld. Pay for the proof and read it again. |
416
+ | `"..."` | `false` | Released. |
417
+
418
+ The empty string is the point: `""` means *nothing was attached* and `null` means *you have not
419
+ paid yet*. Reporting `null` for both would leave a client who has already paid unable to tell a
420
+ worker who sent nothing from a note they still have to unlock. Both values are falsy, so
421
+ `if (!private_note)` still means "there is nothing to read right now".
422
+
423
+ **The release condition is payment, not approval.** The note opens only when the proof is
424
+ `approved` or `timeout_approved` **and** a rollup has stamped `paid_out_at` on it — meaning the
425
+ money moved on chain. Approving a proof does not open it. A `rejected` or `reported` proof never
426
+ opens, even though clearing those writes a $0 rollup straight to `paid`.
427
+
428
+ The submitting gigworker always reads their own note back, at any status. The client polls
429
+ `GET /gigs/:id/proofs/:proof_id` after their rollup settles.
430
+
431
+ Put a big file in S3 and link it from the note. The link is presigned when the note is released
432
+ and expires in an hour, so fetch the proof again for a fresh one. The S3 key itself is random,
433
+ which is what keeps the file private while the note is still locked.
434
+
435
+ A gigworker who submits through a share link (`POST /public/submit-proof`) has no account, so
436
+ they cannot read the note back afterwards. It is write-once from that route.
437
+
438
+ **On an order machine this field is not optional practice — it is the vendor's only protection.**
439
+ The buyer reads `proofs[]` at review time and may undo the whole order, deposit and all, right up
440
+ until they approve. So put the deliverable in `private_note` and let `proofs[]` carry evidence
441
+ only: a watermarked preview, a hash, a word count, a description. Enough to rule on, not enough to
442
+ use. `POST .../proofs/:proof_id/withdraw` is refused in that mode for the same reason — the buyer
443
+ has already paid and may already have read it, so un-delivering would leave them holding a funded
444
+ order with nothing against it. See [orders.md](https://dollarplatoon.com/skill/orders.md).
445
+
446
+ ## Private aliases
447
+
448
+ A task or a proof can carry an **alias**: a short title from your point of view only. It is never
449
+ shared. The client and the gigworker each keep their own alias for the same row and neither can
450
+ read the other's.
451
+
452
+ ```json
453
+ PATCH /gigs/:id/tasks/:msgId/alias { "alias": "Acme profile review" }
454
+ PATCH /gigs/:id/proofs/:proof_id/alias { "alias": "Applicant — Jane, senior editor" }
455
+ → { "success": true, "id": "PROOF_01J...", "alias": "Applicant — Jane, senior editor" }
456
+ ```
457
+
458
+ - `""` or `null` clears it. Maximum 120 characters.
459
+ - Every read of the row returns your own alias as `alias` (`null` if you set none). The stored map
460
+ of other users' aliases never leaves the API.
461
+ - The web app shows the alias in place of the row id, and quick-search matches it alongside the
462
+ subject and the `task_identifier`.
463
+ - Who may set one: the gig owner on any task or proof in their gig; the gigworker on a task their
464
+ mailbox holds (or one still queued in a gig they belong to) and on their own proofs.
465
+
466
+ This pairs with approval notes on an `inbound_proof` gig: alias each inbound proof with what it
467
+ actually is ("Applicant — Jane"), then answer it with `feedback` when you approve.
468
+
469
+ **Alias versus tags:** an alias is private to you; tags are shared by both parties. Use an alias to
470
+ recognise a row, tags to filter a set of them.
471
+
472
+ ## Share links — submitting without an account
473
+
474
+ Every mailbox has a `share_token` that lets someone submit a proof for it without logging in —
475
+ useful for delegating to a teammate, or embedding in your own tool.
476
+
477
+ ```
478
+ https://dollarplatoon.com/submit/SHARE_TOKEN
479
+ ```
480
+
481
+ | Method | Path | Auth | Description |
482
+ |--------|------|------|-------------|
483
+ | GET | `/public/mailbox-info?token=` | No | Gig title, terms, price, mailbox name |
484
+ | GET | `/public/task?token=&task=` | No | One task, if the link opted in with `?task=` |
485
+ | POST | `/public/submit-proof` | No | Submit a proof |
486
+ | POST | `/public/task/decline` | No | Skip a task |
487
+ | POST | `/public/task/violation` | No | Report a task |
488
+ | POST | `/public/upload-presign` | No | Presigned upload URL (any type, 100MB max) |
489
+
490
+ ```json
491
+ POST /public/submit-proof
492
+ { "share_token": "SHARE_...", "task_identifier": "TASK_...", "proofs": ["https://..."] }
493
+ → { "proof_id": "PROOF_...", "status": "pending" }
494
+ ```
495
+
496
+ **Treat the link as a password.** It is per mailbox, not per task, it covers every task in that
497
+ mailbox, and it never expires. Anyone holding it can submit as that worker. Rotate a compromised
498
+ one with `POST /gigs/:id/mailboxes/:mbxId/regenerate-token`.
499
+
500
+ Get the token from `GET /mailboxes/mine` with the worker's own API key — the gig owner's mailbox
501
+ list strips it.
502
+
503
+ **Showing a task on the page is opt-in.** By default `/submit/:token` reveals nothing about the
504
+ mailbox contents. Append `?task=` with a task id to render that one task with its identifier
505
+ pre-filled and locked:
506
+
507
+ ```
508
+ https://dollarplatoon.com/submit/SHARE_TOKEN?task=TASK_01KXQ...
509
+ ```
510
+
511
+ The task must belong to the token's mailbox or the route answers `404`. There is deliberately **no
512
+ route that lists a mailbox's tasks by share token**, so a leaked link alone cannot dump the
513
+ history — the holder also needs each task id. A bad task id does not break the page; it shows a
514
+ notice and the plain form still works.
515
+
516
+ **Skip and report work from the share page too**, with the same rules as the authenticated routes
517
+ including solo-queue claim slots:
518
+
519
+ ```bash
520
+ curl -X POST https://dollarplatoon.com/api/public/task/decline \
521
+ -H "Content-Type: application/json" \
522
+ -d '{"share_token":"SHARE_...","task":"TASK_01KXQ..."}'
523
+
524
+ curl -X POST https://dollarplatoon.com/api/public/task/violation \
525
+ -H "Content-Type: application/json" \
526
+ -d '{"share_token":"SHARE_...","task":"TASK_01KXQ...","violation":"The brief links to a dead page"}'
527
+ ```
528
+
529
+ Both need a queue gig (`400` otherwise) and a task the mailbox holds (`404` otherwise). A task
530
+ that already has a proof answers `409`; an expired one answers `410`.
531
+
532
+ For embedding the page in an iframe, see
533
+ [web-pages.md](https://dollarplatoon.com/skill/web-pages.md) — clipboard and popup permissions
534
+ need explicit attributes.
535
+
536
+ ## Uploading files
537
+
538
+ ```json
539
+ POST /upload/presign
540
+ { "filename": "delivery.zip", "content_type": "application/zip", "prefix": "proofs", "content_length": 5242880 }
541
+ → { "presigned_url": "https://s3...", "url": "https://s3...", "key": "proofs/...", "bucket": "...", "max_bytes": 104857600 }
542
+ ```
543
+
544
+ PUT the file to `presigned_url`, then put the returned `url` in your proof's `proofs` array.
545
+ Prefixes: `"proofs"` (default), `"avatars"`, `"gig-icons"`. The presigned URL expires in one hour.
546
+
547
+ Any file type is accepted — image, document, video, archive, design file. The limit is **100MB
548
+ per file**. Use `application/octet-stream` when you do not know the content type.
549
+
550
+ Send `content_length` with the exact byte count of the file. The API refuses a larger value with
551
+ `413`, and it signs the count into the URL, so the upload fails if the byte count does not match.
552
+ `content_length` is optional for older clients, but without it the size is not enforced.
553
+
554
+ The key holds a random UUID, so nobody can guess it. The bucket blocks public access; a proof URL
555
+ is signed for one hour when the API returns it. Files expire after 365 days.