@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,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
|
+
```
|