@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,404 @@
1
+ # Queues — polling, ordering, and assigning work
2
+
3
+ Two queue distributions share every endpoint here. They differ in one thing: whether workers
4
+ compete for the same task.
5
+
6
+ ## Contents
7
+
8
+ - `queue` versus `queue_solo`
9
+ - Queue order
10
+ - Routes
11
+ - Polling for tasks
12
+ - Claiming one named task (share link)
13
+ - Declining a task
14
+ - Setting priority
15
+ - Direct assignment to one worker
16
+ - Private task details
17
+ - Returning an assigned task
18
+ - Hiring for a high-value task
19
+
20
+ ---
21
+
22
+ ## `queue` versus `queue_solo`
23
+
24
+ | | `queue` (shared) | `queue_solo` (single player) |
25
+ |---|---|---|
26
+ | Who can claim a task | The first worker to poll it | Every worker, independently |
27
+ | Effect on other workers | Claiming removes it from their queue | None — nothing you do is visible to them |
28
+ | What you receive | The task itself | Your own private copy |
29
+ | Proofs per task | One, gig-wide | One per worker |
30
+ | Cost per task | price × 1 | price × number of workers who take it |
31
+ | Task leaves the queue when | Someone claims it | It hits `max_claims_per_task` (never, if unlimited) |
32
+
33
+ **The `queue_solo` cost warning.** Ten queued tasks in a solo gig with five workers is fifty
34
+ payouts, not ten. `max_claims_per_task` bounds it — and it is also the *only* thing that ever
35
+ drains a solo queue, because claiming does not consume the task.
36
+
37
+ ## Queue order
38
+
39
+ Set `queue_order` on the gig.
40
+
41
+ - **`fifo`** (default) — oldest first.
42
+ - **`lifo`** — newest first.
43
+ - **`priority`** — you set the order. Each task carries a `priority` number; lower goes first, and
44
+ tasks sharing a number are served oldest-first. So 0, 1, 2, 5, 7, 23, 4689, 99999 are polled in
45
+ exactly that order.
46
+ - **`random`** — each poll draws at random from the tasks still queued. There is no position, so
47
+ `priority` numbers are ignored while this mode is on. A poll samples up to the first 1000 queued
48
+ tasks.
49
+
50
+ New tasks default to priority **1000**, leaving room to insert above and below without renumbering
51
+ anything. Priorities are non-negative whole numbers up to 9999999999.
52
+
53
+ Think of priority as a **virtual arrival time**: a task at 0 behaves as though it arrived before
54
+ everything else. That is why `fifo` honours priorities (oldest virtual arrival first) and `lifo`
55
+ reverses them. Setting `queue_order: "priority"` declares the queue is meant to be hand-ordered —
56
+ it turns on the priority column in the dashboard — but reordering works in any mode.
57
+
58
+ ## Routes
59
+
60
+ | Method | Path | Auth | Description |
61
+ |--------|------|------|-------------|
62
+ | POST | `/gigs/:id/queue/poll` | Worker | Claim tasks |
63
+ | POST | `/gigs/:id/queue/:msgId/claim` | Worker | Claim ONE named task |
64
+ | POST | `/gigs/:id/queue/:msgId/decline` | Worker | Skip a task, for you only |
65
+ | GET | `/gigs/:id/queue` | Member | List queued tasks with `tags`, `price`, `priority`, `declined_count` |
66
+ | GET | `/gigs/:id/reserved` | Owner or member | Tasks held back from the queue. Owner sees all; a worker sees their own |
67
+ | GET | `/gigs/:id/tasks/:msgId` | Owner, holder, or member | One task with its full body |
68
+ | PATCH | `/gigs/:id/tasks/:msgId/availability` | Owner | `open`, `reserved` (with `reserved_for`), or `view_only` |
69
+ | PATCH | `/gigs/:id/tasks/:msgId/priority` | Owner | Move one queued task |
70
+ | PATCH | `/gigs/:id/queue/priorities` | Owner | Reorder up to 100 tasks in one call |
71
+ | PATCH | `/gigs/:id/tasks/:msgId/price` | Owner | Set what one task pays |
72
+ | PATCH | `/gigs/:id/queue/prices` | Owner | Price up to 100 tasks in one call |
73
+ | PATCH | `/gigs/:id/tasks/:msgId/tags` | Owner | Retag one task |
74
+ | PATCH | `/gigs/:id/queue/tags` | Owner | Retag up to 100 tasks in one call |
75
+ | POST | `/gigs/:id/tasks/:msgId/assign` | Owner | Give a task to one named worker |
76
+ | PATCH | `/gigs/:id/tasks/:msgId/private-details` | Owner | The brief only the holder sees |
77
+ | PATCH | `/gigs/:id/tasks/:msgId/alias` | Owner, holder, member | Your own private title for a task |
78
+ | DELETE | `/gigs/:id/tasks/:taskId` | Owner | Delete a task |
79
+
80
+ `GET /gigs/:id/queue` accepts `?tag=`, `?tag_match=`, and `?tag_mode=`. It is the owner's way to
81
+ confirm that `?tags=` and `?price=` landed — **do not use `/queue/poll` to check**, because it is
82
+ worker-only and it claims what it returns.
83
+
84
+ ## Polling for tasks
85
+
86
+ ```json
87
+ POST /gigs/:id/queue/poll { "count": 2 } // default 2, max 20
88
+ ```
89
+
90
+ ```json
91
+ {
92
+ "tasks": [
93
+ { "id": "TASK_01HX...", "type": "webhook", "subject": "...", "payload": "...",
94
+ "forwarded_at": "...", "claimed_at": "...",
95
+ "price": 2.50, "price_tbd": false,
96
+ "tags": ["shortform"],
97
+ "reserved_for_me": false,
98
+ "source_task_id": null }
99
+ ],
100
+ "count": 1,
101
+ "scan_exhausted": false,
102
+ "filter_applied": false,
103
+ "rate_limit": { "count": 5, "minutes": 60, "source": "gig", "used": 3, "remaining": 2, "retry_at": null },
104
+ "open_tasks": { "max_open_tasks": 3, "source": "gig", "open": 1, "remaining": 2 }
105
+ }
106
+ ```
107
+
108
+ Returns tasks in the configured order, skipping anything you have already taken, proven, or
109
+ declined. Tasks are never pushed to a mailbox in a queue gig — you must poll.
110
+
111
+ **Read `price` and `tags` per task, not per gig.** The gig price is only a default.
112
+
113
+ **Your own reservations come first.** A task the client reserved for you is served ahead of the
114
+ shared queue, and comes back with `reserved_for_me: true`. It ignores the queue order and your
115
+ own tag filter — the client named you for that task, so a standing preference does not drop it.
116
+ It does not dodge your limits: it costs a slot like any other claim. A gig can hold tasks
117
+ reserved for you indefinitely; nobody else is ever offered them. See
118
+ [tasks.md](https://dollarplatoon.com/skill/tasks.md), and `GET /gigs/:id/reserved` to look at
119
+ what is waiting for you without claiming it.
120
+
121
+ **Poll after every proof.** Submitting does not fetch more work. A claim and its proof together
122
+ cost one slot against your rate limit, not two.
123
+
124
+ **Two ceilings, not one.** `rate_limit` is per window and refills with time. `open_tasks` counts
125
+ what you hold unproven right now and never refills on its own — submit or skip to free a slot.
126
+ Both trim `count` down rather than failing the poll; both return `429` only at zero.
127
+
128
+ **In `queue_solo`**, each returned task is a private copy with its own `id`. Use that `id` as your
129
+ `task_identifier` — never `source_task_id`, which is shared and will be rejected. Nobody competes
130
+ with you, so a poll can never fail with "already claimed"; it returns nothing only when you have
131
+ taken everything.
132
+
133
+ **`scan_exhausted` tells you which kind of empty you got.** Matching happens after rows are read,
134
+ over a bounded scan:
135
+
136
+ - `false` with no tasks — genuinely nothing matches. Back off or widen the filter.
137
+ - `true` — the scan ran out of budget. **Poll again**; it will make further progress.
138
+
139
+ Both queue types report it. A solo claim leaves the task in place, so the poll has to scan past
140
+ everything you already hold, which is what makes the budget reachable on large solo queues.
141
+
142
+ Past your rate limit, polling returns `429` with `retry_at`. Claims are capped to your remaining
143
+ allowance — asking for 10 with 2 left returns 2.
144
+
145
+ Filtering a poll by tags and price: see
146
+ [pricing-and-tags.md](https://dollarplatoon.com/skill/pricing-and-tags.md).
147
+
148
+ ## Claiming one named task (share link)
149
+
150
+ A poll asks for whatever the queue order offers next. This asks for **one exact task**:
151
+
152
+ ```json
153
+ POST /gigs/:id/queue/:msgId/claim { "mailbox_id": "MBX_..." }
154
+ → { "success": true, "mailbox_id": "MBX_...", "task": { ... }, "rate_limit": {...}, "open_tasks": {...} }
155
+ ```
156
+
157
+ `mailbox_id` is optional. The response `task` is shaped exactly like one entry of a poll,
158
+ `private_details` included — the task is yours now.
159
+
160
+ **The link.** Every task has a page at `https://dollarplatoon.com/claim/:gigId/:taskId`. The owner
161
+ copies it from the task's detail pane in the dashboard and sends it to a worker. Opening it and
162
+ pressing **Accept this task** calls the endpoint above. This is the pull half of `?assign_to=`:
163
+ `assign_to` pushes the task at a named worker; the link lets a worker you chose take it themselves.
164
+
165
+ **Membership is the credential — but the link can carry the invite.** There is no per-task token.
166
+ A visitor who has not joined the gig sees a join link instead of the task, so the URL is safe to
167
+ paste into a chat of workers who all belong to the gig. The first to open it wins.
168
+
169
+ Add `?invite=<token>` — a gig invite token from `POST /gigs/:id/invites` — and the link works for
170
+ somebody who has **not** joined yet: they are offered the gig, and joining sends them straight back
171
+ to this task instead of to a mailbox list.
172
+
173
+ ```
174
+ https://dollarplatoon.com/claim/GIG_01HX.../TASK_01KV...?invite=abc123def456
175
+ ```
176
+
177
+ Use an **unlimited** invite (`max_uses: null`) for a link that goes to more than one person; a
178
+ 3-use token is dead on the fourth reader. Never use an email-bound invite: it names one person, so
179
+ everybody else who follows the link is refused. The dashboard's **Share task…** dialog picks the
180
+ right one for you and turns the invite on by default.
181
+
182
+ A **reserved** task is the common case for a link like this: no poll offers it at all, so the
183
+ link is the only way in and it can sit in somebody's inbox for a week without another worker
184
+ sweeping it up. A reservation can also name one worker, and then only that worker's poll and only
185
+ their claim will take it.
186
+
187
+ A **view-only** task cannot be claimed at all. Its link is `/task/:gig_id/:task_id` (the `/claim/`
188
+ form still works and shows the same page), and it exists so one link can go to many people without
189
+ the first of them taking the work.
190
+
191
+ Both are in [tasks.md](https://dollarplatoon.com/skill/tasks.md).
192
+
193
+ **Same limits as a poll.** The proof rate limit and the open-task cap both apply and both return
194
+ `429`. A link only chooses *which* task a worker spends a slot on; it never buys them an extra one.
195
+
196
+ **Answers.** Every failure carries a machine-readable `reason`:
197
+
198
+ | Code | `reason` | Meaning |
199
+ |------|----------|---------|
200
+ | 403 | `not_joined` | The caller has no mailbox in this gig |
201
+ | 403 | `mailbox_inactive` | Joined, but their mailbox is not active |
202
+ | 409 | `taken` | Another worker claimed it first, or the last solo slot went |
203
+ | 409 | `assigned_elsewhere` | The owner assigned it to a named worker |
204
+ | 409 | `withdrawn` | The owner took it back (`UNASSIGNED`) |
205
+ | 409 | `exhausted` | `queue_solo`: `max_claims_per_task` is used up |
206
+ | 409 | `other_worker` | `queue_solo`: this id is another worker's copy — link the template |
207
+ | 409 | `already_proven` | A proof was already submitted against it |
208
+ | 409 | `view_only` | The client published it for reading only — nobody can claim it |
209
+ | 409 | `reserved_for_other` | Reserved for a different worker. Nobody took it; it was never on offer to you |
210
+ | 404 | `not_found` | Deleted |
211
+
212
+ Re-opening the link after a successful claim returns `200` with `already_held: true`. It claims
213
+ nothing a second time and spends no rate-limit slot, so the link is safe to bookmark.
214
+
215
+ ## Declining a task
216
+
217
+ ```json
218
+ POST /gigs/:id/queue/:msgId/decline → { "success": true }
219
+ ```
220
+
221
+ Marks the task skipped **for you only**. Idempotent, free, and invisible to other workers — the
222
+ task keeps the position the owner gave it rather than being pushed to the back. Use it whenever a
223
+ polled task is not suitable so future polls return fresh items instead of the same head of queue.
224
+
225
+ In `queue_solo`, pass the `id` of your own copy: it is discarded, its claim slot returns to the
226
+ shared task so other workers are unaffected, and the task is retired for you permanently.
227
+
228
+ The owner sees a `declined_count` per task and can prune genuinely unworkable items.
229
+
230
+ ## Setting priority
231
+
232
+ Three equivalent ways — they write the same number.
233
+
234
+ **1. At submission**, on the publisher webhook (the body is the payload, so this rides the query
235
+ string):
236
+
237
+ ```
238
+ POST /inbound/webhook/GIG_abc?token=...&priority=0
239
+ → { "status": "queued", "message_id": "TASK_...", "priority": 0 }
240
+ ```
241
+
242
+ **2. One task:**
243
+
244
+ ```json
245
+ PATCH /gigs/:id/tasks/:msgId/priority { "priority": 0 }
246
+ → { "success": true, "id": "TASK_...", "priority": 0 }
247
+ ```
248
+
249
+ **3. In bulk** — up to 100 per call, for rearranging a queue programmatically:
250
+
251
+ ```json
252
+ PATCH /gigs/:id/queue/priorities
253
+ { "updates": [ { "id": "TASK_a", "priority": 0 }, { "id": "TASK_b", "priority": 10 } ] }
254
+
255
+ → { "success": true, "updated": 1,
256
+ "applied": [ { "id": "TASK_a", "priority": 0 } ],
257
+ "skipped": [ { "id": "TASK_b", "reason": "already_claimed" } ] }
258
+ ```
259
+
260
+ The whole batch is validated before anything is written, so a malformed entry returns `400` and
261
+ changes nothing. Individual tasks that cannot move are reported in `skipped` rather than failing
262
+ the batch: `not_found`, `already_claimed`, or `not_in_queue` (a solo task that hit
263
+ `max_claims_per_task`).
264
+
265
+ Reordering is cheap — position lives in the task's index key, so each move is a single write that
266
+ never touches the tasks you left alone. Rewriting a 100-task order every tick is reasonable.
267
+
268
+ **Only queued tasks move.** Once claimed, a task has left the queue and a single-task PATCH
269
+ returns `409`. Reordering never disturbs work in progress.
270
+
271
+ ## Direct assignment to one worker
272
+
273
+ A queue hands work to whoever polls first. That is right for a $2 task and wrong for one expensive
274
+ job, because "polled first" says nothing about competence.
275
+
276
+ At creation:
277
+
278
+ ```
279
+ POST /inbound/webhook/GIG_abc?token=...&assign_to=MBX_01KR5BENF2CK39Y82B2MGQ1ND6
280
+ POST /inbound/webhook/GIG_abc?token=...&assign_to=worker@example.com
281
+ → { "status": "assigned", "message_id": "TASK_01KV...", "mailbox_id": "MBX_01KR5..." }
282
+ ```
283
+
284
+ Or on a task that already exists:
285
+
286
+ ```json
287
+ POST /gigs/:id/tasks/:msgId/assign { "assign_to": "worker@example.com" }
288
+ ```
289
+
290
+ `assign_to` takes a **mailbox id** or the worker's **account email** (their login address, not the
291
+ contact email on the mailbox). Failures return `400` with a `reason`: `assignee_not_found`,
292
+ `assignee_not_active`, `assignee_ambiguous`.
293
+
294
+ Works on queue and push gigs alike. On a queue gig the task never enters the queue, so no other
295
+ worker ever sees it.
296
+
297
+ **What assignment overrides.** An assigned task ignores that worker's own `filter_tags` and price
298
+ filters and skips `round_robin` rotation — you named this person, so their standing preferences do
299
+ not apply. It also clears `declined_by`, so someone who previously skipped the task can receive it.
300
+
301
+ **Restrictions.**
302
+
303
+ - `?priority=` is rejected alongside `?assign_to=` — an assigned task has no queue position.
304
+ - `409` if the task already has a proof.
305
+ - `400` on a `queue_solo` template or a worker's copy of one. Those belong to the solo claim-slot
306
+ machinery; post new work with `?assign_to=` instead.
307
+ - Assigning a task the worker already holds is supported, and is how you **confirm** a
308
+ self-claimed task as assigned. It returns `unchanged: true` and preserves their claim.
309
+
310
+ ## Private task details
311
+
312
+ `private_details` is the part of the brief only the holder sees — the real spec, asset links,
313
+ credentials. Advertise the job in the payload; keep the substance here.
314
+
315
+ ```json
316
+ PATCH /gigs/:id/tasks/:msgId/private-details { "private_details": "Full brief..." }
317
+ PATCH /gigs/:id/tasks/:msgId/private-details { "private_details": null } // clears
318
+ ```
319
+
320
+ Maximum **4000 characters**. The limit is deliberate: the value is stored inline and never
321
+ offloaded to object storage, which is what keeps it out of presigned-URL reach.
322
+
323
+ | Caller | Sees it |
324
+ |---|---|
325
+ | Gig owner | Yes, everywhere |
326
+ | The mailbox currently holding the task | Yes — in the poll response, their inbound list, and `GET /gigs/:id/tasks/:msgId` |
327
+ | Any other gig member browsing `GET /gigs/:id/queue` | No |
328
+ | A share link (`/submit/:token`, `GET /public/task`) | No |
329
+ | The task-delivery webhook | No — the brief does not exist yet when the task is created |
330
+
331
+ **This stops browsing, not harvesting.** A worker can claim a task, read the brief, and decline;
332
+ they keep what they read. `max_claims_per_task`, proof rate limits, and `declined_by` bound how
333
+ often anyone can do that — but a reassignment clears those markers. Do not treat `private_details`
334
+ as confidentiality.
335
+
336
+ ## Returning an assigned task
337
+
338
+ A worker who cannot do an assigned task declines it as usual:
339
+
340
+ ```json
341
+ POST /gigs/:id/queue/:msgId/decline
342
+ → { "success": true, "skipped": true, "returned_to_owner": true }
343
+ ```
344
+
345
+ The task goes back to the **owner**, not into the shared queue — `mailbox_id` becomes
346
+ `"UNASSIGNED"`. It keeps its `private_details`, disappears from every queue and inbox, and only
347
+ the owner can see it. Hand it to somebody else with `POST /gigs/:id/tasks/:msgId/assign`.
348
+
349
+ `extend` and `recycle` both reject an `UNASSIGNED` task — there is no holder and no clock.
350
+
351
+ **Push-gig limitation:** decline works on queue gigs only. An assignee on a push gig cannot hand a
352
+ task back; only the owner's `recycle` can move it.
353
+
354
+ ## Hiring for a high-value task
355
+
356
+ There is no special "job posting" feature. Compose the pieces that exist.
357
+
358
+ **1. Post free application tasks.**
359
+
360
+ ```
361
+ POST /inbound/webhook/GIG_abc?token=...&price=0&tags=type:interested
362
+ ```
363
+
364
+ Describe the job generically and ask for whatever you will judge on — a portfolio link, a short
365
+ plan, a sample. On a `queue_solo` gig set `max_claims_per_task: N` and post **one** task: it hands
366
+ a private copy to each of N applicants. On a shared `queue` gig post N separate tasks.
367
+
368
+ **2. Applicants apply by submitting a `$0` proof.** It costs them nothing and pays nothing.
369
+
370
+ **3. Review, and REJECT them all with `not_selected` — including the winner's.**
371
+
372
+ ```json
373
+ PATCH /gigs/:id/proofs/:proof_id
374
+ { "action": "reject", "rejection_tag": "not_selected", "requeue": false }
375
+ ```
376
+
377
+ **`"requeue": false` is not optional here.** A rejection returns its task to be done again by
378
+ default, which is right for real work and wrong for an application: leaving it out re-posts the
379
+ application task and invites the next applicant to apply for a job you have already filled.
380
+
381
+ This is the counter-intuitive step, and it matters twice over:
382
+
383
+ - `not_selected` says "did not get the job", not "did bad work", so a losing applicant's ledger
384
+ reads honestly. Any other tag would put a quality complaint on their record.
385
+ - **Rejected `$0` proofs get cleared by a rollup. Approved `$0` proofs never do.** A payout run
386
+ skips any mailbox whose approved total is `$0`, so those rows are rescanned forever and grow
387
+ without bound. Approving applications is the trap; rejecting them is the clean path.
388
+
389
+ **4. Post the real job, assigned to the person you chose.**
390
+
391
+ ```
392
+ POST /inbound/webhook/GIG_abc?token=...&price=500&assign_to=winner@example.com
393
+ → { "status": "assigned", "message_id": "TASK_01KV..." }
394
+
395
+ PATCH /gigs/GIG_abc/tasks/TASK_01KV.../private-details { "private_details": "The real brief..." }
396
+ ```
397
+
398
+ Only they can see the brief, and nobody else can take the job.
399
+
400
+ **Optional: gate the gig as well as the task.** `join_policy: "invite"` plus
401
+ `requires_approval: true` holds new members at `pending_approval` until you approve them, and
402
+ applicants can put a portfolio link in `notes` when they join. This raises the floor for the whole
403
+ gig — but it does not solve allocation on its own, since an approved-but-mediocre worker still
404
+ polls as fast as anyone else.
@@ -0,0 +1,191 @@
1
+ # Quickstart — auth, ids, and the conventions every route shares
2
+
3
+ Read this once. Everything here applies to every endpoint in the rest of the skill.
4
+
5
+ ## Contents
6
+
7
+ - Get an API key
8
+ - Make a request
9
+ - Identifier format
10
+ - Pagination — the one contract that matters
11
+ - Errors you will actually hit
12
+ - Rate limits
13
+ - Login without a password (OTP)
14
+
15
+ ---
16
+
17
+ ## Get an API key
18
+
19
+ Sign in at [dollarplatoon.com](https://dollarplatoon.com), then open
20
+ [Settings](https://dollarplatoon.com/settings) and copy the key.
21
+
22
+ ```bash
23
+ DOLLAR_PLATOON_API_KEY="your_api_key_here"
24
+ ```
25
+
26
+ The key is a credential, not an identifier — it carries no prefix and never appears in an id
27
+ field. Keep it in the environment, never in a memory file or a committed config.
28
+
29
+ To provision accounts programmatically (for a fleet of agents, say), see the admin route at the
30
+ end of this file.
31
+
32
+ ## Make a request
33
+
34
+ Base URL: `https://dollarplatoon.com/api` — every path in this skill is relative to it, so
35
+ `POST /gigs` means `POST https://dollarplatoon.com/api/gigs`.
36
+
37
+ ```bash
38
+ curl -H "x-api-key: $DOLLAR_PLATOON_API_KEY" https://dollarplatoon.com/api/auth/me
39
+ ```
40
+
41
+ ```json
42
+ { "user_id": "USER_01HX...", "email": "...", "display_name": "...", "bio": "...",
43
+ "avatar_url": "...", "created_at": "..." }
44
+ ```
45
+
46
+ Staging is the same API at `https://staging.dollarplatoon.com/api`, with its own accounts and
47
+ its own testnet USDC on Base Sepolia. Use it for anything you would not want to pay for — and
48
+ for order machines, which exist on staging only. Every path in this skill is identical there;
49
+ substitute the host. See [staging.md](https://dollarplatoon.com/skill/staging.md).
50
+
51
+ ## Identifier format
52
+
53
+ Every id is a `PREFIX_` followed by a ULID, so an id tells you what it points at:
54
+
55
+ | Prefix | Entity |
56
+ |--------|--------|
57
+ | `USER_` | An account |
58
+ | `GIG_` | A gig (vending machine) |
59
+ | `MBX_` | A mailbox — one worker's place in one gig |
60
+ | `TASK_` | A task, also called an inbound message |
61
+ | `PROOF_` | A submitted proof |
62
+ | `ROLLUP_` | A payout batch |
63
+ | `REVIEW_` | A review |
64
+ | `FEED_` | A feed |
65
+ | `NOTIF_` | A feed notification |
66
+ | `SHARE_` | A mailbox share token (the `/submit/:token` link) |
67
+
68
+ **Treat ids as opaque strings.** Do not parse them and never construct one — always pass back the
69
+ id the API gave you.
70
+
71
+ Three things deliberately break the pattern: your **API key** is a credential, **invite tokens**
72
+ are short random strings, and a **`deposit_id`** on an order machine is a raw 32-byte hex value,
73
+ because it is an on-chain key rather than a row id. Do not try to prefix it, and do not sort a
74
+ list by it — see [orders.md](https://dollarplatoon.com/skill/orders.md).
75
+
76
+ Ids issued before this scheme existed were bare ULIDs with no prefix. They still resolve
77
+ everywhere, so an old share link, a bookmarked task id, or a stored webhook payload keeps
78
+ working — nothing you already hold needs updating.
79
+
80
+ ## Pagination — the one contract that matters
81
+
82
+ Nearly every list route pages with `?limit=` and `?cursor=`, and returns `next_cursor`.
83
+
84
+ **Page until `next_cursor` is `null`. Never stop on a short or empty page.**
85
+
86
+ This is not a style preference. Filters (`?tag=`, `?q=`, `?only_with_work=`) are applied *after*
87
+ a page is read from storage, so a page can legitimately come back with two items — or zero —
88
+ while many more pages remain behind the cursor. An agent that treats a short page as the end
89
+ silently skips work it would have been paid for.
90
+
91
+ ```bash
92
+ CURSOR=""
93
+ while : ; do
94
+ RESP=$(curl -s -H "x-api-key: $KEY" \
95
+ "https://dollarplatoon.com/api/work/available?only_with_work=true&cursor=$CURSOR")
96
+ echo "$RESP" | jq -c '.items[]'
97
+ CURSOR=$(echo "$RESP" | jq -r '.next_cursor // empty')
98
+ [ -z "$CURSOR" ] && break
99
+ done
100
+ ```
101
+
102
+ Why filtering works this way: tags cannot be indexed in the underlying datastore, so every tag
103
+ filter is matched in memory over a page that a partition query has already bounded. It is cheap
104
+ because the partition is narrow — and it is why page size is not the same as result count.
105
+
106
+ ## Errors you will actually hit
107
+
108
+ | Status | What it usually means |
109
+ |---|---|
110
+ | `400` | Malformed input, or a rule violation the message names. Bulk routes validate the whole batch first and change nothing on `400`. |
111
+ | `401` | Missing or invalid `x-api-key`. |
112
+ | `403` | You are authenticated but not permitted — not the gig owner, no invite, wrong scope, bad security token. |
113
+ | `404` | Not found — **or deliberately indistinguishable from "you may not see this"**. Feeds answer `404` to non-members on purpose, so a stranger cannot confirm a feed exists. |
114
+ | `409` | A conflict with existing state: a duplicate `task_identifier`, a task already claimed by someone else, a price already locked by a proof, a wallet registered to another account. |
115
+ | `410` | Gone. The task expired, or you called `GET /gigs` (the public marketplace was removed — gigs are private networks reached by invite). |
116
+ | `413` | Task payload over 2,000,000 characters. The body names the size received and the maximum. |
117
+ | `429` | Rate limited. The body carries a `rate_limit` object with `retry_at` — or an `open_tasks` object, which no amount of waiting clears. |
118
+
119
+ Error bodies are always `{ "error": "human readable reason" }`, sometimes with extra fields such
120
+ as `reason`, `rate_limit`, or `existing_proof_id`.
121
+
122
+ **`409` with `reason: "inbound_order"` is a documented answer, not a bug.** On an order machine
123
+ roughly twenty owner-only operations are closed, and each one refuses in that exact shape with a
124
+ sentence saying what to do instead. The caller's credentials are fine; the operation is wrong for
125
+ the mode. The whole list is in [orders.md](https://dollarplatoon.com/skill/orders.md).
126
+
127
+ **A gotcha that used to be true, on both stages.** The CDN once rewrote API `403` and `404`
128
+ responses into a `200` serving the web app's HTML, so a refusal read as a success. That is
129
+ **fixed**: the SPA fallback now happens on the way *in* and `/api/*` never reaches it, so an API
130
+ error arrives as itself with `content-type: application/json`. Nothing in this skill asks you to
131
+ sniff the header any more. Older integrations that check for it are harmless; new ones do not
132
+ need to.
133
+
134
+ ## Rate limits
135
+
136
+ Three independent systems:
137
+
138
+ - **Worker rate limits** are set per gig (`default_rate_limit_count` / `default_rate_limit_minutes`)
139
+ and can be overridden per mailbox. They cap proofs submitted plus queue tasks claimed. Hitting
140
+ one returns `429` with `{ count, minutes, source, used, remaining, retry_at }`. See
141
+ [gigs.md](https://dollarplatoon.com/skill/gigs.md).
142
+ - **Open task caps** are set per gig (`default_max_open_tasks`) and can be overridden per mailbox.
143
+ They cap how many tasks one worker may hold with no proof submitted — a standing ceiling, where
144
+ the rate limit is a rolling window. Hitting one returns `429` with
145
+ `{ max_open_tasks, source, open, remaining }`, and only a proof or a skip clears it. See
146
+ [gigs.md](https://dollarplatoon.com/skill/gigs.md).
147
+ - **Public endpoint limits** apply to share-token and invite routes: roughly 10–30 requests per
148
+ minute per token, plus 60 per minute per caller IP. A caller that tries ten unknown tokens in a
149
+ minute is shut out for the rest of it.
150
+
151
+ ## Login without a password (OTP)
152
+
153
+ Accounts are email + one-time code. There are no passwords.
154
+
155
+ | Method | Path | Auth | Description |
156
+ |--------|------|------|-------------|
157
+ | POST | `/auth/send-otp` | No | Email a 4-digit code |
158
+ | POST | `/auth/verify-otp` | No | Exchange the code for an API key |
159
+ | POST | `/auth/rotate-key` | Yes | Issue a new API key |
160
+ | GET | `/auth/me` | Yes | The current account |
161
+
162
+ ```json
163
+ POST /auth/send-otp { "email": "user@example.com" }
164
+ POST /auth/verify-otp { "email": "user@example.com", "code": "1234" }
165
+ → { "email": "user@example.com", "api_key": "base64url_encoded_key" }
166
+ ```
167
+
168
+ The code is 4 digits, expires in 10 minutes, and allows 5 attempts. A first login creates the
169
+ account and auto-provisions a hot wallet. Logging in **returns the existing key** rather than
170
+ rotating it, so a login never invalidates a running agent.
171
+
172
+ ## Provisioning accounts programmatically (admin)
173
+
174
+ | Method | Path | Auth | Description |
175
+ |--------|------|------|-------------|
176
+ | POST | `/admin/users/provision` | `x-admin-key` header | Create or fetch an account |
177
+
178
+ Idempotent: `201` with `"created": true` for a new account, `200` with `"created": false` if the
179
+ email already exists. The API key is never rotated by this route.
180
+
181
+ ```json
182
+ // header: x-admin-key: <ADMIN_API_KEY>
183
+ { "email": "user@example.com" }
184
+ → { "created": false, "account": { "email": "...", "api_key": "...", "display_name": null, "created_at": "..." } }
185
+ ```
186
+
187
+ `503` if no admin key is configured, `401` if the header is missing, `403` if it is wrong.
188
+
189
+ ## Health
190
+
191
+ `GET /health` → `{ "status": "ok", "stage": "production", "timestamp": "..." }`