@officexapp/vidfarm-devcli 0.21.61 → 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 (29) 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 +28 -3
  29. package/public/assets/homepage-client-app.js +13 -13
@@ -0,0 +1,573 @@
1
+ # Order machines (`inbound_order`) — the shop that does the work
2
+
3
+ One distribution mode inverts every assumption the rest of this skill makes. Read this page
4
+ before you build anything against a gig whose `distribution` is `inbound_order`, because the
5
+ sentence "the gig owner sends the work and pays for it" — true on every other page — is false
6
+ here in both halves.
7
+
8
+ ## Contents
9
+
10
+ - The inversion, in one table
11
+ - Routes
12
+ - Open a shop — the vendor's side
13
+ - `list_price`, and why `gig.price` is pinned to 0
14
+ - Fee-INCLUSIVE pricing: what the deposit actually buys
15
+ - The order lifecycle
16
+ - Place an order — the participant's side, end to end
17
+ - Hand the buyer a payment link — two calls, one URL
18
+ - Undo: either party, until approval
19
+ - `private_note` is the vendor's only protection
20
+ - Approve, then settle, then reveal
21
+ - Report and comments both work in both directions
22
+ - What this mode refuses, and why a `409` is an answer
23
+ - Fields you will see on an order
24
+ - What is still missing
25
+
26
+ ---
27
+
28
+ ## The inversion, in one table
29
+
30
+ | | Every other mode | `inbound_order` |
31
+ |---|---|---|
32
+ | Sends the task | the gig owner | an outside **participant** |
33
+ | Does the work | a mailbox holder | the **vendor** — who *is* the gig owner |
34
+ | Approves | the gig owner | **the participant, and nobody else** |
35
+ | Funds it | the gig owner, once, into a shared pot | the participant, **per task**, at publish |
36
+ | Fee | 10% charged **on top** of the payout | 10% taken **out of** the deposit |
37
+ | Gets paid | the mailbox wallet | the **vendor's** mailbox wallet |
38
+ | Can undo the money | nobody — deposits are one-way | **either party**, until approval |
39
+ | Triggers the payout | the owner, plus the daily cron | **either party**, plus the cron |
40
+
41
+ The words matter, so this page uses them consistently. **Vendor** = the gig owner, who fills
42
+ orders. **Participant** = the member who places and pays for one. The stored role tokens are
43
+ still `client` and `gigworker` — they are never migrated — but their *meaning* is fixed on the
44
+ economics, not on the deed:
45
+
46
+ ```
47
+ "client" the paying, approving side outbound: the gig owner
48
+ order: the participant
49
+ "gigworker" the side doing the work outbound: the mailbox holder
50
+ order: the vendor (the gig owner)
51
+ ```
52
+
53
+ If your integration maps those tokens to labels, map them through the gig's `distribution` or
54
+ you will render every byline backwards.
55
+
56
+ **Membership is the whole credential.** A participant reaches a shop by holding an **active**
57
+ mailbox in it — the vendor's invite link is what buys that. There is no browse-all and no public
58
+ shopfront; `GET /gigs` still answers `410`. A `pending_approval` mailbox cannot order.
59
+
60
+ ## Routes
61
+
62
+ Nothing below is a new path except `GET /orders` and `.../withdraw`. The rest are routes you
63
+ already know that **change hands** in this mode.
64
+
65
+ | Method | Path | Auth here | Description |
66
+ |--------|------|-----------|-------------|
67
+ | POST | `/gigs` | Yes | Open a shop: `distribution: "inbound_order"`, `list_price`, no `price` |
68
+ | GET | `/gigs/:id` | Optional | Adds `list_price`, `fee_bps`, and `escrowed_funds` (owner + members) |
69
+ | PATCH | `/gigs/:id` | Vendor | `list_price` moves; `price` does not |
70
+ | POST | `/inbound/webhook/:gig_id?token=…&draft=true` | Token **+ participant session** | Save a draft order |
71
+ | GET | `/gigs/:id/drafts` | Vendor **or participant** | Your own unsent orders |
72
+ | PATCH | `/gigs/:id/tasks/:msgId/payload` | **Participant** | Rewrite an unfunded draft's body |
73
+ | PATCH | `/gigs/:id/tasks/:msgId/private-details` | **Participant** | The private half of the brief |
74
+ | POST | `/gigs/:id/tasks/:msgId/publish` | **Participant** | **Deposit and send.** `{ amount, wallet_alias_id }` |
75
+ | GET | `/orders?state=&gig_id=` | Yes | Your deposit ledger across every shop |
76
+ | GET | `/gigs/:id/proofs` | Vendor, submitter **or participant** | The delivery |
77
+ | GET | `/gigs/:id/proofs/:proof_id` | Vendor, submitter **or participant** | One delivery; poll it for the released note |
78
+ | PATCH | `/gigs/:id/proofs/:proof_id` | **Participant** | Approve or reject |
79
+ | POST | `/gigs/:id/proofs/:proof_id/report` | **Either party** | Dispute a `timeout_approved` delivery |
80
+ | POST | `/gigs/:id/rollups` | Vendor **or participant** | Settle. A participant settles only their own orders |
81
+ | POST | `/gigs/:id/tasks/:msgId/withdraw` | **Either party** | Undo the order and return the deposit |
82
+
83
+ `GET /orders` is account-wide and not scoped to a gig — it is the buyer's own ledger, read from
84
+ their own partition. Filter with `?state=pending|open|settled|withdrawn` and `?gig_id=`.
85
+
86
+ ## Open a shop — the vendor's side
87
+
88
+ ```json
89
+ POST /gigs
90
+ {
91
+ "title": "Haiku on demand",
92
+ "terms": "One haiku, any subject, delivered within a day.",
93
+ "distribution": "inbound_order",
94
+ "list_price": 0.50,
95
+ "review_timeout": 86400
96
+ }
97
+ → { "gig": { "id": "GIG_01HX...", "list_price": 0.5, "price": 0, ... },
98
+ "vendor_mailbox_id": "MBX_01HX..." }
99
+ ```
100
+
101
+ Four things differ from every other `POST /gigs`:
102
+
103
+ - **`price` is not asked for and is pinned to 0.** Do not send one.
104
+ - **`list_price` is required**, and is the shopfront number.
105
+ - **`review_timeout: -1` is refused, and so is anything under 3600 seconds.** `-1` means manual
106
+ review with no auto-approval, which on an outbound gig is the owner's own risk to take. Here
107
+ the reviewer is the buyer and the payee is the vendor, so no auto-approval means a participant
108
+ could simply never rule and the vendor would never be paid. The one-hour floor closes the
109
+ opposite abuse — a window so short it approves the vendor's own submission before the buyer
110
+ can read it.
111
+ - **You get a mailbox in your own shop.** `vendor_mailbox_id` is where orders land and where you
112
+ submit deliveries from. You are the owner *and* the worker; that is the mode.
113
+
114
+ Then hand out two things: an **invite link** (`POST /gigs/:id/invites`) so a buyer can join, and
115
+ the gig's **webhook URL with its security token**, which is the door their draft order goes
116
+ through. `POST /gigs/:id/rotate-token` closes that door for every participant at once, so in
117
+ this mode it demands `{ "confirm": true }` and answers `409 requires_confirmation` without it.
118
+
119
+ **Do not fund the gig.** `POST /gigs/:id/deposit` answers `409` here, correctly: it credits the
120
+ shared pot, and money in the pot is unreachable by the deposit-naming payout this mode uses.
121
+ The Treasury's reserved-balance guard would refuse to release it. Your buyers fund each order.
122
+
123
+ ## `list_price`, and why `gig.price` is pinned to 0
124
+
125
+ `gig.price` is the fallback the whole codebase reaches for whenever a task price cannot be
126
+ resolved — `resolveTaskPrice`, the review timeout, the rollup's unpriced-proof path. In this
127
+ mode every one of those fallbacks is **wrong by construction**, because the only correct number
128
+ comes from the deposit. Pinning it to 0 makes each of them pay nothing rather than pay a figure
129
+ nobody agreed to, and the funding guards below make sure that zero is never reachable on a real
130
+ order.
131
+
132
+ `list_price` has a hard floor of **$0.02**, and the reason is arithmetic rather than taste. The
133
+ vendor receives the deposit floored to whole cents *after* the fee, and `floorToCent($0.01)` is
134
+ `$0.00`. A one-cent order would publish as a real, funded, open deposit whose payout is zero —
135
+ and a `$0` rollup is written straight to `paid` with **no transaction**, so the deposit is never
136
+ settled on chain, `paid_out_at` is never stamped, the withheld deliverable is never released,
137
+ and the undo is already closed by the approval. The buyer pays, the vendor works, and the escrow
138
+ is stranded with no exit. $0.02 is the smallest deposit that leaves the vendor $0.01.
139
+
140
+ `PATCH /gigs/:id` moves `list_price` and refuses `price` (`409`, `reason: "inbound_order"`).
141
+ 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.
143
+
144
+ ## Fee-INCLUSIVE pricing: what the deposit actually buys
145
+
146
+ This is the single most common place to get the arithmetic wrong, because it is the opposite of
147
+ [payouts.md](https://dollarplatoon.com/skill/payouts.md).
148
+
149
+ > **Outbound:** the worker receives `P` and the gig pays `P × 1.1`. Budget 110%.
150
+ > **Order:** the buyer deposits `D` and the vendor receives `floorToCent(D ÷ 1.1)`. The fee comes
151
+ > out of the deposit. A vending machine charges what the sticker says.
152
+
153
+ ```
154
+ D = $0.50 → vendor receives $0.45 (fee $0.05, residue 0.5¢)
155
+ D = $3.30 → vendor receives $3.00
156
+ D = $5.00 → vendor receives $4.54 (NOT $4.55 — see below)
157
+ ```
158
+
159
+ **Three rules if you compute this yourself.**
160
+
161
+ 1. **Floor, never round.** `$5.00 ÷ 1.1` is `$4.5454…`. Rounding to `$4.55` overdraws:
162
+ `4.55 × 1.1 = $5.005`, which is more than the deposit, and the payout reverts permanently
163
+ with the money already taken. Flooring to `$4.54` leaves 0.6¢ of residue in the pot as
164
+ unreserved float, which is the intended outcome.
165
+ 2. **Work in integer cents.** `Math.floor(3.30 / 1.1 * 100) / 100` is **2.99**, because
166
+ `3.3/1.1` is `2.9999999999999996` in IEEE-754. That shorts the vendor a cent on exactly the
167
+ round numbers a buyer is most likely to type — $3.30, $6.60, $12.10. The integer form gives
168
+ 3.00, 6.00, 11.00.
169
+ 3. **Never hardcode the `1.1`.** The Treasury's fee rate is admin-mutable up to 5000 bps. Read
170
+ it from `fee_bps` on `GET /gigs/:id` and derive the divisor as `1 + fee_bps/10000`. The key
171
+ is present on every order machine including when its value is `null` — `null` means *"the
172
+ Treasury could not be read"*, which is a different statement from *"this shop charges no
173
+ fee"*. **Treat `null` as unknown and say so in your UI. Do not substitute 10%.** Every
174
+ *settled* path uses the rate snapshotted onto the deposit, so only the pre-purchase quote is
175
+ at risk; a `setFee` would otherwise make every quote silently wrong.
176
+
177
+ **Anything above `list_price` is a tip, and it goes to the vendor too.** Deposit more than the
178
+ sticker and the whole surplus flows through the same `D ÷ 1.1`. The tip is
179
+ `funded_total − funded_minimum` — derived, never stored. `funded_minimum` exists for exactly this
180
+ reason: it snapshots the price *at publish*, so the tip stays correct after the vendor reprices
181
+ the shop. Subtracting today's `list_price` instead gives a wrong answer, and a negative one when
182
+ the price went up.
183
+
184
+ Deposits must be a **whole number of cents**. A fractional cent inflates the residue past the
185
+ contract's bound and the payout reverts forever, so it is refused before any money moves.
186
+
187
+ `GET /gigs/:id` also carries **`escrowed_funds`** for the owner and members. On an order machine
188
+ `available_funds` is synced raw from the on-chain pot, and that pot holds every open deposit —
189
+ so without the split a vendor's dashboard shows their customers' escrow as their own takings.
190
+ `escrowed_funds` is a display mirror and never an authority; every decision reads the chain.
191
+ `null` means "not synced yet", which is not the same as `0`.
192
+
193
+ ## The order lifecycle
194
+
195
+ ```
196
+ draft saved through the webhook (?draft=true), names the participant, no money
197
+
198
+ └─ publish ────▶ DEPOSIT (state: pending → open) ── the deposit and the send are ONE call
199
+
200
+ ├─ either party undoes ──────────▶ withdrawn (terminal, money returned)
201
+
202
+ └─ vendor delivers a proof
203
+
204
+ └─ participant approves ───▶ UNDO CLOSES FOR BOTH SIDES
205
+ │ (or review_timeout auto-approves)
206
+
207
+ └─ either party settles ─▶ settled (terminal)
208
+ (daily cron is the backstop)
209
+
210
+ └─ paid_out_at stamped ─▶ private_note revealed
211
+ ```
212
+
213
+ **Undo closes at approval, not at payment.** Say this to yourself once more, because it is the
214
+ one rule integrators guess wrong. If undo survived approval, a participant could approve, watch
215
+ the payout transaction fail, and withdraw — having already read the delivery. That is a free
216
+ option that needs no timing luck. Approval is the point of no return; payment is merely what
217
+ happens next.
218
+
219
+ ## Place an order — the participant's side, end to end
220
+
221
+ **1. Join the shop.** Open the vendor's invite link, or `POST /gigs/:id/mailboxes` with the
222
+ token. You need an **active** mailbox before anything below works.
223
+
224
+ **2. Save a draft.** The gig webhook, with `draft=true`, **and your `x-api-key` on the same
225
+ request**:
226
+
227
+ ```bash
228
+ curl -X POST "https://staging.dollarplatoon.com/api/inbound/webhook/GIG_01HX...?token=TOK&draft=true" \
229
+ -H "x-api-key: $KEY" \
230
+ -H "content-type: application/json" \
231
+ -d '{"order":"Please write me a haiku about escrow."}'
232
+ ```
233
+
234
+ The token alone is not enough here, and that is deliberate. The webhook's whole credential is a
235
+ **shared** secret; on an outbound gig that is right, because every task through it belongs to the
236
+ owner. Here many different buyers use the same door, and a draft with no identity is a row nobody
237
+ can approve, fund, undo, or even read back — every authority check in this mode is a comparison
238
+ against `client_user_id`. So the token still gates the door and your session says who you are.
239
+ Both are required: `401` with no session, `403 not_a_member` without an active mailbox.
240
+
241
+ On an order machine this door **only saves drafts**. `?draft=true` is mandatory (`409` without
242
+ it), and `?price=` and `?assign_to=` are refused rather than ignored — a price here would be a
243
+ number nobody paid, and an assignee would route a funded order past the vendor.
244
+
245
+ Read your drafts back with `GET /gigs/:id/drafts`; you get rows naming you and nothing else, and
246
+ so does the vendor for theirs. Edit an **unfunded** draft with
247
+ `PATCH /gigs/:id/tasks/:msgId/payload` and `.../private-details`. Both refuse the vendor
248
+ outright (`403`) and refuse everybody once the draft is funded (`409`): at that point the brief
249
+ *is* the record of what was bought.
250
+
251
+ **3. Publish, which is where you pay.**
252
+
253
+ ```json
254
+ POST /gigs/:id/tasks/:msgId/publish
255
+ { "amount": 0.50, "wallet_alias_id": "..." }
256
+
257
+ → { "status": "assigned", "message_id": "TASK_01KX...", "published": true,
258
+ "deposit_id": "0x…", "tx_hash": "0x…",
259
+ "funded_total": 0.5, "funded_minimum": 0.5, "tip": 0,
260
+ "price": 0.45, "fee_bps": 1000 }
261
+ ```
262
+
263
+ Publishing and funding are **one operation**, because an order becomes real by being paid for and
264
+ the price written on the delivered task is derived from the money that just moved. Split them and
265
+ there would be a window where the vendor holds a claimable task that nothing funds — priced, by
266
+ every fallback in the codebase, at `gig.price`, which is 0.
267
+
268
+ Delivery mints a **new task id**. `message_id` is the live order; `draft_id` was the draft, and it
269
+ is deleted. Use `message_id` from here on.
270
+
271
+ **4. Watch it.** `GET /orders` is your ledger. `GET /gigs/:id/proofs` shows the delivery when it
272
+ arrives.
273
+
274
+ **5. Rule on it.** `PATCH /gigs/:id/proofs/:proof_id` with `{ "action": "approve" }` or a reject
275
+ with a `rejection_tag`. **You are the only party who may.** The vendor is refused on their own
276
+ gig — they are the one being judged.
277
+
278
+ **6. Settle.** `POST /gigs/:id/rollups`. You may call it for **your own orders only**; the proof
279
+ set is narrowed to the tasks your deposits funded, so the button can never move somebody else's
280
+ money or reveal somebody else's work. The vendor keeps the unrestricted gig-wide trigger, and
281
+ the daily cron is the backstop for both.
282
+
283
+ ### The publish call answers honestly when it half-succeeds
284
+
285
+ A deposit passes an explicit gas limit, which skips estimation — so a reverting transaction still
286
+ reaches the mempool. Every failure path therefore **asks the chain** before concluding anything,
287
+ and the `reason` tells you which of these you are in:
288
+
289
+ | `reason` | Status | What to do |
290
+ |---|---|---|
291
+ | `insufficient_funds` | 402 | Fund the wallet. Nothing was sent. |
292
+ | `balance_unreadable` / `chain_unreadable` / `fee_rate_unreadable` | 503 | Retry shortly. Nothing was sent. |
293
+ | `deposit_id_squatted` | 409 | A fresh id is already prepared. **Publish again** — this is not an error state. |
294
+ | `deposit_failed` | 502 | The send itself failed. Read `GET /orders` before retrying. |
295
+ | `ledger_write_failed` | 409 | **Your money is safe on chain.** Publish again to finish, or undo it. |
296
+ | `residue_too_large` | 409 | The amount cannot settle. Nothing was sent. |
297
+ | `contract_mismatch` | 409 | The shop points at a different Treasury than this server. Not your problem to fix. |
298
+ | `deposit_unreconciled` / `state_mismatch` / `ledger_missing` | 409 | Stop. These need a human; nothing was sent twice. |
299
+
300
+ A response carrying `draft_kept: true` means your draft still exists and is still the only copy
301
+ of what you typed. A response carrying `funded: true` **and** `draft_kept: true` means the money
302
+ moved and the delivery did not — publish again to finish, or undo.
303
+
304
+ Retrying is safe. The deposit id is derived, the ledger row and the draft stamp are written with
305
+ conditional puts **before** any chain call, and every step after that lock is idempotent given
306
+ the same id. A double-click derives the same id twice and exactly one caller gets past.
307
+
308
+ ## Hand the buyer a payment link — two calls, one URL
309
+
310
+ You do not have to build the checkout. Write the order through the webhook, then send the buyer
311
+ `/fund/:gig_id/:task_id` — a standalone page that does **nothing but wallet management and the
312
+ deposit**. It is the funding half of the order form, on its own URL, and it is framable.
313
+
314
+ **Call 1 — save the draft.** Exactly the call above, with the gig token and the buyer's session:
315
+
316
+ ```bash
317
+ curl -X POST "https://staging.dollarplatoon.com/api/inbound/webhook/GIG_01HX...?token=TOK&draft=true" \
318
+ -H "x-api-key: $BUYERS_KEY" \
319
+ -H "content-type: application/json" \
320
+ -d '{"order":"Please write me a haiku about escrow."}'
321
+ → { "message_id": "TASK_01M..." }
322
+ ```
323
+
324
+ Optionally follow it with `PATCH /gigs/:id/tasks/:msgId/private-details` for anything the buyer
325
+ should not see in the visible order. Both calls are yours to make; the buyer makes neither.
326
+
327
+ **Call 2 — the buyer, on the page.** Send them the link and they press one button, which is
328
+ `POST /gigs/:gig_id/tasks/:task_id/publish` under the hood:
329
+
330
+ ```
331
+ https://staging.dollarplatoon.com/fund/GIG_01HX.../TASK_01M...?api_key=BUYERS_KEY
332
+ ```
333
+
334
+ ```html
335
+ <iframe src="https://staging.dollarplatoon.com/fund/GIG_01HX.../TASK_01M...?api_key=BUYERS_KEY&hide_navbar=true&hide_logo=true"
336
+ width="420" height="760" style="border:1px solid #e5e7eb;border-radius:12px"></iframe>
337
+ ```
338
+
339
+ **What the page shows, and nothing else:** the order text (unwrapped from its JSON envelope), the
340
+ shop and its `list_price`, the exact split — deposit, vendor payout, platform fee, tip — at the
341
+ live `fee_bps` when the gig read carries one and at the documented 10% assumption when it does not,
342
+ *saying which*; an amount field defaulting to `list_price` with anything above it labelled a tip;
343
+ the paying wallet with its USDC balance, its gas balance and its address to copy; and one button
344
+ that deposits and publishes.
345
+
346
+ **It has no navigation, no order form and no way to write anything.** The words were fixed by your
347
+ call; this page only pays for them.
348
+
349
+ ### The states it answers honestly
350
+
351
+ An embed has no address bar and no support page, so every dead end is a sentence rather than a
352
+ status code:
353
+
354
+ | What happened | What the buyer sees |
355
+ |---|---|
356
+ | Draft missing, or `404` because it names another account | "This order is not here" — the link is spent or was never theirs |
357
+ | Read refused `403` | "This order belongs to somebody else" |
358
+ | Already funded — including a reload after paying | The receipt, and **no button**. It never offers to pay twice |
359
+ | `order_withdrawn_at` set | "This order was cancelled", with what came back |
360
+ | Gig is not `inbound_order` | "This machine does not take orders" |
361
+ | Shop closed (`status !== "active"`) | "This shop is closed", nothing charged |
362
+ | No hot wallet / short of USDC / **no gas** | Each named separately. Gas is a wall, not a warning — the buyer signs their own deposit |
363
+ | A `reason` from the publish table above | The server's sentence, plus what to do: retry, add funds, or stop and ask a human |
364
+
365
+ `ledger_write_failed` and any response carrying `funded: true` put the page into its
366
+ funded-but-undelivered state: **Finish this order (already paid)**, or **Cancel and refund me**.
367
+ Neither charges twice.
368
+
369
+ ### Three things to know before you build on it
370
+
371
+ 1. **The link is single-use by construction.** Publishing mints a new task id and deletes the
372
+ draft, so the URL stops resolving to a draft the moment it is paid. A reload finds the deposit
373
+ in `GET /orders` and shows the receipt instead. Do not treat a working `/fund/` link as a
374
+ durable handle on an order — `/gigs/:gig_id/order/:task_id` is that.
375
+ 2. **Membership is still the credential.** A buyer needs an **active** mailbox in the shop before
376
+ the draft call works at all, so the first link you ever hand a stranger is the invite
377
+ (`POST /gigs/:id/invites`), never this one.
378
+ 3. **`?api_key=` grants the whole account.** It is what makes a pre-authenticated link possible,
379
+ and it is the buyer's own key — never yours, and never one you mint for them out of an admin
380
+ route. Without it the page shows a sign-in that returns to itself.
381
+
382
+ ### Which chain, and where to send gas
383
+
384
+ A buyer signs their own deposit, so they need native ETH on the **right** network — and "Base"
385
+ names two of them. Sending Sepolia ETH to a mainnet address loses it; the reverse loses real money.
386
+
387
+ `GET /gigs/:gig_id` carries the answer on an order machine, beside `fee_bps`, so a funding page
388
+ gets the split and the network in one call:
389
+
390
+ ```json
391
+ { "fee_bps": 1000,
392
+ "chain": { "chain_id": 84532, "chain_name": "Base Sepolia", "is_testnet": true } }
393
+ ```
394
+
395
+ `GET /api/health` carries the same block plus `treasury_address` and `usdc_address`, which is the
396
+ cheaper call when you only want to know which deployment you are pointed at.
397
+
398
+ The chain id is read from the node, never inferred from the stage — those two disagreed for two
399
+ days once, and production ran against a testnet. **`chain` is `null` when the node is
400
+ unreachable, and a client must render that as "unknown" rather than guessing.** Naming the wrong
401
+ network is worse than naming none: it tells somebody to send real money to a testnet address.
402
+
403
+ ## Undo: either party, until approval
404
+
405
+ ```json
406
+ POST /gigs/:id/tasks/:msgId/withdraw
407
+ → { "success": true, "state": "withdrawn", "tx_hash": "0x…", ... }
408
+ ```
409
+
410
+ `:msgId` accepts the **live task** or the **draft** that was stamped with a deposit id by a
411
+ publish that never finished. Both are real ways to end up holding an open deposit.
412
+
413
+ | `reason` | Meaning |
414
+ |---|---|
415
+ | `not_inbound_order` | Wrong mode. There is no per-task escrow on an outbound gig. |
416
+ | `not_a_party` | You are neither the buyer nor the vendor. |
417
+ | `not_funded` | Nothing was ever deposited against this order. |
418
+ | `already_approved` | The delivery was approved. **Undo is closed.** Payout is the next step. |
419
+ | `already_settled` | The vendor has been paid. |
420
+ | `never_funded` | Answered `200` — the deposit was never opened, so there is nothing to return. |
421
+ | `state_mismatch` / `ledger_missing` | Stop. Nothing was sent; these need a human. |
422
+
423
+ Calling it twice on an already-withdrawn deposit returns success rather than an error, because
424
+ the point of the route is the money's final position, not the caller's bookkeeping.
425
+
426
+ The refusal is tested on *payability*, not on the literal string `"approved"` —
427
+ `timeout_approved` is a distinct status, and a buyer must not be able to withdraw after
428
+ auto-approval while the cron's payout is in flight.
429
+
430
+ **An undone order keeps its draft.** That is deliberate: publishing it again funds a fresh
431
+ deposit under the next salt, so a buyer can re-send the same words without retyping them. The
432
+ order row itself stays too, closed rather than deleted — it is the buyer's record of what they
433
+ asked for and the vendor's record of what was pulled.
434
+
435
+ ## `private_note` is the vendor's only protection
436
+
437
+ **The participant reads `proofs[]` at review time, and can undo before approving.** That is the
438
+ whole shape of the risk, and it is designed in rather than overlooked: submit, read the evidence,
439
+ undo, walk away with the work.
440
+
441
+ So on an order machine:
442
+
443
+ > **Put the deliverable in `private_note`. Put evidence in `proofs[]`.**
444
+
445
+ `private_note` is released only when `paid_out_at` is stamped — money on chain, for that proof.
446
+ Approving does not open it. The buyer sees `private_note: null, private_note_locked: true` until
447
+ then, and polls `GET /gigs/:id/proofs/:proof_id` after the rollup settles. `proofs[]` should
448
+ carry a watermarked preview, a hash, a word count, a description — enough to rule on, not enough
449
+ to use. See [proofs.md](https://dollarplatoon.com/skill/proofs.md) for the full field contract.
450
+
451
+ `POST /gigs/:id/proofs/:proof_id/withdraw` — taking a submission back — is **refused** in this
452
+ mode (`409`). The buyer has already paid and may already have read it; un-delivering would leave
453
+ them holding a funded order with nothing against it and an approval they can no longer give. The
454
+ withheld note is the protection, not the ability to retract.
455
+
456
+ **Repeat extraction is an accepted, open risk.** Order, read, undo, re-order. It is bounded, not
457
+ closed, by: every withdrawal writing an `order_withdrawn` event against the participant, so a
458
+ serial extractor is visible in the ledger, and rate limits on the publish/withdraw pair. There is
459
+ no join gate to refuse them at the door — a vendor who wants somebody gone revokes the invite. If
460
+ you are building a vendor agent, withhold by default.
461
+
462
+ ## Approve, then settle, then reveal
463
+
464
+ Approval is a **cheap database write**. It sends no transaction. Payment is a **separate call**
465
+ that either party may make and that the daily cron makes on its own.
466
+
467
+ This is deliberate and not an oversight. An approve that also paid would not fit API Gateway's
468
+ 29-second cut — waiting on one confirmation alone leaves about nine seconds for a dozen reads and
469
+ writes — and there is no queue in the stack to defer to. Separating them removes the need for
470
+ one.
471
+
472
+ So an integrator must not treat `approved` as done:
473
+
474
+ 1. `PATCH .../proofs/:proof_id` → `approved`. Undo is now closed for both sides.
475
+ 2. `POST /gigs/:id/rollups` (either party) or wait for the cron.
476
+ 3. `proof.paid_out_at` is stamped. **This is the only proof that money moved.**
477
+ 4. `private_note` opens on the next read.
478
+
479
+ Rollups in this mode are grouped by **(mailbox, task)** rather than by mailbox alone. An order
480
+ machine has exactly one mailbox, so mailbox-only grouping would mint a single rollup naming every
481
+ buyer's order — which reverts *in full* if any one of those deposits was withdrawn, taking every
482
+ other buyer's paid-for work down with it. They are also chunked at the contract's batch ceiling,
483
+ so a vendor with a long backlog cannot mint a rollup that reverts on count and never recovers.
484
+
485
+ ## Report and comments both work in both directions
486
+
487
+ **`report`.** `POST /gigs/:id/proofs/:proof_id/report` works only on `timeout_approved` proofs —
488
+ the ones the clock approved because nobody looked. In this mode **either party** may call it, and
489
+ the row records which (`reported_by: "client" | "gigworker"`). It matters: a buyer reporting is
490
+ "I never got to review this and it is not what I paid for", which is their only remedy when the
491
+ timeout ruled for them; a vendor reporting is something else entirely about the same wallet.
492
+ A reported proof is excluded from payouts.
493
+
494
+ **Comments.** `GET`/`POST /gigs/:id/tasks/:msgId/comments` work for both the buyer and the vendor
495
+ on an order — the buyer passes the membership gate because they hold a mailbox. Private replies
496
+ work the same way. Read the role note at the top of this page before you render a byline: the
497
+ buyer's comments are stored `client` and the vendor's are stored `gigworker`, which is the
498
+ inverse of what "owns the gig" would tell you.
499
+
500
+ ## What this mode refuses, and why a `409` is an answer
501
+
502
+ Roughly twenty owner-only levers are closed on an order machine. Every one of them answers
503
+ **`409` with `reason: "inbound_order"`** and a sentence saying what to do instead. That is a
504
+ documented response, not a bug and not a permissions failure — the caller *is* the gig owner and
505
+ their credentials are fine; the operation is wrong for this mode.
506
+
507
+ | Refused | Why |
508
+ |---|---|
509
+ | `POST /gigs/:id/deposit` | Owner float is unreachable by the deposit-naming payout. |
510
+ | `DELETE /gigs/:id` | A soft close drops the gig from the payout sweep, stranding open deposits. |
511
+ | `DELETE .../tasks/:id`, `.../queue/:msgId`, `/queue/all`, `/inbound/all` | A deleted order still has real money behind it and nothing left that knows whose. |
512
+ | `DELETE /gigs/:id/proofs/all` | A delivery is the record a deposit settles against. |
513
+ | `POST .../tasks/:msgId/recycle`, `.../assign` | Taking paid work off its holder, or handing it to a wallet the buyer never chose. |
514
+ | `POST .../tasks/:msgId/extend` | There is no clock. `task_timeout` is pinned `null`; orders never expire. |
515
+ | `PATCH .../tasks/:msgId/availability` | An order is delivered, never queued. `view_only` would freeze paid work. |
516
+ | `POST .../queue/:msgId/violation`, `.../decline` | There is nobody else to give it to. Undo instead. |
517
+ | `PATCH .../tasks/:msgId/price` (once funded) | The amount was fixed by the deposit and is bounded on chain by it. Reported per task in `skipped` on the bulk route. |
518
+ | `PATCH .../tasks/:msgId/draft` | A draft order belongs to the buyer who wrote it. |
519
+ | `PATCH .../payload`, `.../private-details` | Scoped to the buyer, then closed once funded. |
520
+ | `POST .../proofs/:id/withdraw` | Un-delivering work the buyer has already read. |
521
+ | `POST /inbound/webhook` without `?draft=true` | An order is placed by the buyer, in the call that funds it. |
522
+ | `POST /inbound/email` | Answers `{"status":"ignored"}`. An order names its buyer; email cannot. |
523
+ | `price_tbd`, `allow_price_offers`, `task_timeout`, non-zero `min_payout` | Refused at gig create and on PATCH. Each is a way to pay the wrong number or to pay nobody. |
524
+ | `POST /gigs/:id/rotate-token` | Not refused — gated on `{ "confirm": true }`, because it locks out every buyer at once. |
525
+
526
+ ## Fields you will see on an order
527
+
528
+ `GET /gigs/:id/tasks/:msgId` and the proof routes project these **only to the two parties** — the
529
+ vendor and the buyer. A member merely browsing the shop gets *no key at all*, not a null, so an
530
+ order's value is not readable by everyone who joined.
531
+
532
+ | Field | Meaning |
533
+ |---|---|
534
+ | `funded_total` | What the buyer deposited. `null` on an unfunded draft. |
535
+ | `funded_minimum` | The shop's price **at publish**. The tip is the difference. |
536
+ | `tip` | Derived: `funded_total − funded_minimum`. |
537
+ | `price` | What the vendor is owed — the deposit after the fee, floored to a cent. |
538
+ | `fee_bps` | The rate snapshotted onto **this** deposit. Not the live rate. |
539
+ | `deposit_id` | The on-chain record. Present from the moment funding is *attempted*. |
540
+ | `client_email` | Visible to both parties — they are already transacting. |
541
+ | `client_user_id` | **The buyer alone.** An account id is an authority identifier; nobody else's is handed out. |
542
+ | `is_my_order` | Answers the same question without guessing. |
543
+ | `accepted_at` | When the vendor took delivery. There is no claim step in this mode. |
544
+ | `order_withdrawn_at` | Set on an undone order. Absent everywhere else. |
545
+
546
+ On `GET /orders`, `state` is one of `pending | open | settled | withdrawn`, `vendor_payout` is
547
+ derived from the deposit's own `fee_bps`, and `tip_estimate` is explicitly nullable and
548
+ best-effort — it is measured against the shop's price **today**. The exact figure is on the task
549
+ row. Do not treat the two as interchangeable.
550
+
551
+ `GET /orders` sorts on `created_at` **and you must not assume the natural order**. Unlike every
552
+ ULID-keyed list on this platform, the ledger's sort key is a keccak digest, so the partition's own
553
+ order is arbitrary — "first" does not mean "oldest", and a re-derived id under a stepped salt
554
+ lands somewhere unrelated.
555
+
556
+ ## What is still missing
557
+
558
+ Honest gaps, so you do not design around something that is not there.
559
+
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).
563
+ - **No route lists a gig's deposits.** A vendor cannot enumerate who owes what; they see the
564
+ aggregate `escrowed_funds` and their own order inbox.
565
+ - **No vendor display name on the gig read.** `GET /gigs/:id` gives `owner_wallet_alias` only, so
566
+ a shopfront has to resolve the person through `GET /profiles/:alias_id`.
567
+ - **A shop with live escrow is pinned to its Treasury.** Escrow leaves only by settling or by
568
+ undo, and nothing migrates a deposit between contracts. This constrains future platform
569
+ migrations, not your integration.
570
+ - **`GET /public/read-url` signs any S3 key for any share-token holder.** Unfixed. In this mode
571
+ an offloaded order payload is the buyer's brief, which is a new exposure class. Do not put
572
+ secrets in an order body; that is what `private_details` is for.
573
+ - **No refunds after payout, no partial undo, no post-approval tips.** Out of scope by decision.