@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.
- package/.agents/skills/dollarplatoon-skill/SKILL.md +163 -1174
- package/.agents/skills/dollarplatoon-skill/SOURCE.md +62 -0
- package/.agents/skills/dollarplatoon-skill/skill/clients.md +234 -0
- package/.agents/skills/dollarplatoon-skill/skill/feeds.md +326 -0
- package/.agents/skills/dollarplatoon-skill/skill/gigs.md +395 -0
- package/.agents/skills/dollarplatoon-skill/skill/gigworkers.md +324 -0
- package/.agents/skills/dollarplatoon-skill/skill/orders.md +573 -0
- package/.agents/skills/dollarplatoon-skill/skill/payouts.md +234 -0
- package/.agents/skills/dollarplatoon-skill/skill/platform.md +174 -0
- package/.agents/skills/dollarplatoon-skill/skill/prices.md +75 -0
- package/.agents/skills/dollarplatoon-skill/skill/pricing-and-tags.md +255 -0
- package/.agents/skills/dollarplatoon-skill/skill/proofs.md +555 -0
- package/.agents/skills/dollarplatoon-skill/skill/queue.md +404 -0
- package/.agents/skills/dollarplatoon-skill/skill/quickstart.md +191 -0
- package/.agents/skills/dollarplatoon-skill/skill/staging.md +178 -0
- package/.agents/skills/dollarplatoon-skill/skill/tasks.md +588 -0
- package/.agents/skills/dollarplatoon-skill/skill/web-pages.md +586 -0
- package/.agents/skills/vidfarm/SKILL.md +3 -3
- package/.agents/skills/vidfarm/references/core-workflows.md +39 -0
- package/SKILL.director.md +42 -3
- package/SKILL.md +3 -1
- package/clipper.md +20 -0
- package/dist/src/cli.js +50 -6
- package/dist/src/devcli/delivery-seal.js +119 -0
- package/dist/src/devcli/marketplace-console.js +1253 -0
- package/dist/src/devcli/marketplace-gigs.js +162 -16
- package/marketplace.md +275 -1
- package/package.json +24 -3
|
@@ -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.
|