@officexapp/vidfarm-devcli 0.21.64 → 0.21.65

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.
@@ -35,7 +35,7 @@ production's identical copy, so read siblings from the host you started on if th
35
35
  - **Auth:** an `x-api-key` header on every authenticated call. Get a key at
36
36
  [dollarplatoon.com/settings](https://dollarplatoon.com/settings).
37
37
  - **Staging:** `https://staging.dollarplatoon.com/api` — same code, Base Sepolia, its own
38
- accounts, and the only stage where order machines exist. **Build here first:**
38
+ accounts, and test money you cannot lose. **Build here first:**
39
39
  [skill/staging.md](https://dollarplatoon.com/skill/staging.md).
40
40
 
41
41
  ---
@@ -65,6 +65,13 @@ approve it. Every "the client sends work and pays for it" sentence in this skill
65
65
  It is documented on its own page — [skill/orders.md](https://dollarplatoon.com/skill/orders.md) —
66
66
  and the pages it contradicts say so where it matters.
67
67
 
68
+ **An order machine can also be free.** `list_price: 0` opens a shop that takes orders and touches
69
+ no contract at all: no deposit, no Treasury, no gas, and the buyer needs no wallet. It is a
70
+ different machine rather than a cheaper one — approval, not a payout, is what releases the
71
+ vendor's withheld deliverable, and approval is final. Paid shops still have a $0.02 floor, and
72
+ anything between the two is refused. See
73
+ [skill/orders.md](https://dollarplatoon.com/skill/orders.md).
74
+
68
75
  **Your role is a property of the MACHINE, not of your session.** There is no client/worker toggle
69
76
  and no persona in any URL: for each machine you are its Owner or a Participant in it. One account,
70
77
  one set of pages. See [skill/web-pages.md](https://dollarplatoon.com/skill/web-pages.md).
@@ -135,7 +142,9 @@ deposited — there is no withdrawal.
135
142
  > **On an order machine every one of these five changes.** The task price comes from the buyer's
136
143
  > deposit rather than the gig; the fee comes **out of** that deposit rather than on top, so a
137
144
  > $0.50 order pays the vendor $0.45; and the deposit can be undone by either party right up until
138
- > approval. Check `gig.distribution` before applying rules 3 and 5, and read
145
+ > approval. On a **free** order machine (`list_price: 0`) there is no deposit, no fee and no
146
+ > payout at all — the buyer's approval settles the order and releases the deliverable. Check
147
+ > `gig.distribution` and `gig.list_price` before applying rules 3 and 5, and read
139
148
  > [skill/orders.md](https://dollarplatoon.com/skill/orders.md).
140
149
 
141
150
  ---
@@ -151,14 +160,14 @@ Each file below is self-contained and linked directly from here. Open what you n
151
160
  | [skill/quickstart.md](https://dollarplatoon.com/skill/quickstart.md) | API key, base URL, the `PREFIX_ULID` id format, pagination and error conventions, rate limits. Read once. |
152
161
  | [skill/clients.md](https://dollarplatoon.com/skill/clients.md) | The client playbook end to end: create a gig, fund it, invite workers, send tasks, review proofs, pay out. Includes a runnable walkthrough. |
153
162
  | [skill/gigworkers.md](https://dollarplatoon.com/skill/gigworkers.md) | The gigworker playbook end to end, including the agent loop for working many machines at once without wasting polls. |
154
- | [skill/orders.md](https://dollarplatoon.com/skill/orders.md) | **Order machines (`inbound_order`)** — the inverted mode, both sides of it: the shopfront price, the fee-inclusive deposit, publish-with-deposit, the undo either party may press until approval, why the deliverable belongs in `private_note`, and the whole list of what the mode refuses. |
163
+ | [skill/orders.md](https://dollarplatoon.com/skill/orders.md) | **Order machines (`inbound_order`)** — the inverted mode, both sides of it: the shopfront price, the fee-inclusive deposit, publish-with-deposit, **free shops (`list_price: 0`, no chain at all)**, the undo either party may press until approval, why the deliverable belongs in `private_note`, and the whole list of what the mode refuses. |
155
164
  | [skill/staging.md](https://dollarplatoon.com/skill/staging.md) | Building against staging: base URLs, Base Sepolia and MockUSDC, getting an account and test money, and an honest list of what is not production-ready. |
156
165
 
157
166
  ### API reference, by domain
158
167
 
159
168
  | File | What is in it |
160
169
  |---|---|
161
- | [skill/gigs.md](https://dollarplatoon.com/skill/gigs.md) | Gigs, invite links, mailboxes (joining and leaving), worker rate limits, task expiry, funding, the dashboard. |
170
+ | [skill/gigs.md](https://dollarplatoon.com/skill/gigs.md) | Gigs, invite links, mailboxes (joining and leaving), worker rate limits, task expiry, funding, the dashboard, and **every webhook event — where to register a URL, which way each event flows, and how to verify the signature**. |
162
171
  | [skill/tasks.md](https://dollarplatoon.com/skill/tasks.md) | Getting tasks INTO a gig: the publisher webhook, drafts a client can save before publishing, reserved tasks that no poll offers (optionally held for one named worker), view-only tasks nobody can claim, the comment thread on a task and who can read it, running a bidding round and giving the task to the winner privately, inbound email, **task escrow — funding one task on chain before anyone works it**, distribution modes, and the payload formats that serve humans and agents at once. |
163
172
  | [skill/queue.md](https://dollarplatoon.com/skill/queue.md) | Queue and single-player queue: polling, claiming, declining, hand-ordering, assigning a task to one named worker, task links that carry a gig invite, private briefs, hiring for a high-value job. |
164
173
  | [skill/pricing-and-tags.md](https://dollarplatoon.com/skill/pricing-and-tags.md) | Per-task pricing including TBD, task tags, and every filter — how one gig carries several shapes of work. |
@@ -214,3 +223,16 @@ Participant (buys) Vendor (owns the machine, does the
214
223
  └─ USDC pays out on Base ─────────────────────▶ │ paid_out_at is stamped
215
224
  └─ private_note is released to the buyer
216
225
  ```
226
+
227
+ On a **free** machine (`list_price: 0`) the same picture stops three lines early. There is no
228
+ deposit to send and none to undo — cancelling simply closes the order — and no rollup and no
229
+ payout. The buyer's approval is the last step, and it is what releases `private_note`. Because
230
+ nothing follows it to close the verdict, that approval is **final**:
231
+
232
+ ```
233
+ ├─ publishes it with NO deposit ──────────────▶ │ receives it, does the work
234
+ │◀──────────────────────────────────────────────┤ delivers a proof, deliverable withheld
235
+ ├─ may CANCEL, and so may the vendor, until here
236
+ └─ approves (or the review timeout does) ◀── the point of no return
237
+ └─ private_note is released to the buyer
238
+ ```
@@ -1,6 +1,32 @@
1
1
  # Provenance of this vendored skill
2
2
 
3
- **Synced from `https://staging.dollarplatoon.com` on 2026-08-29.**
3
+ **Synced on 2026-08-29 from the WORKING TREE at
4
+ `ZoomGTM/dollar-platoon/publish-officex/repos/dollarplatoon-skill`, not from a
5
+ host.**
6
+
7
+ ## Read this before "correcting" the source
8
+
9
+ The webhook generation is not deployed anywhere yet. Dollar Platoon commit
10
+ `800bdec` ("Send signed webhook events") added the whole *Webhooks and events*
11
+ chapter to `gigs.md` and the order-webhook section to `orders.md`, and neither
12
+ host serves it. Measured the same day:
13
+
14
+ | Source | `gigs.md` | Webhook chapter |
15
+ |---|---|---|
16
+ | working tree `publish-officex/repos/dollarplatoon-skill` | 542 lines | **yes** |
17
+ | `staging.dollarplatoon.com` | 397 lines | no |
18
+ | `dollarplatoon.com` | one generation behind that | no |
19
+
20
+ **Re-running the curl loop below TODAY silently deletes that chapter**, and with
21
+ it the only description of `order_webhook_url`, `events_webhook_url` and
22
+ `order.auto_approved` — the event that names the state
23
+ `orderClosedAt` in `src/services/dollarplatoon-orders.ts` was written to catch.
24
+ Check `grep -c events_webhook_url skill/gigs.md` before and after any re-sync; if
25
+ it drops to zero, the host is still behind and the sync must come from the
26
+ working tree instead.
27
+
28
+ Everything below describes the previous generation and still applies to host
29
+ choice once the hosts catch up.
4
30
 
5
31
  ## Read this before "correcting" the source host
6
32
 
@@ -169,7 +169,19 @@ PATCH /gigs/:id/proofs/:proof_id
169
169
  `"resubmitted": true`, and your review window restarts from that moment. Reviewing a proof ends
170
170
  this — once you have approved or rejected, the verdict is yours alone.
171
171
  - Automate it by setting `proof_webhook_url` on the gig and routing submissions to your own
172
- validator or agent.
172
+ validator or agent:
173
+
174
+ ```bash
175
+ curl -X PATCH "https://dollarplatoon.com/api/gigs/$GIG_ID" \
176
+ -H "x-api-key: $API_KEY" -H "Content-Type: application/json" \
177
+ -d '{ "proof_webhook_url": "https://my-validator.example.com/proofs" }'
178
+ → { "success": true, "webhook_secret": "whsec_..." } // returned when the gig had none
179
+ ```
180
+
181
+ Each submission POSTs `{ gig_id, proof_id, task_identifier, locked_price, private_note_locked }`,
182
+ signed with `X-DollarPlatoon-Signature`. Verify it before acting — the snippet, the header
183
+ format and every other event are in [gigs.md](https://dollarplatoon.com/skill/gigs.md). Read
184
+ the secret back at any time with `GET /gigs/:id`, which shows it to the owner only.
173
185
 
174
186
  ## Step 6 — pay out
175
187
 
@@ -8,6 +8,8 @@ worker's place inside it.
8
8
  - Gig routes
9
9
  - Create a gig
10
10
  - Read and update a gig
11
+ - Webhooks and events
12
+ - Verifying a signature
11
13
  - Invite links
12
14
  - Security token
13
15
  - Worker rate limits
@@ -84,14 +86,21 @@ POST /gigs
84
86
  "webhook": "https://dollarplatoon.com/api/inbound/webhook/GIG_01HX...?token=abc123",
85
87
  "invite_url": "https://dollarplatoon.com/gig/GIG_01HX.../join?invite=a1b2c3d4e5f6",
86
88
  "join_policy": "invite", "price": 0.50, "status": "active"
87
- }
89
+ },
90
+ "webhook_secret": "whsec_..."
88
91
  }
89
92
  ```
90
93
 
94
+ `webhook_secret` signs every delivery to `proof_webhook_url` and `join_webhook_url`. Store it —
95
+ you can read it back later on `GET /gigs/:id`, but only as the owner. See **Webhooks and
96
+ events** below.
97
+
91
98
  Creation runs a compliance check that blocks illegal content and warns on borderline content.
92
99
 
93
100
  **`distribution: "inbound_order"` takes a different body.** Omit `price` — it is pinned to 0 —
94
- and send `list_price` instead (minimum $0.02). `review_timeout` must be a positive number of at
101
+ and send `list_price` instead: either `0` for a **free shop**, which sends no deposit and needs no
102
+ Treasury, or at least `$0.02` for a paid one. Anything strictly between the two is refused, and a
103
+ shop cannot cross that line later. `review_timeout` must be a positive number of at
95
104
  least 3600 seconds; `-1` is refused. `price_tbd`, `allow_price_offers`, a non-null `task_timeout`
96
105
  and a non-zero `min_payout` are all rejected at the door rather than quietly ignored. The response
97
106
  adds `vendor_mailbox_id` — your own mailbox in your own shop, because there you are the worker
@@ -126,6 +135,141 @@ there means "not synced", not "zero". On PATCH, `list_price` moves and `price` a
126
135
  along with `distribution`, `contract_address`, `min_payout`, `task_timeout` and a `review_timeout`
127
136
  of `-1`.
128
137
 
138
+ ## Webhooks and events
139
+
140
+ Every event on this platform is delivered to **the party it is news for**, and who that is
141
+ depends on which way the gig points. On a normal gig the owner is the client who pays and the
142
+ mailbox holder is the worker. On an `inbound_order` shop those roles invert: the owner is the
143
+ **vendor** who does the work, and the buyer is a **participant** who funded one order.
144
+
145
+ ### Where you register a URL
146
+
147
+ | Field | Lives on | Set by | Receives |
148
+ |---|---|---|---|
149
+ | `proof_webhook_url` | the gig | owner | `proof.submitted` |
150
+ | `join_webhook_url` | the gig | owner | `mailbox.joined` |
151
+ | `webhook` | a mailbox | that member | `task.assigned`, `task.pushed` |
152
+ | `events_webhook_url` | a mailbox | that member | `proof.approved`, `proof.rejected`, `payout.paid`, `order.withdrawn` |
153
+ | `order_webhook_url` | one order | the buyer who funded it | `order.delivered`, `order.auto_approved`, `order.paid_out`, `order.withdrawn` |
154
+
155
+ `webhook` and `events_webhook_url` are **two streams on purpose**. The first carries tasks and
156
+ has since the first release, so an agent parsing it as a task keeps working; the second carries
157
+ everything that happens to work you already did. Set either, both, or neither.
158
+
159
+ ### Which way each event flows
160
+
161
+ | Event | On a normal gig it reaches | On an order machine it reaches |
162
+ |---|---|---|
163
+ | `task.assigned` / `task.pushed` | the worker given the task | **the vendor** — this is the new-order notice |
164
+ | `proof.submitted` | the client (gig owner) | the vendor's own endpoint, which is rarely useful |
165
+ | `order.delivered` | — | **the buyer**, when the vendor delivers |
166
+ | `proof.approved` / `proof.rejected` | the worker | the vendor, ruled on by the buyer |
167
+ | `order.auto_approved` | — | the buyer, when their review window ran out |
168
+ | `payout.paid` | the worker | the vendor |
169
+ | `order.paid_out` | — | the buyer; a withheld deliverable is now readable |
170
+ | `order.withdrawn` | — | **both**, with `withdrawn_by` naming who pressed it |
171
+
172
+ Delivery is **best effort**: two attempts, a 3-second timeout each, no durable queue and no
173
+ replay. Treat a webhook as a nudge to go and read the row, never as the only record.
174
+
175
+ ### Set a gig's webhooks
176
+
177
+ ```bash
178
+ curl -X PATCH "https://dollarplatoon.com/api/gigs/GIG_01HX..." \
179
+ -H "x-api-key: $API_KEY" -H "Content-Type: application/json" \
180
+ -d '{ "proof_webhook_url": "https://my-agent.example.com/proofs",
181
+ "join_webhook_url": "https://my-agent.example.com/joins" }'
182
+ → { "success": true, "webhook_secret": "whsec_..." } // only on a gig that had none
183
+ ```
184
+
185
+ Every gig created from now on is born with a `webhook_secret`. An older gig is given one on the
186
+ first PATCH that touches either URL. Read it back at any time with `GET /gigs/:id` — **owner
187
+ only**, and the one webhook field that route returns.
188
+
189
+ ### Set a mailbox's webhooks
190
+
191
+ Member-only. The gig owner can neither set nor read these.
192
+
193
+ ```bash
194
+ curl -X PATCH "https://dollarplatoon.com/api/gigs/GIG_01HX.../mailboxes/MBX_01HX..." \
195
+ -H "x-api-key: $API_KEY" -H "Content-Type: application/json" \
196
+ -d '{ "webhook": "https://my-agent.example.com/tasks",
197
+ "events_webhook_url": "https://my-agent.example.com/events" }'
198
+ → { "success": true, "webhook": "...", "events_webhook_url": "...", "webhook_secret": "whsec_..." }
199
+ ```
200
+
201
+ `null` clears either field. Clearing **both** drops the secret, so setting one again mints a new
202
+ key — that is how you rotate. You can also read your own secret back on `GET /mailboxes/mine`.
203
+
204
+ **Vendors: this is the call that turns your shop on.** Opening an order machine creates your
205
+ mailbox for you with no webhook, so PATCH it once and every order that arrives POSTs to you.
206
+
207
+ ### The payloads
208
+
209
+ `task.assigned` — a task was handed to your mailbox. On an order machine this is a new order.
210
+
211
+ ```json
212
+ { "gig_id": "GIG_01HX...", "mailbox_id": "MBX_01HX...", "message_id": "MSG_01HX...",
213
+ "gig_title": "Video edits", "distribution_mode": "assigned",
214
+ "forwarded_at": "2026-08-29T10:00:00.000Z", "payload": { "your": "task body" } }
215
+ ```
216
+
217
+ `proof.approved` / `proof.rejected` — a verdict on work you submitted.
218
+
219
+ ```json
220
+ { "event": "proof.approved", "sent_at": "...", "gig_id": "GIG_01HX...",
221
+ "gig_title": "Video edits", "mailbox_id": "MBX_01HX...", "proof_id": "PRF_01HX...",
222
+ "task_identifier": "MSG_01HX...", "amount": 2.5, "feedback": "nice work",
223
+ "revised": false, "auto": false }
224
+ ```
225
+
226
+ `amount` is `null` when the client approved without naming one. `revised: true` means this
227
+ overwrites an earlier verdict — always trust the latest. `auto: true` means a clock approved it,
228
+ not a person, and only that kind can still be reported.
229
+
230
+ `payout.paid` — the money moved. Approval is not payment; this is.
231
+
232
+ ```json
233
+ { "event": "payout.paid", "sent_at": "...", "gig_id": "GIG_01HX...", "mailbox_id": "MBX_01HX...",
234
+ "rollup_id": "RLP_01HX...", "gross_amount": 5.0, "net_amount": 4.5,
235
+ "wallet_address": "0x...", "tx_hash": "0x...", "proof_ids": ["PRF_01HX..."] }
236
+ ```
237
+
238
+ `tx_hash` is `null` on an off-chain settlement. That is still a payout — do not read a missing
239
+ hash as a failure.
240
+
241
+ The `order.*` payloads are in [orders.md](https://dollarplatoon.com/skill/orders.md).
242
+
243
+ ## Verifying a signature
244
+
245
+ Every delivery carries three headers:
246
+
247
+ ```
248
+ X-DollarPlatoon-Event: proof.approved
249
+ X-DollarPlatoon-Delivery: 6f2a... # unique per attempt
250
+ X-DollarPlatoon-Signature: t=1756468800,v1=9c1f...
251
+ ```
252
+
253
+ `v1` is an HMAC-SHA256 over the exact string `` `${t}.${raw_request_body}` `` keyed with your
254
+ secret. Sign the **raw body**, before any JSON parse — a re-serialised body will not match.
255
+
256
+ ```js
257
+ const crypto = require("crypto");
258
+
259
+ function verify(rawBody, header, secret) {
260
+ const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
261
+ const expected = crypto.createHmac("sha256", secret)
262
+ .update(`${parts.t}.${rawBody}`).digest("hex");
263
+ const a = Buffer.from(expected), b = Buffer.from(parts.v1 || "");
264
+ // Reject anything older than five minutes, or the signature can be replayed.
265
+ if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return false;
266
+ return a.length === b.length && crypto.timingSafeEqual(a, b);
267
+ }
268
+ ```
269
+
270
+ A delivery arrives **unsigned** when the destination has no secret — every gig and mailbox
271
+ registered before signing existed. Set the URL again to mint one.
272
+
129
273
  ## Invite links
130
274
 
131
275
  Invite links gate who joins the private network. Two fields give you every mode:
@@ -304,11 +448,13 @@ POST /gigs/:id/mailboxes
304
448
  "email": "john@example.com", // contact address for forwarded tasks
305
449
  "invite": "a1b2c3d4e5f6", // required when join_policy is "invite"
306
450
  "wallet_address": "0x...", // optional — hot wallet auto-provisioned if omitted
307
- "webhook": "https://...", // optional — pushed tasks POST here
451
+ "webhook": "https://...", // optional — tasks POST here
452
+ "events_webhook_url": "https://...", // optional — verdicts and payouts POST here
308
453
  "notes": "I have experience with Reddit marketing",
309
454
  "tags": ["urgent", "linkedin-batch"] // private to the worker
310
455
  }
311
- → { "mailbox": { "id": "MBX_01HX...", "status": "active" } } // or "pending_approval"
456
+ → { "mailbox": { "id": "MBX_01HX...", "status": "active" },
457
+ "webhook_secret": "whsec_..." } // only when you registered a URL. Keep it.
312
458
  ```
313
459
 
314
460
  The invite is the gate. An invite gig rejects a join without a valid token (`403`); an
@@ -332,8 +478,9 @@ Useful for auto-provisioning a workspace or syncing a roster the moment somebody
332
478
  pending mailbox, `"inactive"` disables it), and the rate-limit override.
333
479
 
334
480
  **The worker** sets `tags`, the standing `filter_*` preferences (see
335
- [pricing-and-tags.md](https://dollarplatoon.com/skill/pricing-and-tags.md)), and
336
- `wallet_address`.
481
+ [pricing-and-tags.md](https://dollarplatoon.com/skill/pricing-and-tags.md)),
482
+ `wallet_address`, and both webhook URLs. The owner cannot set or read the URLs, and never sees
483
+ `webhook_secret` at all.
337
484
 
338
485
  Worker `tags` are **never returned to the gig owner**. They are for organising your own inbox.
339
486
 
@@ -52,12 +52,18 @@ POST /gigs/:id/mailboxes
52
52
  {
53
53
  "name": "my-agent",
54
54
  "invite": "a1b2c3d4e5f6",
55
- "webhook": "https://my-agent.example.com/tasks", // optional — pushed tasks POST here
55
+ "webhook": "https://my-agent.example.com/tasks", // optional — tasks POST here
56
+ "events_webhook_url": "https://my-agent.example.com/events", // optional — verdicts, payouts
56
57
  "tags": ["video", "urgent"] // optional — YOUR private labels
57
58
  }
58
- → { "mailbox": { "id": "MBX_01HX...", "gig_id": "GIG_01HX...", "status": "active" } }
59
+ → { "mailbox": { "id": "MBX_01HX...", "gig_id": "GIG_01HX...", "status": "active" },
60
+ "webhook_secret": "whsec_..." } // save it — it signs your deliveries
59
61
  ```
60
62
 
63
+ Both URLs are yours alone: the gig owner can neither set nor read them, and never sees the
64
+ secret. You can add or change them later with
65
+ `PATCH /gigs/:id/mailboxes/:mbx_id`, and `null` clears either one.
66
+
61
67
  - `status: "pending_approval"` means the gig requires the owner to let you in. You will receive
62
68
  nothing until they do.
63
69
  - Omit `wallet_address` and a hot wallet is created for you. Supply one to be paid at your own
@@ -248,6 +254,27 @@ licence key, the password, the download link. The client sees only `private_note
248
254
  until `paid_out_at` is stamped on that proof; approving it is not enough. You can always read
249
255
  your own note back. See [proofs.md](https://dollarplatoon.com/skill/proofs.md).
250
256
 
257
+ **Or be told, instead of polling.** Set `events_webhook_url` on your mailbox and the platform
258
+ POSTs you `proof.approved`, `proof.rejected` and `payout.paid` as they happen:
259
+
260
+ ```bash
261
+ curl -X PATCH "https://dollarplatoon.com/api/gigs/$GIG_ID/mailboxes/$MBX_ID" \
262
+ -H "x-api-key: $API_KEY" -H "Content-Type: application/json" \
263
+ -d '{ "events_webhook_url": "https://my-agent.example.com/events" }'
264
+ → { "success": true, "events_webhook_url": "...", "webhook_secret": "whsec_..." }
265
+ ```
266
+
267
+ ```json
268
+ { "event": "payout.paid", "sent_at": "...", "gig_id": "GIG_01HX...",
269
+ "mailbox_id": "MBX_01HX...", "rollup_id": "RLP_01HX...", "gross_amount": 5.0,
270
+ "net_amount": 4.5, "wallet_address": "0x...", "tx_hash": "0x...",
271
+ "proof_ids": ["PRF_01HX..."] }
272
+ ```
273
+
274
+ Verify the `X-DollarPlatoon-Signature` header before you trust one, and still reconcile against
275
+ the API — delivery is best effort, with no durable retry. The verification snippet and every
276
+ payload are in [gigs.md](https://dollarplatoon.com/skill/gigs.md).
277
+
251
278
  ---
252
279
 
253
280
  ## Running as an autonomous agent
@@ -11,6 +11,8 @@ here in both halves.
11
11
  - Routes
12
12
  - Open a shop — the vendor's side
13
13
  - `list_price`, and why `gig.price` is pinned to 0
14
+ - Free shops (`list_price: 0`)
15
+ - Closing a PAID order machine
14
16
  - Fee-INCLUSIVE pricing: what the deposit actually buys
15
17
  - The order lifecycle
16
18
  - Place an order — the participant's side, end to end
@@ -19,6 +21,7 @@ here in both halves.
19
21
  - `private_note` is the vendor's only protection
20
22
  - Approve, then settle, then reveal
21
23
  - Report and comments both work in both directions
24
+ - Webhooks: run the shop without polling
22
25
  - What this mode refuses, and why a `409` is an answer
23
26
  - Fields you will see on an order
24
27
  - What is still missing
@@ -137,9 +140,91 @@ settled on chain, `paid_out_at` is never stamped, the withheld deliverable is ne
137
140
  and the undo is already closed by the approval. The buyer pays, the vendor works, and the escrow
138
141
  is stranded with no exit. $0.02 is the smallest deposit that leaves the vendor $0.01.
139
142
 
143
+ **`list_price: 0` is the one value below that floor which is allowed, and it is a different shop
144
+ rather than a cheaper one.** See [Free shops](#free-shops-list_price-0). Anything strictly between
145
+ `0` and `$0.02` is refused.
146
+
140
147
  `PATCH /gigs/:id` moves `list_price` and refuses `price` (`409`, `reason: "inbound_order"`).
141
148
  Repricing the shop **does not touch orders already funded** — their amounts were fixed by the
142
- money that was actually sent, and are bounded on chain by it.
149
+ money that was actually sent, and are bounded on chain by it. A PATCH that would cross the
150
+ free/paid line in either direction is refused with `409 list_price_mode_change`.
151
+
152
+ ## Free shops (`list_price: 0`)
153
+
154
+ A free order machine takes orders and **touches no contract at all**. No deposit, no Treasury, no
155
+ `DEPOSIT_ID_SECRET`, no gas, and the buyer needs no wallet.
156
+
157
+ That is the point of it rather than a side effect: a paid shop needs a Treasury carrying the
158
+ escrow surface, and a deployment whose Treasury has been drained cannot run one. A free shop runs
159
+ anywhere.
160
+
161
+ Everything the trap above describes is a property of escrow. A free order has no deposit, so
162
+ there is no escrow to strand, no rollup to settle, no `paid_out_at` to lose and no undo to close.
163
+
164
+ **Open one** exactly as above, with `list_price: 0`.
165
+
166
+ **Place an order** with the same two calls as a paid one — draft through the webhook, then
167
+ `POST /gigs/:id/tasks/:msgId/publish` — with **no body**. An `amount` other than `0` is refused
168
+ (`409`, `reason: "free_order"`), never silently dropped, so a buyer is never told they tipped when
169
+ they did not. The response carries `free: true`, `price: 0`, and no `deposit_id` or `tx_hash`.
170
+
171
+ What changes downstream:
172
+
173
+ | | Paid shop | Free shop |
174
+ |---|---|---|
175
+ | Marker on the sent order | `deposit_id` | `order_free: true` |
176
+ | Ledger row `state` | `pending` → `open` → `settled` | `free` (terminal) |
177
+ | Releases the withheld `private_note` | `paid_out_at`, after the payout | the buyer's **approval** |
178
+ | Rollups | one per settled order | none, ever (`409 free_order`) |
179
+ | Approval | revisable until a rollup carries it | **final** (`409 free_order_approved`) |
180
+ | Undo | returns the deposit on chain | closes the order, returns nothing |
181
+ | Closing the shop | allowed once nothing is unsettled | always allowed |
182
+
183
+ The approval being final is the one asymmetry worth reading twice. On a paid order the payout
184
+ stamp is what closes the verdict; a free order never gets one, and approval is what *releases the
185
+ deliverable* — so if approval stayed revisable, a buyer could read a withheld licence key and then
186
+ withdraw the verdict that unlocked it, repeatedly. Rejections stay revisable in both modes,
187
+ because a rejection releases nothing.
188
+
189
+ A free cancel writes **no reputation event**. Those are keyed on the depositor's wallet address,
190
+ and a free order has no depositor.
191
+
192
+ **A free shop can always be closed** — `PATCH /gigs/:id {"status":"closed"}`, or `DELETE /gigs/:id`,
193
+ which writes the same field. It has no deposits and no sweep.
194
+
195
+ Closing stops **new** orders only (`409`, "This shop is not open"). Orders already in flight run to
196
+ their end: auto-approval is a query over proof rows and never reads a gig, and delivery, the
197
+ verdict and the undo have no gig-status check either. Reopen with `{"status":"active"}`.
198
+
199
+ ## Closing a PAID order machine
200
+
201
+ Allowed too, but only while no order is unsettled. Closing takes a gig out of `scanActiveGigs()`,
202
+ and two cron sweeps read that scan: `processApprovedProofs` (the payout backstop for a buyer who
203
+ approved and never pressed the button) and `reconcilePendingDeposits`. So the refusal asks about
204
+ the money, not about the mode:
205
+
206
+ | Deposit state | Blocks the close? | Why |
207
+ |---|---|---|
208
+ | `pending` | **yes** | money may be on chain with nothing recorded; only the reconciler resolves it |
209
+ | `open` | **yes** | in escrow, and the review timeout can still auto-approve it into a payout |
210
+ | `settled` | no | the chain already moved it |
211
+ | `withdrawn` | no | the buyer has it back |
212
+ | `free` | no | there never was any |
213
+
214
+ An **abandoned** row never blocks, whatever its state — the id was given up on and the salt
215
+ stepped. A ledger read that fails refuses the close (`409 ledger_unreadable`): "we could not tell"
216
+ is not "there is nothing there".
217
+
218
+ The refusal names the offending deposits so you know what to chase:
219
+
220
+ ```json
221
+ { "error": "This shop cannot be closed while 2 orders are unsettled. …",
222
+ "reason": "unsettled_orders", "unsettled": 2,
223
+ "deposits": [ { "deposit_id": "0x…", "state": "open", "amount": 0.5, "task_id": "TASK_…" } ] }
224
+ ```
225
+
226
+ An `open` deposit clears by the buyer approving the delivery (or undoing the order); a `pending`
227
+ one clears when the reconciler catches up on the next cron run.
143
228
 
144
229
  ## Fee-INCLUSIVE pricing: what the deposit actually buys
145
230
 
@@ -252,7 +337,9 @@ outright (`403`) and refuse everybody once the draft is funded (`409`): at that
252
337
 
253
338
  ```json
254
339
  POST /gigs/:id/tasks/:msgId/publish
255
- { "amount": 0.50, "wallet_alias_id": "..." }
340
+ { "amount": 0.50, "wallet_alias_id": "...",
341
+ "order_webhook_url": "https://my-app.example.com/callback", // optional, per order
342
+ "order_webhook_secret": "a-key-i-chose" } // optional, signs it
256
343
 
257
344
  → { "status": "assigned", "message_id": "TASK_01KX...", "published": true,
258
345
  "deposit_id": "0x…", "tx_hash": "0x…",
@@ -497,6 +584,99 @@ work the same way. Read the role note at the top of this page before you render
497
584
  buyer's comments are stored `client` and the vendor's are stored `gigworker`, which is the
498
585
  inverse of what "owns the gig" would tell you.
499
586
 
587
+ ## Webhooks: run the shop without polling
588
+
589
+ Both sides of an order machine can be driven by events. Full field-by-field reference, including
590
+ the signature scheme, is in [gigs.md](https://dollarplatoon.com/skill/gigs.md); this section is
591
+ the order-specific half.
592
+
593
+ **The direction rule.** `proof_webhook_url` on the gig belongs to the **gig owner**, who here is
594
+ the vendor. It is the wrong channel for a buyer, and pointing it at one would tell a seller about
595
+ their own delivery. The buyer's channel is on the **order**, and the vendor's is on their
596
+ **mailbox**.
597
+
598
+ ### Vendor: be told when an order arrives
599
+
600
+ Opening a shop creates your own mailbox (`vendor_mailbox_id` in the create response) with no
601
+ webhook on it. One PATCH turns the shop on:
602
+
603
+ ```bash
604
+ curl -X PATCH "https://dollarplatoon.com/api/gigs/$GIG_ID/mailboxes/$VENDOR_MAILBOX_ID" \
605
+ -H "x-api-key: $VENDOR_KEY" -H "Content-Type: application/json" \
606
+ -d '{ "webhook": "https://my-shop.example.com/orders",
607
+ "events_webhook_url": "https://my-shop.example.com/order-events" }'
608
+ → { "success": true, "webhook_secret": "whsec_..." }
609
+ ```
610
+
611
+ Every funded order now POSTs to `/orders` as `task.assigned`, carrying `message_id` — the order
612
+ you deliver a proof against. Verdicts (`proof.approved`, `proof.rejected`), payouts
613
+ (`payout.paid`) and cancellations (`order.withdrawn`) arrive on `/order-events`.
614
+
615
+ ### Buyer: be told what happens to your order
616
+
617
+ Name a callback in the **publish** call — the one that funds the order. It is per order, because
618
+ you may hold more than one mailbox in a shop and "your webhook" would otherwise be ambiguous.
619
+
620
+ ```bash
621
+ curl -X POST "https://dollarplatoon.com/api/gigs/$GIG_ID/tasks/$DRAFT_ID/publish" \
622
+ -H "x-api-key: $BUYER_KEY" -H "Content-Type: application/json" \
623
+ -d '{ "amount": 5.00,
624
+ "order_webhook_url": "https://my-app.example.com/orders/callback",
625
+ "order_webhook_secret": "a-key-i-chose" }'
626
+ ```
627
+
628
+ `order_webhook_secret` is **yours to pick**, unlike every other secret on the platform, and it
629
+ signs every `order.*` event for this order. Omit it and the deliveries go out unsigned. Both
630
+ fields work identically on a free shop.
631
+
632
+ ### The `order.*` payloads
633
+
634
+ `order.delivered` — the vendor submitted work. Review it, or let the clock do it for you.
635
+
636
+ ```json
637
+ { "event": "order.delivered", "sent_at": "...", "gig_id": "GIG_01HX...",
638
+ "gig_title": "Thumbnail shop", "task_id": "MSG_01HX...", "proof_id": "PRF_01HX...",
639
+ "price": 4.54, "deliverable_locked": true, "resubmitted": false }
640
+ ```
641
+
642
+ `deliverable_locked: true` means the vendor withheld the file itself in `private_note`.
643
+ Approving does **not** open it on a paid shop — payment does. Wait for `order.paid_out`.
644
+
645
+ `order.auto_approved` — your review window ran out and the clock ruled for the vendor.
646
+
647
+ ```json
648
+ { "event": "order.auto_approved", "sent_at": "...", "gig_id": "GIG_01HX...",
649
+ "task_id": "MSG_01HX...", "proof_id": "PRF_01HX...", "price": 4.54, "reportable": true }
650
+ ```
651
+
652
+ `reportable: true` is your remedy, and it is the only event that carries it: `POST
653
+ /gigs/:id/proofs/:proof_id/report` takes the delivery out of the payable set — but only until a
654
+ rollup carries it.
655
+
656
+ `order.paid_out` — the vendor was paid, and a withheld deliverable is now readable.
657
+
658
+ ```json
659
+ { "event": "order.paid_out", "sent_at": "...", "gig_id": "GIG_01HX...",
660
+ "task_id": "MSG_01HX...", "proof_id": "PRF_01HX...", "paid_to_vendor": 4.54,
661
+ "tx_hash": "0x...", "deliverable_unlocked": true }
662
+ ```
663
+
664
+ On `deliverable_unlocked: true`, fetch `GET /gigs/:id/proofs/:proof_id` to read the note. A free
665
+ shop never sends this event — nothing settles, and approval is what releases the note there.
666
+
667
+ `order.withdrawn` — the order was cancelled and the money went back. **The one event both sides
668
+ receive.** Read `withdrawn_by` before you react, or you will handle your own cancellation as if
669
+ the other party had sent it.
670
+
671
+ ```json
672
+ { "event": "order.withdrawn", "sent_at": "...", "gig_id": "GIG_01HX...",
673
+ "task_id": "MSG_01HX...", "withdrawn_by": "participant", "withdrawn_at": "..." }
674
+ ```
675
+
676
+ `withdrawn_by` is `"participant"` (the buyer cancelled), `"vendor"` (the shop declined the job),
677
+ or `"reconciler"` (a publish that never completed, cleaned up by the cron). The vendor's copy
678
+ also carries `mailbox_id`.
679
+
500
680
  ## What this mode refuses, and why a `409` is an answer
501
681
 
502
682
  Roughly twenty owner-only levers are closed on an order machine. Every one of them answers
@@ -543,6 +723,10 @@ order's value is not readable by everyone who joined.
543
723
  | `accepted_at` | When the vendor took delivery. There is no claim step in this mode. |
544
724
  | `order_withdrawn_at` | Set on an undone order. Absent everywhere else. |
545
725
 
726
+ `order_webhook_url` and `order_webhook_secret` are **write-only**. They are stored on the order
727
+ and no route ever reads them back — not to the vendor, and not to the buyer who set them. Keep
728
+ your own record of what you registered.
729
+
546
730
  On `GET /orders`, `state` is one of `pending | open | settled | withdrawn`, `vendor_payout` is
547
731
  derived from the deposit's own `fee_bps`, and `tip_estimate` is explicitly nullable and
548
732
  best-effort — it is measured against the shop's price **today**. The exact figure is on the task
@@ -557,9 +741,11 @@ lands somewhere unrelated.
557
741
 
558
742
  Honest gaps, so you do not design around something that is not there.
559
743
 
560
- - **No production shops.** `inbound_order` runs on **staging only**. Production has not had its
561
- Treasury migration and its `DEPOSIT_ID_SECRET` is unset, so a publish there would `500` by
562
- design. See [staging.md](https://dollarplatoon.com/skill/staging.md).
744
+ - **On production, only gigs created since 2026-08-29 can be shops.** `inbound_order` shipped to
745
+ production that day on a new Treasury. Gigs older than that are on the retired contract, which
746
+ has no escrow in its bytecode, and a publish-with-deposit there is refused `contract_no_escrow`.
747
+ This cannot be configured away and escrow cannot be moved between contracts — create a new gig.
748
+ See [staging.md](https://dollarplatoon.com/skill/staging.md).
563
749
  - **No route lists a gig's deposits.** A vendor cannot enumerate who owes what; they see the
564
750
  aggregate `escrowed_funds` and their own order inbox.
565
751
  - **No vendor display name on the gig read.** `GET /gigs/:id` gives `owner_wallet_alias` only, so
@@ -44,9 +44,9 @@ curl -H "x-api-key: $DOLLAR_PLATOON_API_KEY" https://dollarplatoon.com/api/auth/
44
44
  ```
45
45
 
46
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).
47
+ its own testnet USDC on Base Sepolia. Use it for anything you would not want to pay for. Every
48
+ path in this skill is identical there; substitute the host.
49
+ See [staging.md](https://dollarplatoon.com/skill/staging.md).
50
50
 
51
51
  ## Identifier format
52
52
 
@@ -25,14 +25,16 @@ be wrong with.
25
25
  | API | `https://staging.dollarplatoon.com/api` | `https://dollarplatoon.com/api` |
26
26
  | Chain | **Base Sepolia** (chain id `84532`) | Base mainnet (chain id `8453`) |
27
27
  | USDC | **MockUSDC** `0xE4E5…c6a8` — worthless by design | Real USDC `0x8335…2913` |
28
- | Treasury | `0x932B9D4CA0e11D7859C43F7e58492F2C6206D485` | `0xE4E5…c6a8` |
28
+ | Treasury | `0x932B9D4CA0e11D7859C43F7e58492F2C6206D485` | `0x42118E4F0c0E326c5c09d2aC5d764957025214A9` |
29
29
  | Accounts | Entirely separate. Your production key does **not** work here. | |
30
- | `inbound_order` | **Available** | Not deployed — see below |
30
+ | `inbound_order` | **Available** | **Available** |
31
31
  | Inbound email | `…@fwd.zoomgtm.com`, with a `staging.` infix in the address | same domain, no infix |
32
32
 
33
- The two Treasury addresses are not a copy-paste error, and neither is production's Treasury
34
- matching staging's MockUSDC: they are the same deployer account at the same nonce on two
35
- different chains. Nothing is shared between them.
33
+ Staging's MockUSDC address is `0xE4E5…c6a8`, which was also production's Treasury address until
34
+ 2026-08-29. That was never a copy-paste error: it is the same deployer account at the same nonce
35
+ on two different chains. Nothing is shared between them. Production has since moved to
36
+ `0x42118E4F…14A9`, and `0xE4E5…c6a8` is retired there — so if you see that address in an older
37
+ document, check which chain and which role is meant.
36
38
 
37
39
  Everything in the rest of this skill is written with production URLs. To read it as a staging
38
40
  integrator, substitute the host — every path is identical. `POST /gigs` means
@@ -109,29 +111,35 @@ history. Compose it with `?hide_navbar=true&hide_logo=true` for an embed. See
109
111
 
110
112
  ## What staging has that production does not
111
113
 
112
- **`inbound_order` the order machine mode runs on staging only.** A full order has been taken
113
- end to end there on Base Sepolia: deposit, deliver, approve, settle, reveal. Production has none
114
- of it, and would refuse: its Treasury migration has not run and its deposit-id secret is unset,
115
- so a publish-with-deposit would `500` by design rather than take money it could not settle.
114
+ **Money that costs nothing to be wrong with.** That is now the main difference. `inbound_order`
115
+ shipped to production on 2026-08-29, so both stages carry it.
116
116
 
117
- If you are building against `inbound_order`, staging is not a rehearsal it is the only place it
118
- exists. See [orders.md](https://dollarplatoon.com/skill/orders.md).
117
+ One thing to know if you read older notes: production runs **two Treasuries at once**. Gigs
118
+ created before 2026-08-29 live on the retired `0xE4E5…c6a8` and can never do escrow — that
119
+ contract predates the feature, and an order against one of those gigs is refused
120
+ `contract_no_escrow`. Gigs created since then are on `0x42118E4F…14A9` and have the full set. A
121
+ gig keeps the Treasury it was born on for life, so this is a property of the GIG, not of the
122
+ stage. `GET /gigs/:id` tells you which one you are on.
123
+
124
+ See [orders.md](https://dollarplatoon.com/skill/orders.md).
119
125
 
120
126
  ## What is NOT production-ready — read before you ship
121
127
 
122
128
  Stated plainly, because the difference between the stages is not only "one has fake money".
123
129
 
124
- - **The wallet encryption key rotation has not run on either stage.** Hot wallet private keys
125
- are encrypted with a key that is the published development default. On production that is 782
126
- real wallets. The dual-key read path that makes rotation possible is deployed to staging only.
130
+ - **The wallet encryption key rotation has not run on production.** Hot wallet private keys there
131
+ are encrypted with a key that is the published development default 782 real wallets. The
132
+ dual-key read path that makes rotation possible is now deployed to both stages, but the rotation
133
+ itself has not been done on production. Staging has been rotated.
127
134
  **Do not treat a Dollar Platoon hot wallet as cold storage on either stage.** Withdraw earnings
128
135
  to a wallet you control (`POST /wallets/:alias_id/transfer`).
129
- - **The production Treasury migration has not happened.** Production still runs the old contract,
130
- which has no per-task deposits, no undo, and no reserved-balance guard. Every `inbound_order`
131
- feature depends on the new one.
132
- - **The maintenance flag does not exist.** There is no way to quiesce the money routes on either
133
- stage, so a platform migration cannot cleanly stop the world. This is the platform's problem,
134
- not yours, but it means a production cutover is not a background event.
136
+ - **Gigs created before 2026-08-29 cannot do escrow, on production.** They live on the retired
137
+ Treasury, which has no per-task deposits, no undo and no reserved-balance guard. This is
138
+ permanent for those gigs — escrow cannot be migrated between contracts. Create a new gig.
139
+ - **Those older production gigs settle by hand, not on chain.** Their Treasury was drained on
140
+ 2026-08-29, so they mint no payouts and refuse deposits; approved work there is paid directly to
141
+ worker wallets. `POST /gigs/:id/rollups` on one answers `409 settled_offchain`. New gigs are
142
+ unaffected and settle on chain as normal.
135
143
  - **`GET /public/read-url` signs any S3 key for any share-token holder.** Unfixed on both stages.
136
144
  Anything offloaded to S3 — a large task body, an uploaded file — should be treated as readable
137
145
  by anyone holding any share token on the platform. Keep secrets in `private_details` and
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: vidfarm
3
- skill_version: 0.21.64
3
+ skill_version: 0.21.65
4
4
  description: Use Vidfarm as a director. Run a strategy **consultation** (the `brainstorm/*` chain — cold-start interview, awareness stages, persuasive angles, hooks, product placement). Answer "give me content ideas" / "what should I post" / "I need 30 videos this month" from the bundled idea banks (50 content frames x 5 awareness stages x 44 problem angles). Browse/add inspiration videos, browse the free public raws catalog BY CATEGORY (curated shelves like scroll-stoppers/greenscreen/reaction — the cheapest way to source footage for one video, and a ready-made clip pool for bulk scripting N variants), fork a template into a composition, edit it in the Trackpad Editor (timeline-based like Premiere/DaVinci), auto-decompose source video into scenes, render to MP4, approve into a shareable post, and schedule it. Includes login, provider keys, discovery, versioning, uploads/downloads, and billing. Every step is available as raw REST; `vidfarm-devcli` wraps those routes and composes the file-backed scripting flows.
5
5
  ---
6
6
 
package/SKILL.director.md CHANGED
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: vidfarm
3
- skill_version: 0.21.64
3
+ skill_version: 0.21.65
4
4
  description: Use Vidfarm as a director. Run a strategy **consultation** (the `brainstorm/*` chain — cold-start interview, awareness stages, persuasive angles, hooks, product placement). Answer "give me content ideas" / "what should I post" / "I need 30 videos this month" from the bundled idea banks (50 content frames x 5 awareness stages x 44 problem angles). Browse/add inspiration videos, browse the free public raws catalog BY CATEGORY (curated shelves like scroll-stoppers/greenscreen/reaction — the cheapest way to source footage for one video, and a ready-made clip pool for bulk scripting N variants), fork a template into a composition, edit it in the Trackpad Editor (timeline-based like Premiere/DaVinci), auto-decompose source video into scenes, render to MP4, approve into a shareable post, and schedule it. Includes login, provider keys, discovery, versioning, uploads/downloads, and billing. Every step is available as raw REST; `vidfarm-devcli` wraps those routes and composes the file-backed scripting flows.
5
5
  ---
6
6
 
package/SKILL.md CHANGED
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: vidfarm
3
- skill_version: 0.21.64
3
+ skill_version: 0.21.65
4
4
  description: Entry point for making, editing, and rendering short-form video with Vidfarm. Use when the task is to create a video, ad, explainer, or social clip; to edit or re-theme an existing composition; to source footage or audio; or to run a batch of video variants. Routes to the full director skill and the `vidfarm` CLI.
5
5
  ---
6
6
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@officexapp/vidfarm-devcli",
3
- "version": "0.21.64",
3
+ "version": "0.21.65",
4
4
  "description": "Local bridge for the Vidfarm Trackpad Editor. `vidfarm serve <template_id>` boots the FULL editor on localhost (disk-backed records/storage, free in-process render); edit composition.html on disk (Claude Code, Codex, etc.) and the browser live-morphs it.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -161,6 +161,8 @@
161
161
  "test:order-decision": "node --import tsx --test test/order-decision.test.ts",
162
162
  "test:reviews": "node --import tsx --test test/shop-reviews.test.ts",
163
163
  "test:marketplace-api": "node --import tsx --test test/marketplace-api.test.ts",
164
+ "test:marketplace-regressions": "node --import tsx --test test/marketplace-prod-regressions.test.ts",
165
+ "test:new-shop-free-order": "node --import tsx --test test/new-shop-free-order.test.ts",
164
166
  "test:notifications": "node --import tsx --test test/marketplace-notifications.test.ts",
165
167
  "test:delivery": "node --import tsx --test test/delivery-items.test.ts",
166
168
  "test:dporders": "node --import tsx --test test/dp-order-fee.test.ts",