@officexapp/vidfarm-devcli 0.21.62 → 0.21.64
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 +107 -3
- package/SKILL.director.md +109 -5
- 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 +1418 -0
- package/dist/src/devcli/marketplace-gigs.js +162 -16
- package/marketplace.md +299 -1
- 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": "..." }`
|