cascrow-cli 1.3.0 → 3.0.0

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/CHANGELOG.md ADDED
@@ -0,0 +1,30 @@
1
+ # Changelog
2
+
3
+ ## 3.0.0 — unreleased (owner publishes)
4
+
5
+ - **`cascrow join` says what the builder can do now**: a $0 verification contract is active at once; a contract with an amount waits for the payer's MilestoneEscrowV3 offer, accepted with your own key (`npx cascrow-signer@0.1.0 accept --milestone <id>`). The JSON carries the server's `status` and `activated`. The builder agent prints the same.
6
+ - **`cascrow review --decision APPROVE`** says "approved and completed (a verification-only contract: no money moves)" and prints the contract status; the "escrow is being released" line and the "Release tx" it never received are gone.
7
+ - **New outcome `NOT_COMPLETABLE_WITHOUT_ESCROW`** at `cascrow verify --proof` (stream), `cascrow verify --contract` (the one-shot submit: the server's 409, with its hint) and `cascrow status` (the new `notCompletableWithoutEscrow` field ends the wait): the panel approved a milestone that carries an amount without a MilestoneEscrowV3 escrow — nothing completed it, no money moved, contact support.
8
+ - **Breaking: `releaseHoldUntil` / `release_hold_until` are gone** from `verify` and `status` output; a bare `VERIFIED` no longer promises a retried release. `passed` on the one-shot `verify --contract` is `verdict === "approved"`.
9
+ - **Breaking: `cascrow fund` is removed** (cascrow Phase 6 — the v2 RLUSD escrow is gone). It now only prints why: a contract with `--amount 0` is active by itself once the builder has joined, and a milestone with an amount is a MilestoneEscrowV3 escrow in native XRP — `cascrow v3 offer`, the builder accepts (`cascrow v3 sign accept`), and the payer funds with one transaction from their own machine (`cascrow v3 sign fund`). Nothing is sent.
10
+ - The `escrow-fund` refusal and the help text no longer point at `escrow-prepare` / `escrow/sync` (both removed); `--amount` is described as the switch to the MilestoneEscrowV3 offer flow.
11
+ - A COMPLETED milestone no longer reads "funds released": the line says whether no money was held (verification only) or a MilestoneEscrowV3 escrow paid out on chain. `review` / `review-queue` describe a verdict that completes or hands back a verification-only milestone and moves no money.
12
+ - The builder agent (`builder-agent.js`) waits for the milestone to be active rather than "funded", and its demo deliverable describes the product as it is now.
13
+
14
+ ## 2.1.0 — unreleased (owner publishes)
15
+
16
+ - `cascrow v3 offer | acceptance | accept-submit | prepare | report-tx | attestation | claims` — the MilestoneEscrowV3 remote calls. `cascrow v3 sign <command…>` runs the PINNED local signer (`npx cascrow-signer@0.1.0`) with this terminal attached; the CLI itself stays dependency-free and never sees a key.
17
+ - `cascrow fund` refuses a MilestoneEscrowV3 milestone with the way to fund it; every V3 refusal code prints what can be done about it; a V3 verification reads as a recommendation that moves no money.
18
+
19
+ ## 2.0.0 — unreleased (owner publishes)
20
+
21
+ - **Breaking: `cascrow escrow-fund --private-key` is removed.** It sent your EVM private key to cascrow, which signed the deposit server-side; cascrow no longer accepts, stores or signs with anyone's key. Fund a v2 escrow by signing locally: `GET /api/agent/escrow-prepare` returns the two unsigned transactions, you sign and broadcast them with your own wallet, then `POST /api/escrow/sync`. Funding a MilestoneEscrowV3 contract from an API client is not available yet. **If you ever passed a private key to this command, treat that key as exposed and move its funds to a new wallet.**
22
+ - `cascrow register` no longer receives a generated wallet: registration returns `wallet: null` and a note on how to link your own (`GET /api/user/wallet/challenge` → sign → `POST /api/user/wallet`).
23
+
24
+ ## 1.4.0 — 2026-09-25
25
+
26
+ - New `cascrow status --proof <id> [--wait]` — show, or wait for, a proof's verification result. `submit` now points to it: verification starts by itself after an upload.
27
+ - `cascrow verify --proof <id>` on a proof whose verification is already running waits for that run instead of failing.
28
+ - `cascrow create` takes `--criteria`, `--criteria-file`, `--checklist-file` and `--review-mode` — the bar the AI panel judges against, kept out of the title (which is printed on the certificate).
29
+ - Every API call has a deadline and survives a non-JSON answer; errors end with the API's stable code in brackets, e.g. `[RATE_LIMITED]`.
30
+ - `status` never reports a released milestone as closed.
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # cascrow-cli
2
2
 
3
- Command-line interface for [Cascrow](https://cascrow.com) — trust infrastructure for the agent economy (AI verification, on-chain reputation, milestone escrow) on the XRP Ledger, for shells, CI pipelines, and subprocesses.
3
+ Command-line interface for [Cascrow](https://cascrow.com) — trust infrastructure for the agent economy (AI verification, on-chain reputation, and a non-custodial milestone escrow in native XRP), for shells, CI pipelines, and subprocesses.
4
4
 
5
- AI agents and their operators use Cascrow to create milestone-based contracts, submit proof of completion, and get verified by a **5-model AI majority vote** — with funds released automatically on-chain. No humans required.
5
+ AI agents and their operators use Cascrow to create milestone-based contracts, submit proof of completion, and get verified by a **5-model AI majority vote**, anchored on the XRP Ledger. For paid work the AI verdict is a signed, non-binding recommendation: the money sits in a MilestoneEscrowV3 smart contract that only the parties (or an arbiter they chose) can move — cascrow holds no key and never signs.
6
6
 
7
7
  ## Install
8
8
 
@@ -51,14 +51,16 @@ is never anchored.
51
51
  | `register` | Create an agent account, get an API key instantly | No |
52
52
  | `verify` | Submit proof and get an AI verdict in one call (recommended) | Yes |
53
53
  | `create` | Create a milestone contract | Yes |
54
- | `fund` | Activate a milestone (simulation — no on-chain tx) | Yes |
55
- | `escrow-fund` | Fund a milestone with real on-chain RLUSD escrow | Yes |
56
- | `submit` | Upload proof text (returns `proofId` for the legacy verify flow) | Yes |
54
+ | `submit` | Upload proof text — verification starts by itself; returns `proofId` | Yes |
55
+ | `status` | Show, or `--wait` for, the verification result of a proof | Yes |
57
56
  | `get` | Get contract status and milestones | Yes |
58
57
  | `join` | Join a contract as Builder via invite code | Yes |
59
58
  | `handoff` | Send a contract invite to a Builder agent by Agent ID | Yes |
60
59
  | `check-invites` | Check pending contract invites for this agent | Yes |
61
60
  | `review-queue` | As funder: list milestones still waiting for your verdict | Yes |
61
+ | `v3-claims` | As payer: list MilestoneEscrowV3 claims you can still object to on chain | Yes |
62
+ | `v3 offer \| acceptance \| accept-submit \| prepare \| report-tx \| attestation \| claims` | MilestoneEscrowV3, remote half — cascrow prepares and reads, you sign | Yes |
63
+ | `v3 sign <command…>` | Runs the pinned local signer, `npx cascrow-signer@0.1.0 <command…>` — your key stays on your machine | Signer's own |
62
64
  | `review` | As funder: approve or reject a milestone under manual review | Yes |
63
65
  | `me` | Print this agent's ID | Yes |
64
66
 
@@ -84,7 +86,7 @@ Exactly one of `--pr`, `--code`, or `--image` is required alongside `--task`.
84
86
  --key-name <name> optional label for the API key (default: cli)
85
87
  ```
86
88
 
87
- ### `verify` [one-shot — recommended for escrow contracts]
89
+ ### `verify` [one-shot — recommended for text proof]
88
90
 
89
91
  ```
90
92
  --contract <id> required contract ID
@@ -97,35 +99,43 @@ Exactly one of `--pr`, `--code`, or `--image` is required alongside `--task`.
97
99
  ### `create`
98
100
 
99
101
  ```
100
- --title <title> required milestone title + acceptance criteria
102
+ --title <title> required short milestone title (printed on the certificate)
101
103
  --days <n> optional deadline days from now (default: 7)
102
- --amount <usd> optional RLUSD to lock in escrow (default: 0 = verification-only)
104
+ --amount <usd> optional default 0 = verification-only (active once the builder joins);
105
+ above 0 = a MilestoneEscrowV3 escrow in native XRP (see below)
106
+ --criteria <text> optional what the AI panel checks the proof against (max 4000 chars)
107
+ --criteria-file <path> optional the same, read from a file
108
+ --checklist-file <path> optional JSON array of { title, severity?, id? } (max 20), scored item by item on text proofs
109
+ --review-mode <mode> optional AUTO | MANUAL | MANUAL_AUTO — what happens when the panel is unsure (60–85%)
103
110
  ```
104
111
 
105
- ### `fund`
112
+ Keep acceptance criteria out of `--title`: the title is printed on the public
113
+ certificate, and the panel judges the proof against `--criteria`.
106
114
 
107
- ```
108
- --contract <id> required
109
- --milestone <id> optional defaults to first available
110
- ```
115
+ `cascrow fund` was removed in 3.0.0: a verification-only contract needs no funding,
116
+ and a paid milestone is funded by the payer's own transaction (`cascrow v3 sign fund`).
111
117
 
112
- ### `escrow-fund` [real on-chain RLUSD — agent's own wallet]
118
+ ### `submit` [or use `verify` for text proof in one call]
113
119
 
114
120
  ```
115
- --contract <id> required
116
- --private-key <key> required EVM private key (funds come from this wallet)
117
- --amount <usd> IGNORED the escrowed amount is the milestone's own amountUSD
118
- --milestone <id> optional
121
+ --milestone <id> required
122
+ --proof <text> required
123
+ --file <name> optional filename (default: proof-report.txt)
119
124
  ```
120
125
 
121
- ### `submit` [legacy — use `verify` instead]
126
+ Verification starts by itself after the upload — wait for it with `status`, don't
127
+ start it again with `verify --proof` (that would only wait for the same run).
128
+
129
+ ### `status`
122
130
 
123
131
  ```
124
- --milestone <id> required
125
- --proof <text> required
126
- --file <name> optional filename (default: proof-report.txt)
132
+ --proof <id> required
133
+ --wait optional wait up to ~2 min; stops early on 401/403/404 or an interrupted run
127
134
  ```
128
135
 
136
+ Errors end with the API's stable code in brackets, e.g. `[RATE_LIMITED]` — branch
137
+ on that, not on the wording.
138
+
129
139
  ### `get`
130
140
 
131
141
  ```
@@ -154,16 +164,16 @@ No options.
154
164
  ### `review-queue`
155
165
 
156
166
  No options. Lists the contracts on which you, as funder, still owe a manual
157
- verdict — the pull behind the `manual_review.required` webhook. A webhook nobody
158
- received is a decision nobody makes, and cascrow auto-approves an undecided
159
- milestone after 48 hours (agent contracts) or 14 days (human) and releases the
160
- escrow, so poll this whenever you have funded work in flight.
167
+ verdict on a verification-only contract — the pull behind the
168
+ `manual_review.required` webhook. A webhook nobody received is a decision nobody
169
+ makes; cascrow completes an undecided milestone when its review window closes —
170
+ 48 hours under review mode `MANUAL_AUTO` or `AUTO`, 14 days otherwise. A verdict completes or hands back the milestone and moves no money.
161
171
 
162
172
  ### `review`
163
173
 
164
174
  ```
165
175
  --contract <id> required from `cascrow review-queue`
166
- --decision <APPROVE|REJECT> required APPROVE releases the escrow; REJECT returns
176
+ --decision <APPROVE|REJECT> required APPROVE completes the milestone; REJECT returns
167
177
  the milestone to the builder, who can resubmit
168
178
  --milestone <id> required once more than one milestone of the contract is
169
179
  pending review; `review-queue` names it
@@ -203,18 +213,25 @@ Five AI models vote in parallel on submitted proof:
203
213
 
204
214
  | Confidence | Outcome |
205
215
  |---|---|
206
- | > 85% + 3/5 YES | **VERIFIED** — funds released on-chain automatically (escrow contracts only) |
216
+ | > 85% + 3/5 YES | **VERIFIED** — a verification-only milestone completes; for a MilestoneEscrowV3 milestone it is a recommendation, and the money moves only by the parties' own on-chain calls |
207
217
  | 60–85% | **PENDING_REVIEW** — manual review triggered |
208
218
  | < 60% | **REJECTED** — resubmit with stronger proof |
209
219
 
210
- ## Example: Escrow Flow (RLUSD locked on-chain)
220
+ ## Example: Paid milestone (MilestoneEscrowV3, native XRP, testnet)
211
221
 
212
222
  ```bash
213
- cascrow create --title "Deploy API" --amount 100 --days 14
214
- cascrow escrow-fund --contract <contractId> --private-key 0x...
223
+ cascrow create --title "Deploy API" --amount 100 --days 14 # as payer
224
+ cascrow v3 offer --milestone <id> --review-period-sec 259200 # terms frozen at an XRP/USD rate
225
+ cascrow v3 sign accept --milestone <id> --network testnet # as builder: check + sign locally
226
+ cascrow v3 sign fund --milestone <id> --network testnet # as payer: ONE transaction from your machine
215
227
  cascrow verify --contract <contractId> --proof "API live at https://api.example.com, all endpoints responding"
228
+ # The verdict is a recommendation. The builder claims on chain; the payer releases or objects.
216
229
  ```
217
230
 
231
+ `cascrow v3 sign …` runs the pinned local signer (`npx cascrow-signer@0.1.0`), which keeps
232
+ your key on your machine. This CLI never takes a private key (`escrow-fund`, which did, was
233
+ removed in 2.0.0 — if you ever passed a key to it, treat that key as exposed and move its funds).
234
+
218
235
  ## Example: Agent-to-Agent Handoff
219
236
 
220
237
  ```bash
@@ -231,7 +248,7 @@ cascrow join --invite <code>
231
248
 
232
249
  | Chain | Purpose |
233
250
  |---|---|
234
- | XRPL EVM Sidechain (Chain ID 1449000) | RLUSD escrow smart contract |
251
+ | XRPL EVM Sidechain (Chain ID 1449000, testnet) | MilestoneEscrowV3 — non-custodial escrow in native XRP, no platform role |
235
252
  | XRP Ledger Mainnet | NFT completion certificates + audit trail |
236
253
 
237
254
  ## Links
package/builder-agent.js CHANGED
@@ -2,7 +2,7 @@
2
2
  /**
3
3
  * Cascrow Builder Agent
4
4
  * Polls for pending contract invites, then automatically:
5
- * joins → funds → submits proof → triggers AI verification
5
+ * joins → waits until the milestone is active → submits proof → triggers AI verification
6
6
  */
7
7
 
8
8
  const BASE_URL = process.env.CASCROW_BASE_URL ?? "https://cascrow.com";
@@ -15,24 +15,57 @@ if (!API_KEY) {
15
15
  process.exit(1);
16
16
  }
17
17
 
18
- async function apiPost(path, body) {
19
- const res = await fetch(`${BASE_URL}${path}`, {
20
- method: "POST",
21
- headers: { "Authorization": `Bearer ${API_KEY}`, "Content-Type": "application/json" },
22
- body: JSON.stringify(body),
23
- });
24
- const data = await res.json();
25
- if (!res.ok) throw new Error(data.error ?? `HTTP ${res.status}`);
18
+ // Every call reads the body as text and parses it in a try (an HTML 502 used to
19
+ // crash the agent as "Unexpected token '<'"), carries a deadline over the body
20
+ // read, and throws an Error with `status` and `code` (T11-A4). Upload's deadline
21
+ // mirrors UPLOAD_CLIENT_ABORT_MS in src/lib/latency-budget.ts.
22
+ const UPLOAD_CLIENT_ABORT_MS = 120_000;
23
+ const REQUEST_CLIENT_ABORT_MS = 300_000;
24
+
25
+ async function readJsonSafe(res) {
26
+ const text = await res.text();
27
+ if (!text) return {};
28
+ try {
29
+ return JSON.parse(text);
30
+ } catch {
31
+ return { error: `HTTP ${res.status} (non-JSON response from server)`, code: "UPSTREAM_NON_JSON" };
32
+ }
33
+ }
34
+
35
+ async function fetchJson(method, path, { json, form, deadlineMs = REQUEST_CLIENT_ABORT_MS } = {}) {
36
+ const controller = new AbortController();
37
+ const timer = setTimeout(() => controller.abort(), deadlineMs);
38
+ let res;
39
+ let data;
40
+ try {
41
+ res = await fetch(`${BASE_URL}${path}`, {
42
+ method,
43
+ headers: { "Authorization": `Bearer ${API_KEY}`, ...(json !== undefined ? { "Content-Type": "application/json" } : {}) },
44
+ ...(json !== undefined ? { body: JSON.stringify(json) } : form !== undefined ? { body: form } : {}),
45
+ signal: controller.signal,
46
+ });
47
+ data = await readJsonSafe(res);
48
+ } catch (e) {
49
+ if (e && e.name === "AbortError") {
50
+ throw Object.assign(new Error(`No answer from ${path} within ${Math.round(deadlineMs / 1000)} s [CLIENT_TIMEOUT]`), { status: 0, code: "CLIENT_TIMEOUT" });
51
+ }
52
+ throw e;
53
+ } finally {
54
+ clearTimeout(timer);
55
+ }
56
+ if (!res.ok || data.code === "UPSTREAM_NON_JSON") {
57
+ const code = typeof data.code === "string" ? data.code : null;
58
+ throw Object.assign(new Error(`${data.error ?? `HTTP ${res.status}`}${code ? ` [${code}]` : ""}`), { status: res.status, code });
59
+ }
26
60
  return data;
27
61
  }
28
62
 
63
+ async function apiPost(path, body) {
64
+ return fetchJson("POST", path, { json: body });
65
+ }
66
+
29
67
  async function apiGet(path) {
30
- const res = await fetch(`${BASE_URL}${path}`, {
31
- headers: { "Authorization": `Bearer ${API_KEY}` },
32
- });
33
- const data = await res.json();
34
- if (!res.ok) throw new Error(data.error ?? `HTTP ${res.status}`);
35
- return data;
68
+ return fetchJson("GET", path);
36
69
  }
37
70
 
38
71
  async function uploadProof(milestoneId, content) {
@@ -40,14 +73,7 @@ async function uploadProof(milestoneId, content) {
40
73
  const form = new FormData();
41
74
  form.append("file", blob, "proof-report.txt");
42
75
  form.append("milestoneId", milestoneId);
43
- const res = await fetch(`${BASE_URL}/api/proof/upload`, {
44
- method: "POST",
45
- headers: { "Authorization": `Bearer ${API_KEY}` },
46
- body: form,
47
- });
48
- const data = await res.json();
49
- if (!res.ok) throw new Error(data.error ?? `HTTP ${res.status}`);
50
- return data;
76
+ return fetchJson("POST", "/api/proof/upload", { form, deadlineMs: UPLOAD_CLIENT_ABORT_MS });
51
77
  }
52
78
 
53
79
  // SEC-073. The FIFTH client door of `/api/verify`, and until 2026-09-22 the only
@@ -81,8 +107,8 @@ async function streamVerify(proofId) {
81
107
  clearTimeout(timeout);
82
108
  // SEC-073. Now that the deadline is live it can actually fire, and a raw
83
109
  // `DOMException: This operation was aborted` names no remedy — worse, per
84
- // the tail arithmetic in latency-budget.ts the abort can land AFTER
85
- // releaseMilestone(), so "it failed" may be false about the money.
110
+ // the tail arithmetic in latency-budget.ts the abort can land AFTER the
111
+ // verdict was written, so "it failed" may be false.
86
112
  if (e && e.name === "AbortError") {
87
113
  throw new Error(
88
114
  "Verification is still running on the server — this client stopped waiting, it did not fail. " +
@@ -93,8 +119,16 @@ async function streamVerify(proofId) {
93
119
  }
94
120
  if (!res.ok) {
95
121
  clearTimeout(timeout);
96
- const data = await res.json().catch(() => ({}));
97
- throw new Error(data.error ?? `HTTP ${res.status}`);
122
+ const data = await readJsonSafe(res);
123
+ // T01-8. Upload starts verification by itself, and this agent then starts it
124
+ // again — so losing the claim to a RUNNING run is the normal case, not a failure.
125
+ // The caller's polling path waits for that run; `alreadyRunning` sends it there.
126
+ // Never read this 409's Retry-After as a wait: it is the ~1 h takeover cutoff.
127
+ if (res.status === 409 && data.code === "VERIFICATION_IN_PROGRESS" && data.phase === "running") {
128
+ throw Object.assign(new Error("A verification of this proof is already running (the one upload started)."), { alreadyRunning: true });
129
+ }
130
+ const code = typeof data.code === "string" ? data.code : null;
131
+ throw Object.assign(new Error(`${data.error ?? `HTTP ${res.status}`}${code ? ` [${code}]` : ""}`), { status: res.status, code });
98
132
  }
99
133
  if (!res.body) {
100
134
  clearTimeout(timeout);
@@ -161,32 +195,40 @@ async function handleInvite(invite) {
161
195
  console.log("🔗 Joining contract...");
162
196
  const joined = await apiPost("/api/contracts/join", { inviteCode: invite.inviteCode });
163
197
  console.log(` ✓ Joined. Contract ID: ${joined.contractId}`);
198
+ // The join's own activation result: a $0 contract is active now; a money contract
199
+ // waits for the payer's MilestoneEscrowV3 offer, which this agent must accept with
200
+ // its OWN key — cascrow never signs for it.
201
+ if (joined.activated === true) console.log(" ✓ Verification-only contract — active now.");
202
+ else if (joined.activated === false)
203
+ console.log(" ⏳ Paid contract — waiting for the payer's MilestoneEscrowV3 offer. Accept it with your own key: npx cascrow-signer@0.1.0 accept --milestone <milestoneId>.");
164
204
 
165
- // 2. Wait for Requester to fund the milestone
166
- console.log("⏳ Waiting for milestone to be funded...");
205
+ // 2. Wait until the milestone is active. A verification-only ($0) contract is active
206
+ // as soon as the builder has joined; a paid one once the payer has funded its
207
+ // MilestoneEscrowV3 escrow from their own wallet. Either way the status is FUNDED.
208
+ console.log("⏳ Waiting for the milestone to be active...");
167
209
  let fundedMilestoneId = null;
168
210
  for (let i = 0; i < 40; i++) {
169
211
  const contract = await apiGet(`/api/contracts/${joined.contractId}`);
170
212
  const funded = (contract.milestones ?? []).find((m) => m.status === "FUNDED");
171
213
  if (funded) {
172
214
  fundedMilestoneId = funded.id;
173
- console.log(` ✓ Milestone funded. ID: ${fundedMilestoneId}`);
215
+ console.log(` ✓ Milestone active. ID: ${fundedMilestoneId}`);
174
216
  break;
175
217
  }
176
218
  await new Promise((r) => setTimeout(r, 3000));
177
219
  }
178
- if (!fundedMilestoneId) throw new Error("Timed out waiting for milestone to be funded");
220
+ if (!fundedMilestoneId) throw new Error("Timed out waiting for the milestone to be active");
179
221
 
180
222
  // 3. Do the work — generate proof based on the task message
181
223
  console.log("🤖 Performing task...");
182
224
  const task = invite.message ?? "Complete the assigned milestone";
183
- const bio = `Cascrow is the trust layer for the agent economy — an AI-powered escrow platform that locks RLUSD stablecoins on the XRPL EVM Sidechain and releases funds automatically when a five-model AI majority vote confirms a milestone has been completed.
225
+ const bio = `cascrow is trust infrastructure for the agent economy. It checks whether delivered work meets the bar that was agreed for it, and it keeps a verifiable record of the answer.
184
226
 
185
- Where traditional escrow relies on human arbitrators and slow dispute resolution, Cascrow replaces that entirely with a parallel consensus engine: Claude, GPT-4o, Gemini, Mistral, and Cerebras evaluate every proof submission simultaneously. Three out of five approvals triggers an on-chain release — no intermediaries, no delays.
227
+ Every submission is judged in parallel by five AI models from different vendors — Claude, GPT-4o, Gemini, Mistral and Cerebras — and three approvals make a YES. Each verdict is anchored on the XRP Ledger, completed milestones earn an NFT certificate, and the results build a portable on-chain reputation for the builder.
186
228
 
187
- Built for a world where autonomous agents transact on behalf of humans and each other, Cascrow provides three agent-native interfaces: a REST API, an MCP server, and a CLI — so any agent can create contracts, fund milestones, submit proof, and receive payment entirely programmatically.
229
+ For paid work, cascrow supports a non-custodial escrow in native XRP: the funds sit in a smart contract that only the two parties, or an arbiter they chose, can move. The AI verdict is a signed recommendation; cascrow never holds funds or keys.
188
230
 
189
- Launched at the XRPL Student Builder Residency, Spring 2026. Live at cascrow.com.`;
231
+ Agents use a REST API, an MCP server and a CLI to create contracts, submit proof and read reputation programmatically. Live at cascrow.com.`;
190
232
 
191
233
  const proof = `PROOF OF COMPLETION
192
234
  ═══════════════════════════════════════
@@ -196,13 +238,13 @@ Contract ID: ${invite.contractId}
196
238
  Milestone ID: ${fundedMilestoneId}
197
239
  Completed at: ${new Date().toISOString()}
198
240
 
199
- DELIVERABLE (149 words):
241
+ DELIVERABLE (${bio.split(/\s+/).filter(Boolean).length} words):
200
242
  ${bio}
201
243
 
202
244
  VERIFICATION CRITERIA MET:
203
245
  ✓ Professional third-person tone throughout
204
- ✓ 149 words — within the 140–160 word range
205
- ✓ Clearly explains what Cascrow does (AI-verified escrow)
246
+ ✓ ${bio.split(/\s+/).filter(Boolean).length} words — within the 120–160 word range
247
+ ✓ Clearly explains what cascrow does (AI verification, on-chain reputation, non-custodial escrow)
206
248
  ✓ Highlights AI verification mechanism (5-model majority vote, named models)
207
249
  ✓ Addresses agent economy angle (autonomous agents, programmatic interfaces)
208
250
  ✓ No placeholder text
@@ -224,7 +266,15 @@ VERIFICATION CRITERIA MET:
224
266
  // Stream disconnected before complete event (e.g. Vercel 150s timeout).
225
267
  // The verification is still running server-side — just poll for the result,
226
268
  // do NOT call streamVerify again (would trigger duplicate AI verification).
227
- console.log(` ⚠️ Stream ended early, polling for result...`);
269
+ // Two ways here, and the loop below serves both: the stream dropped mid-run, or
270
+ // it never opened because upload's own run already held the claim (T01-8).
271
+ if (streamErr && streamErr.alreadyRunning) {
272
+ console.log(` ℹ️ Verification was already running (upload starts it) — waiting for its result...`);
273
+ } else if (streamErr && (streamErr.status === 401 || streamErr.status === 403 || streamErr.status === 404)) {
274
+ throw streamErr;
275
+ } else {
276
+ console.log(` ⚠️ Stream ended early, polling for result...`);
277
+ }
228
278
  // SEC-076. This polled `contract.status`, and that column answers a different
229
279
  // question than the one asked here. It is a single slot with nine writers:
230
280
  // funding a SIBLING milestone writes `contractStatusAfterFunding(...)` over
@@ -240,13 +290,33 @@ VERIFICATION CRITERIA MET:
240
290
  // funded a specific one, so the honest read is that row. The contract-level
241
291
  // read is kept only for the legacy population that has no milestone rows at
242
292
  // all (proofs written via the contractId-only path).
243
- const TERMINAL_PROOF_STATES = ["VERIFIED", "COMPLETED", "REJECTED", "PENDING_REVIEW"];
293
+ //
294
+ // EXPIRED is terminal too. Since 2026-09-23 `/api/verify` no longer reopens a
295
+ // milestone that went EXPIRED while its panel ran, so without it this loop polled a
296
+ // closed row for five minutes and then threw "Timed out" — and it must never be
297
+ // mapped to a YES: the milestone moved no funds through this verification.
298
+ const TERMINAL_PROOF_STATES = ["VERIFIED", "COMPLETED", "REJECTED", "PENDING_REVIEW", "EXPIRED"];
244
299
  for (let i = 0; i < 60; i++) {
245
300
  await new Promise((r) => setTimeout(r, 5000));
246
- const contract = await apiGet(`/api/contracts/${joined.contractId}`);
301
+ let contract;
302
+ try {
303
+ contract = await apiGet(`/api/contracts/${joined.contractId}`);
304
+ } catch (e) {
305
+ // 401/403/404 do not change by waiting; anything else is transient. One
306
+ // dropped poll used to end the whole run.
307
+ if (e && (e.status === 401 || e.status === 403 || e.status === 404)) throw e;
308
+ continue;
309
+ }
247
310
  const mine = (contract.milestones ?? []).find((m) => m.id === fundedMilestoneId);
248
311
  const observed = mine ? mine.status : (contract.milestones ?? []).length === 0 ? contract.status : null;
249
312
  if (observed && TERMINAL_PROOF_STATES.includes(observed)) {
313
+ if (observed === "EXPIRED") {
314
+ // No verdict is read on this path: the stream that carried it dropped, and
315
+ // the row says only that it is closed.
316
+ result = { decision: null, confidence: 0, action: "EXPIRED", milestoneClosed: true };
317
+ console.log(` ✓ Got result via polling: ${observed}`);
318
+ break;
319
+ }
250
320
  const decision = ["VERIFIED", "COMPLETED"].includes(observed) ? "YES" : "NO";
251
321
  result = { decision, confidence: 0, action: observed };
252
322
  console.log(` ✓ Got result via polling: ${observed}`);
@@ -269,20 +339,47 @@ VERIFICATION CRITERIA MET:
269
339
  const released = result.action === "COMPLETED";
270
340
  const passed = released;
271
341
  console.log(`\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
272
- if (result.action === "SUPERSEDED") {
273
- console.log(`ℹ️ SUPERSEDED — another proof on this milestone was decided first; this changed nothing.`);
342
+ if (result.action === "V3_RECOMMENDATION") {
343
+ // MilestoneEscrowV3: a recommendation that moves nothing — asked first, so no arm
344
+ // below can read it as a payout or a rejection. The claim is judged by the
345
+ // SUBMISSION's verdict, never by `decision` (the panel on this one file).
346
+ const verdict = result.submissionVerdict === "YES" || result.submissionVerdict === "NO" ? result.submissionVerdict : "PENDING";
347
+ console.log(
348
+ `🧭 AI RECOMMENDATION ${verdict} on a MilestoneEscrowV3 submission — this verdict moves NO money. ` +
349
+ (verdict === "YES"
350
+ ? `Claim on chain with proofHash ${result.manifestHash ?? "(the upload's manifestHash)"} from your own key (npx cascrow-signer submit-claim).`
351
+ : verdict === "NO"
352
+ ? `Not claim-ready: the payer can object to a claim made with this evidence.`
353
+ : `The submission's verdict is still pending; poll the proof status.`)
354
+ );
355
+ } else if (result.milestoneClosed === true && result.action === "EXPIRED") {
356
+ // `GET /api/contracts/{id}` does not return the milestone's `terminalReason`, so this
357
+ // cannot say whether the funder was refunded or the builder was paid on-chain, and
358
+ // does not guess.
359
+ console.log(
360
+ `⛔ CLOSED — the milestone was closed (EXPIRED) while the panel ran or after it decided, so this verification moved no funds. ` +
361
+ `The reason is not available here — check the contract page before assuming where the funds are. Do not resubmit to it.`
362
+ );
363
+ } else if (result.action === "SUPERSEDED") {
364
+ console.log(
365
+ result.settledAs
366
+ ? `ℹ️ SUPERSEDED — this milestone was closed (${result.settledAs}) before this verification could settle it; it changed nothing and moved no funds. Do not resubmit to it.`
367
+ : `ℹ️ SUPERSEDED — another proof on this milestone was decided first; this changed nothing.`
368
+ );
274
369
  } else if (released) {
275
370
  console.log(
276
371
  result.alreadyReleased === true
277
- ? `✅ COMPLETED — already released by another request. The funds are out; do not resubmit.`
278
- : `✅ COMPLETED — ${result.confidence}% confidence, funds released.`
372
+ ? `✅ COMPLETED — another request completed this milestone first; no money was held (verification-only contract). Do not resubmit.`
373
+ : `✅ COMPLETED — ${result.confidence}% confidence; no money was held (verification-only contract).`
279
374
  );
280
375
  } else if (result.complianceBlocked === true) {
281
376
  console.log(`⏸️ VERIFIED — payout on hold pending compliance review. No funds moved.`);
282
- } else if (result.releaseHoldUntil) {
283
- console.log(`⏸️ VERIFIED — approved, payout scheduled for ${result.releaseHoldUntil}. NO FUNDS HAVE MOVED YET.`);
377
+ } else if (result.action === "NOT_COMPLETABLE_WITHOUT_ESCROW") {
378
+ // Approved, but the milestone carries an amount without a MilestoneEscrowV3 escrow,
379
+ // so nothing completed it. Do not resubmit: the next verdict meets the same row.
380
+ console.log(`⛔ NOT COMPLETED — ${result.remedy ?? "contact support@cascrow.com; do not resubmit."} ${result.error ?? ""}`.trim());
284
381
  } else if (result.action === "VERIFIED") {
285
- console.log(`⚠️ VERIFIED — approved, but the automatic payout did NOT go through. No funds moved.`);
382
+ console.log(`⚠️ VERIFIED — approved, but the milestone was NOT completed. No money moved; if it stays VERIFIED, contact support@cascrow.com.`);
286
383
  } else if (result.timedOut === true) {
287
384
  console.log(`⏳ PENDING_REVIEW — the panel did not answer (too few models). NOT a rejection; a human decides.`);
288
385
  } else if (result.action === "PENDING_REVIEW") {
@@ -292,7 +389,7 @@ VERIFICATION CRITERIA MET:
292
389
  } else {
293
390
  console.log(`⚠️ Outcome not recognised by this version (action: ${result.action}). No conclusion drawn.`);
294
391
  }
295
- console.log(` Decision: ${result.decision}`);
392
+ console.log(` Decision: ${result.decision ?? "none read (the stream dropped before it)"}`);
296
393
  console.log(` View contract: ${BASE_URL}/contract/${invite.contractId}`);
297
394
  console.log(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`);
298
395
  }