@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.
- 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 +39 -0
- package/SKILL.director.md +42 -3
- 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 +1253 -0
- package/dist/src/devcli/marketplace-gigs.js +162 -16
- package/marketplace.md +275 -1
- package/package.json +28 -3
- 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.
|