@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,62 @@
1
+ # Provenance of this vendored skill
2
+
3
+ **Synced from `https://staging.dollarplatoon.com` on 2026-08-29.**
4
+
5
+ ## Read this before "correcting" the source host
6
+
7
+ Staging is not a convenience here — it is the only host serving the current
8
+ documentation. Verified the same day, page by page:
9
+
10
+ | Source | Generation | `orders.md` | `staging.md` |
11
+ |---|---|---|---|
12
+ | `staging.dollarplatoon.com` | **current** | 33,702 B | 8,921 B |
13
+ | `dollarplatoon.com` | one behind | **404** | **404** |
14
+ | `github.com/OfficeXApp/dollarplatoon-skill` | one behind | **absent** | **absent** |
15
+
16
+ Thirteen of the fifteen sub-pages differ between the two hosts; only `prices.md`
17
+ and `pricing-and-tags.md` match. `web-pages.md` alone differs by 346 lines.
18
+
19
+ The index claims the two hosts are "kept identical". **They are not**, and the
20
+ production copy is the stale one. If you re-sync from `dollarplatoon.com` or from
21
+ the GitHub mirror, you will silently delete `orders.md` — the entire
22
+ `inbound_order` contract that `src/services/dollarplatoon-orders.ts` and
23
+ `src/services/marketplace-dp-bridge.ts` are written against.
24
+
25
+ Re-check the table above before switching hosts. Once production catches up and
26
+ serves `orders.md`, prefer it.
27
+
28
+ ## The one local edit
29
+
30
+ `SKILL.md` is byte-identical to the staging copy except its frontmatter `name:`,
31
+ changed from `dollar-platoon` to `dollarplatoon-skill` so it matches the
32
+ directory name, the `skills-lock.json` key, and `vidfarm skills add
33
+ dollarplatoon-skill`. The GitHub mirror makes this exact same single-line change.
34
+
35
+ ## Re-syncing
36
+
37
+ ```bash
38
+ HOST=https://staging.dollarplatoon.com
39
+ DIR=.agents/skills/dollarplatoon-skill
40
+ curl -sSf "$HOST/SKILL.md" -o "$DIR/SKILL.md"
41
+ for p in clients feeds gigs gigworkers orders payouts platform prices \
42
+ pricing-and-tags proofs queue quickstart staging tasks web-pages; do
43
+ curl -sSf "$HOST/skill/$p.md" -o "$DIR/skill/$p.md"
44
+ done
45
+ # then restore the frontmatter name, and refresh skills-lock.json:
46
+ # node scripts/relock-dollarplatoon-skill.mjs
47
+ ```
48
+
49
+ ## What changed in this generation
50
+
51
+ - **Order machines (`inbound_order`)** documented for the first time, on their own
52
+ page. The mode inverts every client/worker sentence in the rest of the skill.
53
+ - **Task escrow** — `task_escrow`, `escrow_funded`, `deposit_id`.
54
+ - **Reputation became an event ledger.** There is no score and no rating; the
55
+ platform publishes `GET /reputation/:wallet/events` and nothing else.
56
+ - **`private_note` is released by `paid_out_at`, not by approval**, and reads as
57
+ `private_note_locked: true` until then.
58
+ - **`/client/*` and `/gigworker/*` URLs are retired**, redirecting to one set of
59
+ `/gigs` pages. Role is a property of the machine, not of the session.
60
+ - **`staging.md` is new**, and carries the two platform cautions worth knowing:
61
+ hot wallet keys are encrypted with the published development default on BOTH
62
+ stages, and `GET /public/read-url` signs any S3 key for any share-token holder.
@@ -0,0 +1,234 @@
1
+ # For clients — getting work done and paying for it
2
+
3
+ You have work. This is how you turn it into a funded gig, get people into it, and pay them.
4
+
5
+ ## Contents
6
+
7
+ - The whole flow in one script
8
+ - Step 1 — create the gig
9
+ - Step 2 — fund it (and why 110%)
10
+ - Step 3 — get workers in
11
+ - Step 4 — send tasks
12
+ - Step 5 — review proofs
13
+ - Step 6 — pay out
14
+ - Choosing a distribution mode
15
+ - Common client mistakes
16
+ - Where to go next
17
+
18
+ ---
19
+
20
+ ## The whole flow in one script
21
+
22
+ Runnable end to end. Each step links to the reference file that explains it properly.
23
+
24
+ ```bash
25
+ KEY=$DOLLAR_PLATOON_API_KEY
26
+ API=https://dollarplatoon.com/api
27
+
28
+ # 1. Create a gig. It comes back with an invite link and an inbound webhook already wired.
29
+ GIG=$(curl -s -X POST $API/gigs -H "x-api-key: $KEY" -H "Content-Type: application/json" -d '{
30
+ "title": "Reddit comments for launch",
31
+ "price": 0.50,
32
+ "terms": "Comment genuinely on the linked thread. Tags used here: reddit, urgent.",
33
+ "distribution": "queue",
34
+ "queue_order": "fifo",
35
+ "review_timeout": 172800
36
+ }')
37
+ GIG_ID=$(echo "$GIG" | jq -r .gig.id)
38
+ TOKEN=$(echo "$GIG" | jq -r .gig.webhook | sed 's/.*token=//')
39
+ echo "$GIG" | jq -r .gig.invite_url # send this to workers
40
+
41
+ # 2. Fund it. Budget 110% of what you expect to pay out.
42
+ curl -s -X POST $API/gigs/$GIG_ID/deposit -H "x-api-key: $KEY" \
43
+ -H "Content-Type: application/json" -d '{"wallet_alias_id":"'$ALIAS'","amount":110}'
44
+
45
+ # 3. Push a task into the queue.
46
+ curl -s -X POST "$API/inbound/webhook/$GIG_ID?token=$TOKEN&subject=Comment+task&tags=reddit" \
47
+ -H "Content-Type: application/json" \
48
+ -d '{"thread_url":"https://reddit.com/r/example/comments/abc","comment":"..."}'
49
+
50
+ # 4. Read what came back in. Proofs arrive here.
51
+ curl -s "$API/gigs/$GIG_ID/dashboard" -H "x-api-key: $KEY" | jq '.proofs[] | {id, status, task_identifier}'
52
+
53
+ # 5. Approve one, with a note the worker will read.
54
+ curl -s -X PATCH $API/gigs/$GIG_ID/proofs/$PROOF_ID -H "x-api-key: $KEY" \
55
+ -H "Content-Type: application/json" -d '{"action":"approve","feedback":"Nice work."}'
56
+
57
+ # 6. Pay. The daily cron also does this on its own.
58
+ curl -s -X POST $API/gigs/$GIG_ID/rollups -H "x-api-key: $KEY"
59
+ ```
60
+
61
+ ---
62
+
63
+ ## Step 1 — create the gig
64
+
65
+ Full field reference: [gigs.md](https://dollarplatoon.com/skill/gigs.md).
66
+
67
+ The four decisions that matter, because changing them later is disruptive:
68
+
69
+ **`distribution`** — how tasks reach workers. See the table further down. If you are unsure,
70
+ `queue` with `fifo` is the safe default: workers pull work when they are ready, and nothing is
71
+ pushed to someone who is asleep.
72
+
73
+ **`price`** — what one task pays by default. Individual tasks can override it, and a task can be
74
+ priced `tbd` and settled at approval. See
75
+ [pricing-and-tags.md](https://dollarplatoon.com/skill/pricing-and-tags.md). For market rates by
76
+ task type, see [prices.md](https://dollarplatoon.com/skill/prices.md).
77
+
78
+ **`review_timeout`** — seconds before an unreviewed proof **auto-approves**. Default 48 hours.
79
+ This protects workers from a client who disappears. If you cannot commit to reviewing, either
80
+ raise it or accept that unreviewed work gets paid.
81
+
82
+ **`terms`** — the one field a worker reads before joining. Put your tag vocabulary here. A worker
83
+ sets their filters before they have seen a single task, so if you send `shortform-vertical` and
84
+ they guess `short`, they silently receive nothing and neither of you gets an error.
85
+
86
+ Every new gig is created with `join_policy: "invite"` and a **default unlimited invite link**,
87
+ returned as `invite_url`. There is no public marketplace — `GET /gigs` returns `410`.
88
+
89
+ ## Step 2 — fund it (and why 110%)
90
+
91
+ ```json
92
+ POST /gigs/:id/deposit { "wallet_alias_id": "...", "amount": 100 }
93
+ → { "tx_hash": "0x...", "available_funds": 100 }
94
+ ```
95
+
96
+ - The worker receives the **full** amount they earned. The platform fee is charged **on top**
97
+ from the gig balance. A $100 payout costs the gig $110.
98
+ - **Funds are locked.** There is no withdrawal function. USDC leaves a gig only as worker
99
+ payouts. Deposit conservatively and top up.
100
+ - **A gig cannot go into debt.** A rollup pre-checks `available_funds >= gross + fee` and fails
101
+ entirely if the gig cannot cover it.
102
+ - Underfunding does not block submissions. Workers can still submit; the proof is approved and
103
+ simply cannot be paid until you deposit. That is a bad look — fund first.
104
+
105
+ ## Step 3 — get workers in
106
+
107
+ Gigs are private networks. People join through an invite link.
108
+
109
+ ```json
110
+ POST /gigs/:id/invites { "max_uses": 1, "email": "worker@example.com", "label": "for Alice" }
111
+ → { "invite": { "token": "a1b2c3d4e5f6", "invite_url": "https://dollarplatoon.com/gig/GIG_.../join?invite=..." } }
112
+ ```
113
+
114
+ Two fields give you every mode you need:
115
+
116
+ - `max_uses`: `1` for one person, `N` for a cohort, `null` for an open link you can post.
117
+ - `email`: bind the link to one address, or `null` for anyone holding it. An email-bound invite
118
+ is also a **pre-approval** — that worker skips `pending_approval` even on a gig that requires it.
119
+
120
+ Revoke with `DELETE /gigs/:id/invites/:token`. Use consumption is atomic, so concurrent joins
121
+ cannot race past `max_uses`.
122
+
123
+ To reach many workers at once through one relationship, publish the gig to a **feed** —
124
+ [feeds.md](https://dollarplatoon.com/skill/feeds.md).
125
+
126
+ ## Step 4 — send tasks
127
+
128
+ Full reference: [tasks.md](https://dollarplatoon.com/skill/tasks.md).
129
+
130
+ Prefer the webhook over email. It is instant, it takes structured data, and it accepts the query
131
+ params that carry a task's price, tags, priority, and assignee:
132
+
133
+ ```bash
134
+ POST /inbound/webhook/:gig_id?token=...&price=2.50&tags=shortform&priority=0
135
+ ```
136
+
137
+ For a human without an account — a teammate, a partner — send them the **Insert Task page**
138
+ instead: `https://dollarplatoon.com/insert/{GIG_ID}?token={SECURITY_TOKEN}`. Same destination, no
139
+ login. See [web-pages.md](https://dollarplatoon.com/skill/web-pages.md).
140
+
141
+ ## Step 5 — review proofs
142
+
143
+ Full reference: [proofs.md](https://dollarplatoon.com/skill/proofs.md).
144
+
145
+ ```json
146
+ PATCH /gigs/:id/proofs/:proof_id
147
+ { "action": "approve", "feedback": "Great work!" }
148
+ { "action": "reject", "rejection_tag": "incomplete", "feedback": "Screenshot doesn't match" }
149
+ ```
150
+
151
+ - **Review promptly.** Silence approves after `review_timeout`.
152
+ - **Always send a `rejection_tag`.** It is written to the rejection's event and is the reason
153
+ anyone reading the worker's ledger will see. It costs them no score — there is no score — but
154
+ it is the only signal other clients get, so label honestly.
155
+ - **Use `not_selected` when you simply hired someone else.** It says "did not get the job", not
156
+ "did bad work", which is what makes free application tasks safe to run.
157
+ - **A rejection returns the task by default.** The work goes back out so somebody else can do
158
+ it. Send `"requeue": false` to close the task with the proof — which is what you want on a
159
+ hiring gig, where rejecting the other applicants must not re-post the job. A returned
160
+ rejection is final: another worker may hold that task now, so its verdict cannot change.
161
+ - **`feedback` is read by the worker on approvals too.** On an `inbound_proof` gig the proof *is*
162
+ the application, so the approval is where you answer: "you're in, join the groupchat: <link>".
163
+ - **Changed your mind? Send the same `PATCH` with the other verdict.** An accidental reject
164
+ becomes an approval, and the reverse — until a payout picks the proof up, which the daily cron
165
+ does. After that the call returns `409`.
166
+ - **A pending proof can disappear.** A worker may withdraw their own submission back to a private
167
+ draft for as long as you have not reviewed it, and it then leaves your dashboard entirely. You
168
+ never see a draft. If they send it again the webhook fires a second time carrying
169
+ `"resubmitted": true`, and your review window restarts from that moment. Reviewing a proof ends
170
+ this — once you have approved or rejected, the verdict is yours alone.
171
+ - Automate it by setting `proof_webhook_url` on the gig and routing submissions to your own
172
+ validator or agent.
173
+
174
+ ## Step 6 — pay out
175
+
176
+ Full reference: [payouts.md](https://dollarplatoon.com/skill/payouts.md).
177
+
178
+ ```json
179
+ POST /gigs/:id/rollups
180
+ → { "rollups": [...], "available_funds": 44.50, "retried_stuck": 1, "skipped_below_minimum": [...] }
181
+ ```
182
+
183
+ A daily cron does this on its own; the manual call is for paying immediately.
184
+
185
+ **Never re-create a failed rollup by hand.** A payout that fails — or one that merely took more
186
+ than 20 seconds to confirm — is retried automatically, reusing the *same* rollup, and the chain
187
+ is checked first so nobody is paid twice. Treat a single `failed` as "not settled yet", never as
188
+ "lost". Triggering a second payout for the same proofs is how you pay twice.
189
+
190
+ ---
191
+
192
+ ## Choosing a distribution mode
193
+
194
+ | Mode | Behaviour | Reach for it when |
195
+ |---|---|---|
196
+ | `queue` | Shared queue. The first worker to poll a task gets it. | The default. High volume, interchangeable work. |
197
+ | `queue_solo` | Every worker gets their own private copy of each task. | You want the same task done by N people (surveys, ratings, redundancy). **Cost is price × workers.** |
198
+ | `round_robin` | Pushed, rotating fairly through active mailboxes. | Even workload across a known roster. |
199
+ | `random` | Pushed to one mailbox at random. | Simple spread, no fairness guarantee needed. |
200
+ | `priority_weighted` | Pushed, weighted by each mailbox's `priority` (1–10). | You want your best workers to get more. |
201
+ | `free_for_all` | Pushed to every active mailbox. | Announcements, or races where you want the first result. |
202
+ | `inbound_proof` | No tasks at all; workers submit proofs directly. | Applications, bounties, anything where the submission *is* the work. |
203
+ | `inbound_order` | **Inverted.** You do the work; outsiders send and fund each order. | You are selling something, not buying it. See [orders.md](https://dollarplatoon.com/skill/orders.md). |
204
+
205
+ `queue_solo` is the one to think twice about: ten tasks and five workers is fifty payouts, not
206
+ ten. Bound it with `max_claims_per_task`.
207
+
208
+ `inbound_order` is not a variant of this page at all — it is the other side of the counter. If
209
+ you pick it, nothing on this page applies: you do not fund the gig, you do not send tasks, and you
210
+ do not approve anything. Read [orders.md](https://dollarplatoon.com/skill/orders.md) first and
211
+ decide deliberately.
212
+
213
+ ## Common client mistakes
214
+
215
+ - **Approving `$0` application proofs.** A rollup skips any mailbox whose approved total is `$0`,
216
+ so those rows are rescanned forever and grow without bound. **Reject** applications with
217
+ `not_selected` instead — same outcome for the applicant, clean ledger for you.
218
+ - **Letting a queue hand a $500 job to whoever polls first.** Use assignment. See the hiring
219
+ walkthrough in [queue.md](https://dollarplatoon.com/skill/queue.md).
220
+ - **Putting the real brief in the public payload.** Advertise in the payload, keep the substance
221
+ in `private_details`, which only the holder sees.
222
+ - **Rotating the security token and forgetting the integrations.** Rotation invalidates the old
223
+ email address, webhook URL, and every Insert Task link at once.
224
+ - **Assuming a worker sees your tags.** If your gig takes tasks by email, set
225
+ `default_task_tags` — an email has nowhere to carry `?tags=`, so filtered workers get nothing.
226
+
227
+ ## Where to go next
228
+
229
+ - Gig fields, invites, mailboxes, funding → [gigs.md](https://dollarplatoon.com/skill/gigs.md)
230
+ - Sending tasks and payload formats → [tasks.md](https://dollarplatoon.com/skill/tasks.md)
231
+ - Ordering, assigning, and pricing work → [queue.md](https://dollarplatoon.com/skill/queue.md),
232
+ [pricing-and-tags.md](https://dollarplatoon.com/skill/pricing-and-tags.md)
233
+ - Dashboards and embeds you can hand to a partner →
234
+ [web-pages.md](https://dollarplatoon.com/skill/web-pages.md)
@@ -0,0 +1,326 @@
1
+ # Feeds — invite-only networks of vending machines
2
+
3
+ A feed sits one level above a gig. A client creates it, mints invite links, and the people who
4
+ accept become **members**. It holds two things:
5
+
6
+ 1. **Registry of gigs** — vending machines, each with a link that can actually be joined.
7
+ 2. **Recent notifications** — a recency stream of `{ title, subtext, destination_url, tags }`.
8
+
9
+ Feeds are **invite only, exactly like gigs**. There is no public board and no anonymous read. A
10
+ non-member gets `404` on every feed route — never `403` — so a stranger cannot confirm a feed
11
+ exists, let alone enumerate feeds.
12
+
13
+ ## Contents
14
+
15
+ - Scopes
16
+ - Routes
17
+ - Create a feed
18
+ - Invites
19
+ - Accept an invite
20
+ - The registry
21
+ - Notifications
22
+ - Filters and paging
23
+ - Members
24
+ - Reader pages and embedding
25
+ - What removing a member does not do
26
+ - Worked example — both sides
27
+
28
+ ---
29
+
30
+ ## Scopes
31
+
32
+ An invite carries a set of scopes, which the joiner inherits. The owner can then edit any one
33
+ member's scopes — the invite is a starting policy, not a permanent binding.
34
+
35
+ | Scope | What it allows |
36
+ |-------|----------------|
37
+ | `read` | Read the registry and the notifications |
38
+ | `register` | List **your own** gigs in this feed's registry |
39
+ | `publish` | Post notifications to this feed |
40
+ | `moderate` | Edit or remove **anybody's** registry entry and **anybody's** notification |
41
+
42
+ Holding any scope implies `read`. The **owner** always has all four and is never a member row, so
43
+ no scope edit can lock them out of their own feed.
44
+
45
+ `moderate` is authority over **content only**. It never reaches the member list, the invites, or
46
+ the feed settings, so a moderator can neither widen their own access nor evict the owner. Treat a
47
+ moderating invite like a key: bind it to one email, or limit it to one use.
48
+
49
+ Content otherwise belongs to whoever put it there. A registry entry belongs to the gig's owner, a
50
+ notification belongs to its author, and only they, the feed owner, or a moderator may change it.
51
+
52
+ ## Routes
53
+
54
+ | Method | Path | Auth | Description |
55
+ |--------|------|------|-------------|
56
+ | POST | `/feeds` | API key | Create a feed; you become its owner |
57
+ | GET | `/feeds/mine` | API key | Feeds you own plus feeds you joined, with `my_scopes` |
58
+ | GET | `/feeds/:feed_id` | Member | The feed. `private_note` only for the owner |
59
+ | PATCH | `/feeds/:feed_id` | Owner | Title, notes, `status` |
60
+ | DELETE | `/feeds/:feed_id` | Owner | Delete the feed. The gigs are never touched |
61
+ | GET | `/feeds/:feed_id/invite-info?invite=` | **No** — valid token | Title, public note, offered scopes |
62
+ | POST | `/feeds/:feed_id/join` | API key | Accept an invite, with an optional display name |
63
+ | POST / GET | `/feeds/:feed_id/invites` | Owner | Mint / list invite links |
64
+ | DELETE | `/feeds/:feed_id/invites/:token` | Owner | Revoke an invite |
65
+ | GET | `/feeds/:feed_id/members` | Owner | Members, cursor-paginated |
66
+ | PATCH | `/feeds/:feed_id/members/:user_id` | Owner | Change one member's scopes |
67
+ | DELETE | `/feeds/:feed_id/members/:user_id` | Owner | Remove a member |
68
+ | PATCH | `/feeds/:feed_id/me` | Member | Change your own display name |
69
+ | GET | `/feeds/:feed_id/registry` | `read` | Page the registry, newest added first |
70
+ | POST | `/feeds/:feed_id/registry` | `register` + own the gig, or `moderate` | List or edit a gig |
71
+ | POST | `/feeds/:feed_id/registry/:gig_id/refresh` | Gig owner, feed owner or `moderate` | Re-mint a dead invite link |
72
+ | DELETE | `/feeds/:feed_id/registry/:gig_id` | Gig owner, feed owner or `moderate` | Remove a gig |
73
+ | GET | `/feeds/:feed_id/notifications` | `read` | Page notifications, newest first |
74
+ | POST | `/feeds/:feed_id/notifications` | `publish` | Publish one |
75
+ | DELETE | `/feeds/:feed_id/notifications/:notif_id` | Author, feed owner or `moderate` | Delete one |
76
+ | GET | `/gigs/:id/feeds` | Gig owner | Which feeds list this gig |
77
+
78
+ ## Create a feed
79
+
80
+ `slug` is optional and **permanent**. It exists so the `feed:<slug>` tag stamped on a listed gig is
81
+ readable by a human; with no slug that tag is the opaque feed id.
82
+
83
+ ```bash
84
+ curl -X POST https://dollarplatoon.com/api/feeds \
85
+ -H "x-api-key: $API_KEY" -H "Content-Type: application/json" \
86
+ -d '{"title":"Cold Email Vending Machines","slug":"cold_email",
87
+ "public_note":"Everything here pays USDC per reply.",
88
+ "private_note":"Only I can read this."}'
89
+ ```
90
+
91
+ ```json
92
+ { "feed": { "id": "FEED_01HX...", "slug": "cold_email", "is_admin": true, "my_scopes": [...] },
93
+ "invite": { "invite_url": "https://dollarplatoon.com/feed/FEED_01HX.../join?invite=...",
94
+ "scopes": ["read"], "max_uses": null } }
95
+ ```
96
+
97
+ Every new feed is created with one unlimited read-only invite, the way a new gig is. Revoke it and
98
+ mint scoped ones whenever you like.
99
+
100
+ `private_note` is stripped from every non-owner response.
101
+
102
+ ## Invites
103
+
104
+ Same shape as gig invites — `max_uses` of `1`, `N`, or `null` (unlimited), and an optional `email`
105
+ that binds the link to one address — plus the `scopes` a joiner inherits.
106
+
107
+ ```bash
108
+ curl -X POST https://dollarplatoon.com/api/feeds/FEED_01HX.../invites \
109
+ -H "x-api-key: $API_KEY" -H "Content-Type: application/json" \
110
+ -d '{"scopes":["read","register"],"max_uses":25,"label":"Partner agencies"}'
111
+ ```
112
+
113
+ **Mint one invite per audience rather than sharing one link.** Scopes and revocation are per
114
+ invite, so a partner who should only read gets `{"scopes":["read"]}` and can be cut off without
115
+ disturbing anybody else.
116
+
117
+ ## Accept an invite
118
+
119
+ Look before you leap — this route needs no account:
120
+
121
+ ```bash
122
+ curl "https://dollarplatoon.com/api/feeds/FEED_01HX.../invite-info?invite=$TOKEN"
123
+ → { "feed": { "id": "...", "title": "...", "public_note": "...", "owner_display_name": "Acme Ops" },
124
+ "invite": { "scopes": ["read","register"], "email_bound": false, "exhausted": false } }
125
+ ```
126
+
127
+ Then join:
128
+
129
+ ```bash
130
+ curl -X POST https://dollarplatoon.com/api/feeds/FEED_01HX.../join \
131
+ -H "x-api-key: $API_KEY" -H "Content-Type: application/json" \
132
+ -d '{"invite":"a1b2c3...","display_name":"Acme Ops"}'
133
+ ```
134
+
135
+ Re-joining is a **safe no-op that consumes no invite use**, so retrying after a timeout is never
136
+ destructive. `display_name` is what the feed owner sees in the member list; it is optional.
137
+
138
+ ## The registry
139
+
140
+ **You may only list a gig you own.** Anything else is `403` — otherwise any member could advertise
141
+ somebody else's gig on an invite link of their choosing.
142
+
143
+ In the web app this is the **Add Gig** button on the feed's Registry tab, which lists only gigs
144
+ you own, searchable by title or id. It is greyed out if you lack the `register` scope.
145
+
146
+ ```bash
147
+ curl -X POST https://dollarplatoon.com/api/feeds/FEED_01HX.../registry \
148
+ -H "x-api-key: $API_KEY" -H "Content-Type: application/json" \
149
+ -d '{"gig_id":"GIG_01HX...","note":"500 sends/day","tags":["cold_email"]}'
150
+ ```
151
+
152
+ The invite link is minted from the gig's own invites: an unlimited invite is preferred over a
153
+ counted one, and revoked or email-bound invites are never offered. Pass an explicit
154
+ `{"invite":"<token>"}` to choose one — it is checked for existence, revocation, and exhaustion
155
+ first. If an `invite` gig has no usable invite the entry is still created, with a `warning`: mint
156
+ one with `POST /gigs/:id/invites`, then call the `refresh` route.
157
+
158
+ Reading the registry re-derives liveness for that page and **writes nothing**:
159
+
160
+ ```json
161
+ { "items": [ { "gig_id": "GIG_01HX...", "title": "...", "note": "...", "tags": ["cold_email"],
162
+ "invite_url": "https://dollarplatoon.com/gig/GIG_01HX.../join?invite=...",
163
+ "invite_live": true, "added_at": "...", "can_edit": true, "can_delete": true,
164
+ "owner": { "user_id": "USER_...", "display_name": "Acme Ops" } } ],
165
+ "next_cursor": null }
166
+ ```
167
+
168
+ `owner` is the **client who listed the gig** — the same person who owns it, because a row is
169
+ credited to the gig's owner even when a moderator adds it. It is resolved when you read, so a
170
+ client who renames themselves does not leave a stale name behind. It is `null` if that account
171
+ cannot be read.
172
+
173
+ It carries a **name and an account id, never an email address**. The same rule holds for
174
+ `owner_display_name` on a feed and `author_display_name` on a notification. An account that set
175
+ no display name is labelled by the local part of its email, so no address is ever published to
176
+ other members. Only `GET /feeds/:feed_id/members`, which the feed owner alone may call, returns
177
+ real email addresses.
178
+
179
+ `can_edit` and `can_delete` say whether **you** may change this row: true for the gig's owner, the
180
+ feed owner, and a moderator. They are hints for a UI. The write routes re-check them.
181
+
182
+ **`invite_live` has three values and the third is the one that matters:**
183
+
184
+ | Value | Meaning |
185
+ |---|---|
186
+ | `true` | Joinable right now. |
187
+ | `false` | Not joinable — tell the gig owner to mint an invite and refresh. |
188
+ | `null` | **NOT CHECKED.** Past the per-page probe cap, or the probe failed. **Never read `null` as dead.** |
189
+
190
+ Ordering is newest **added**, not newest gig. Re-registering an existing gig updates the entry and
191
+ deliberately keeps its original `added_at`, so editing an old entry does not promote it.
192
+
193
+ Registering also stamps a `feed:<slug|id>` tag on the gig, so a client scanning their own gig list
194
+ can see where it is published. That tag is a **mirror for humans, never the source of truth** —
195
+ the tag list caps at 25 entries and a client can hand-edit it. The registry row is what counts.
196
+
197
+ ## Notifications
198
+
199
+ ```bash
200
+ curl -X POST https://dollarplatoon.com/api/feeds/FEED_01HX.../notifications \
201
+ -H "x-api-key: $API_KEY" -H "Content-Type: application/json" \
202
+ -d '{"title":"New batch live","subtext":"2000 leads, pays on reply",
203
+ "destination_url":"https://example.com/brief","tags":["cold_email","urgent"]}'
204
+ ```
205
+
206
+ `destination_url` must be `https://` — anything else is rejected, because the value renders as a
207
+ link for every member.
208
+
209
+ `author_display_name` is stamped by the server. You cannot set it. It is your name in this feed,
210
+ or your account name if you set none — **never your email address**, because every member of the
211
+ feed reads it.
212
+
213
+ Newest first. Each item carries `can_delete`, which is `true` for the author, the feed owner and a
214
+ moderator — use it rather than guessing who may delete. Caps: title 200 characters, subtext 2000, tags 25 of
215
+ 256 characters.
216
+
217
+ An agent polling notifications should record the newest `id` it has seen and stop paging when it
218
+ reaches that one, rather than re-reading the whole stream.
219
+
220
+ ## Filters and paging
221
+
222
+ Both list routes accept `?limit=` (1–200, default 50), `?cursor=`, `?q=` (text search), `?tag=`
223
+ (comma-separated), `?tag_match=` (`substring` | `prefix` | `exact`) and `?tag_mode=`
224
+ (`any` | `all`).
225
+
226
+ Filtering happens inside a page, so **a filtered page can be shorter than `limit` while
227
+ `next_cursor` is still set. Page until `next_cursor` is `null`.** See
228
+ [pricing-and-tags.md](https://dollarplatoon.com/skill/pricing-and-tags.md) for why.
229
+
230
+ ## Members
231
+
232
+ Owner only, and **no scope grants it** — the list carries every member's email address.
233
+
234
+ ```json
235
+ GET /feeds/:feed_id/members
236
+ → { "members": [ { "user_id": "USER_...", "email": "...", "display_name": "Acme Ops",
237
+ "scopes": ["read","register"], "joined_at": "..." } ],
238
+ "owner": { "user_id": "USER_...", "email": "...", "is_owner": true },
239
+ "next_cursor": null }
240
+ ```
241
+
242
+ ```json
243
+ PATCH /feeds/:feed_id/members/:user_id { "scopes": ["read", "publish"] }
244
+ DELETE /feeds/:feed_id/members/:user_id
245
+ ```
246
+
247
+ Neither can target the owner (`400`).
248
+
249
+ ## Reader pages and embedding
250
+
251
+ | Page | URL |
252
+ |------|-----|
253
+ | Notifications (the default tab) | `https://dollarplatoon.com/feed/<feed_id>/notifications` |
254
+ | Registry | `https://dollarplatoon.com/feed/<feed_id>/registry` |
255
+ | Accept an invite | `https://dollarplatoon.com/feed/<feed_id>/join?invite=<token>` |
256
+
257
+ `https://dollarplatoon.com/feed/<feed_id>` with no tab opens the notifications.
258
+
259
+ Plus `https://dollarplatoon.com/feed/<feed_id>/settings` — the owner's one settings page: feed
260
+ details, invite links, and members, in that order. `#members` opens it at the member list. Two
261
+ older paths redirect to it: `/client/feed/<feed_id>` (from before the personas were removed) and
262
+ `/feed/<feed_id>/members` (the old member page).
263
+
264
+ These render inside the app with the navbar, like every other signed-in page. To frame one,
265
+ strip the chrome with `?hide_navbar=true&hide_logo=true`.
266
+
267
+ They need a member session, but **not necessarily `?api_key=`** — a browser that is already
268
+ signed in just opens the URL. Add the key when the context has no session of its own, which is
269
+ the normal case for a cross-site iframe, since browsers partition storage per embedding site. See
270
+ [web-pages.md](https://dollarplatoon.com/skill/web-pages.md).
271
+
272
+ ## What removing a member does not do
273
+
274
+ Removing somebody ends their access to the feed. It does **not** retract the gig invite links they
275
+ already copied out of the registry — those are the gigs' own tokens. To kill one, revoke that
276
+ invite on the gig itself with `DELETE /gigs/:id/invites/:token`.
277
+
278
+ ## Worked example — both sides
279
+
280
+ **Stand up a feed and fill it:**
281
+
282
+ ```bash
283
+ # 1. Create it. The slug is optional and permanent.
284
+ FEED=$(curl -s -X POST https://dollarplatoon.com/api/feeds \
285
+ -H "x-api-key: $API_KEY" -H "Content-Type: application/json" \
286
+ -d '{"title":"Cold Email Machines","slug":"cold_email","public_note":"Pays USDC per reply."}' \
287
+ | jq -r .feed.id)
288
+
289
+ # 2. List two of your own vending machines.
290
+ for GIG in GIG_01AAA GIG_01BBB; do
291
+ curl -s -X POST https://dollarplatoon.com/api/feeds/$FEED/registry \
292
+ -H "x-api-key: $API_KEY" -H "Content-Type: application/json" \
293
+ -d "{\"gig_id\":\"$GIG\",\"tags\":[\"cold_email\"]}"
294
+ done
295
+
296
+ # 3. Announce a batch.
297
+ curl -s -X POST https://dollarplatoon.com/api/feeds/$FEED/notifications \
298
+ -H "x-api-key: $API_KEY" -H "Content-Type: application/json" \
299
+ -d '{"title":"2,000 new leads live","subtext":"Pays $0.40 per verified reply",
300
+ "destination_url":"https://example.com/brief","tags":["cold_email","urgent"]}'
301
+
302
+ # 4. Mint an invite for the audience that should see it.
303
+ curl -s -X POST https://dollarplatoon.com/api/feeds/$FEED/invites \
304
+ -H "x-api-key: $API_KEY" -H "Content-Type: application/json" \
305
+ -d '{"scopes":["read"],"max_uses":null,"label":"Discord announcement"}' | jq -r .invite.invite_url
306
+ ```
307
+
308
+ **An agent consuming it:**
309
+
310
+ ```bash
311
+ # Inspect the invite without an account, then join.
312
+ curl -s "https://dollarplatoon.com/api/feeds/$FEED/invite-info?invite=$TOKEN" \
313
+ | jq '.feed.title, .invite.scopes'
314
+
315
+ curl -s -X POST https://dollarplatoon.com/api/feeds/$FEED/join \
316
+ -H "x-api-key: $API_KEY" -H "Content-Type: application/json" \
317
+ -d '{"invite":"'$TOKEN'","display_name":"my-agent"}'
318
+
319
+ # Take only the machines whose link actually works right now.
320
+ # Note: `!= false` keeps null (NOT CHECKED) — dropping it would discard joinable machines.
321
+ curl -s "https://dollarplatoon.com/api/feeds/$FEED/registry?tag=cold_email" \
322
+ -H "x-api-key: $API_KEY" | jq -r '.items[] | select(.invite_live != false) | .invite_url'
323
+
324
+ # From here on, work is discovered with /work/available — never with the registry.
325
+ curl -s "https://dollarplatoon.com/api/work/available?only_with_work=true" -H "x-api-key: $API_KEY"
326
+ ```