cascrow-mcp 1.8.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 CHANGED
@@ -1,5 +1,34 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.0.0 — unreleased (owner publishes)
4
+
5
+ - **`cascrow_join_contract` says what the builder can do now.** The server's answer carries `status` and `activated`, returned as well as rendered: a $0 verification contract is active at once — submit proof; a contract with an amount waits for the payer's MilestoneEscrowV3 offer, which you accept with your own key (`npx cascrow-signer@0.1.0 accept --milestone <id>`). It used to say "Contract is now AWAITING_ESCROW — ready to receive proof once funded" for both, so a $0 builder waited for money that never comes.
6
+ - **New outcome `NOT_COMPLETABLE_WITHOUT_ESCROW`.** When the panel approves a milestone that carries an amount without a MilestoneEscrowV3 escrow, nothing can complete it: `cascrow_verify` renders "⛔ NOT COMPLETED — … no money moved; contact support", `cascrow_mcp_submit` surfaces the server's 409 with the same hint, and `cascrow_get_proof_status` / `cascrow_watch_verification` read the new `notCompletableWithoutEscrow` field and stop polling. Such a milestone used to read as "approved, the automatic payout did not go through — the release can be retried", a retry that no longer exists.
7
+ - **Breaking: `releaseHoldUntil` / `release_hold_until` are gone** from every result, with the v2 release window they reported. A bare `VERIFIED` no longer promises a retried release.
8
+ - New hint for `RELEASE_HOLD_REMOVED` (the server refuses `releaseHoldHours` on contract creation).
9
+ - **Breaking: `cascrow_fund_milestone` is removed** (cascrow Phase 6 — the v2 RLUSD escrow is gone). A $0 verification contract is active by itself once the builder has joined; there is nothing to fund. A milestone with an amount is a MilestoneEscrowV3 escrow in native XRP: the payer offers terms (`cascrow_v3_offer`), the builder accepts with their own key, and the payer funds it with one transaction from their own machine — `npx cascrow-signer@0.1.0 fund --milestone <milestoneId>`. A host that still calls the old tool gets an error that says exactly this (`[TOOL_REMOVED]`).
10
+ - The `cascrow_escrow_fund` refusal names only the V3 flow and `cascrow-signer`; the pointers to `escrow-prepare` and `escrow/sync` are gone with those endpoints.
11
+ - `amountUSD` in `cascrow_create_contract`: 0 (or absent) = verification only; above 0 = MilestoneEscrowV3 offer flow.
12
+ - `cascrow_review_queue` / `cascrow_review_contract` describe what a funder verdict does now: it completes or hands back a verification-only milestone and moves no money. A COMPLETED milestone no longer reads "funds released" — it says whether no money was held or a MilestoneEscrowV3 escrow paid out on chain.
13
+ - `cascrow_get_reputation` prints `totalReleasedUsd` (and the XRP released by MilestoneEscrowV3 escrows) instead of "RLUSD released". `totalRlusdReleased` is still in the response, deprecated.
14
+
15
+ ## 2.1.0 — unreleased (owner publishes)
16
+
17
+ - **MilestoneEscrowV3, remote half.** New tools: `cascrow_v3_offer` (payer offers terms), `cascrow_v3_acceptance_get` / `cascrow_v3_acceptance_submit` (builder reads the frozen terms and hands over the acceptance signature), `cascrow_v3_prepare` (an UNSIGNED transaction — rebuild `data` and check `to`/`chainId` yourself), `cascrow_v3_report_tx` (a transaction HASH only; a signed transaction is refused before anything is sent) and `cascrow_v3_attestation` (the non-binding AI recommendation for a claim). cascrow never signs, sends or relays: signing happens in the separate, pinned local signer, `npx cascrow-signer@0.1.0`.
18
+ - `cascrow_fund_milestone` refuses a MilestoneEscrowV3 milestone with the way to fund it (`npx cascrow-signer@0.1.0 fund`), and every V3 refusal code (`V3_DISABLED`, `RATE_UNAVAILABLE`, `V3_NOT_OFFERED`, `OFFER_EXISTS`, `V3_OFFER_OPEN`, `ATTESTATION_PENDING`, `CLAIM_PROOF_UNKNOWN`, `V3_ESCROW`, `INVALID_SIGNATURE`, `V3_INDEX_STALE`, …) comes with what the recipient can do.
19
+ - A verification of a V3 milestone reads as `V3_RECOMMENDATION` — "moves NO money" — in `cascrow_verify` and `cascrow_mcp_submit`, never as approved/paid or rejected.
20
+
21
+ ## 2.0.0 — unreleased (owner publishes)
22
+
23
+ - **Breaking: `cascrow_escrow_fund` is removed.** It sent the agent's EVM private key to cascrow, which signed the deposit server-side; cascrow no longer accepts, stores or signs with anyone's key. A host that still calls the tool by name gets an error that says so, and nothing it passed is read. 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 tool, treat that key as exposed and move its funds to a new wallet.**
24
+ - The anonymous-mode message states the real number of tools (it said 18).
25
+ - Includes the unreleased 1.8.1 changes below, which were never published on their own.
26
+
27
+ ## 1.8.1 — unreleased, folded into 2.0.0
28
+
29
+ - `cascrow_mcp_submit`'s description names `V3_ESCROW` as final: a 409 with that code means the milestone is held in a MilestoneEscrowV3 escrow that only its payer, payee or arbiter moves on chain — retrying will not help.
30
+ - `cascrow_get_proof_status` and `cascrow_watch_verification` narrate the MilestoneEscrowV3 terminal reasons (`ARBITER_REFUNDED`, `REFUNDED_AFTER_OBJECTION_AI_NO`, `REFUNDED_AFTER_OBJECTION_DISPUTED`, `RETURNED_BY_PAYEE`, `RETURNED_BEFORE_WORK`) instead of "the reason was not recorded".
31
+
3
32
  ## 1.8.0 — 2026-09-25
4
33
 
5
34
  - **Verification starts by itself after `cascrow_submit_proof`.** Wait for the verdict with `cascrow_watch_verification { proofId }` — the tool now has an attach mode — instead of calling `cascrow_verify`, which only raced the run already going.
package/README.md CHANGED
@@ -1,15 +1,15 @@
1
1
  # cascrow-mcp
2
2
 
3
- MCP server for [Cascrow](https://cascrow.com) — trust infrastructure for the agent economy: AI verification, on-chain reputation, and milestone escrow on the XRP Ledger.
3
+ MCP server for [Cascrow](https://cascrow.com) — trust infrastructure for the agent economy: AI verification, on-chain reputation, and — for work that is paid — a non-custodial milestone escrow (MilestoneEscrowV3, native XRP; testnet for now).
4
4
 
5
- AI agents 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 use Cascrow to create milestone-based contracts, submit proof of completion, and get verified by a **5-model AI majority vote**, with every verdict anchored on the XRP Ledger. For a paid milestone the AI result is a signed, non-binding recommendation: the money sits in a MilestoneEscrowV3 smart contract that only the payer, the builder or an arbiter they chose can move — cascrow holds no key and never signs.
6
6
 
7
7
  ## Quick Start
8
8
 
9
9
  **No API key yet?** Every setup below works without `CASCROW_API_KEY` too — the server starts either
10
10
  way, but only `cascrow_verify_work` (verify AI-generated work, no escrow) is available until you set
11
11
  one, at the same 3-free-per-IP-per-day tier as [cascrow.com/verify](https://cascrow.com/verify). Set
12
- `CASCROW_API_KEY` (see Agent Registration below) once you want the other 17 tools — it also anchors
12
+ `CASCROW_API_KEY` (see Agent Registration below) once you want the other tools — it also anchors
13
13
  `cascrow_verify_work` verdicts on the XRP Ledger mainnet (anonymous calls never are); see below.
14
14
 
15
15
  ### Claude Desktop
@@ -126,19 +126,25 @@ Rate limit: 3 registrations per IP per hour.
126
126
  | `cascrow_verify` | Start or re-run 5-model AI verification. If a run is already going (e.g. the one upload started), waits for it instead of failing. |
127
127
  | `cascrow_get_proof_status` | Poll a submitted proof's verification result. |
128
128
  | `cascrow_watch_verification` | Block until verification completes — either submit + wait (`milestoneId` + `content`) or wait for an existing proof (`proofId`). Stops early and says why when waiting cannot help. |
129
- | `cascrow_mcp_submit` | Submit proof and get an immediate verdict in one call — auto-releases funds and mints the NFT on approval. |
129
+ | `cascrow_mcp_submit` | Submit proof and get an immediate verdict in one call. A verification-only milestone completes on approval (NFT certificate minted); for a MilestoneEscrowV3 milestone the verdict is a recommendation that moves no money. |
130
130
  | `cascrow_verify_work` | Verify AI work with no escrow contract, from a GitHub PR, pasted code, or a base64-encoded screenshot. Returns a public report URL. With `CASCROW_API_KEY` set, the verdict is also anchored on the XRP Ledger mainnet — anonymous calls are not. |
131
- | `cascrow_escrow_fund` | Fund a milestone with an EVM private key (fully autonomous). |
132
- | `cascrow_fund_milestone` | Fund via MetaMask (browser flow). |
133
131
  | `cascrow_get_contract` | Get contract status and all milestone states. |
134
132
  | `cascrow_list_my_contracts` | List all contracts for this agent, as Requester and/or Builder. |
135
- | `cascrow_review_queue` | As funder: list milestones still waiting for your verdict. Poll it — an undecided milestone auto-approves after 48h. |
136
- | `cascrow_review_contract` | As funder: approve or reject a milestone the AI panel scored 60–85. |
137
- | `cascrow_join_contract` | Join a contract as Builder agent via invite code. |
133
+ | `cascrow_review_queue` | As funder of a verification-only contract: list milestones still waiting for your verdict. Poll it — an undecided milestone is completed automatically after 48 h under review mode `MANUAL_AUTO` or `AUTO`, 14 days otherwise. |
134
+ | `cascrow_v3_claims` | As payer of a MilestoneEscrowV3 escrow: list claims that release it unless your own wallet calls `object()` before `objectionDeadline`. cascrow cannot object for you. |
135
+ | `cascrow_v3_offer` | As payer: offer MilestoneEscrowV3 terms (native XRP) for a milestone. The builder accepts with their own key. |
136
+ | `cascrow_v3_acceptance_get` / `cascrow_v3_acceptance_submit` | As builder: read the frozen terms, hand over your EIP-712 acceptance signature (binding until `acceptBy`). |
137
+ | `cascrow_v3_prepare` | An UNSIGNED transaction for any escrow action. Rebuild `data` and check `to`/`chainId` against your own config before signing. |
138
+ | `cascrow_v3_report_tx` | After you broadcast: the transaction **hash** (never a signed transaction), so the index re-reads now. |
139
+ | `cascrow_v3_attestation` | cascrow's signed, non-binding AI recommendation for a claim. |
140
+
141
+ **Signing is never done here.** cascrow holds no key of yours and relays nothing. For MilestoneEscrowV3 use the separate local signer — `npx cascrow-signer@0.1.0` (CLI or its own MCP server, tools `cascrow_signer_*`) — which checks every prepared transaction against its own pinned chain and contract.
142
+ | `cascrow_review_contract` | As funder of a verification-only contract: approve (completes it) or reject (hands it back) a milestone the AI panel scored 60–85. Moves no money. |
143
+ | `cascrow_join_contract` | Join a contract as Builder agent via invite code. The answer says whether it is active now (`activated: true`, a $0 contract — submit proof) or waits for the payer's MilestoneEscrowV3 offer (accept it with your own key: `npx cascrow-signer@0.1.0 accept --milestone <id>`). |
138
144
  | `cascrow_handoff` | Send a contract to another agent by Agent ID. |
139
145
  | `cascrow_get_agent_id` | Get this agent's own Agent ID. |
140
146
  | `cascrow_check_invites` | Check pending contract invites. |
141
- | `cascrow_get_reputation` | Look up any agent's on-chain track record by wallet address — completed, failed and success rate over every eligible milestone, plus the milestone records the builder has published (opt-in, default private) with their proof hashes. No auth required. |
147
+ | `cascrow_get_reputation` | Look up any agent's on-chain track record by wallet address — completed, failed, success rate and value released (`totalReleasedUsd`, plus XRP for MilestoneEscrowV3; `totalRlusdReleased` is deprecated) over every eligible milestone, plus the milestone records the builder has published (opt-in, default private) with their proof hashes. No auth required. |
142
148
  | `cascrow_discover_agents` | Search discoverable Builder agents by skill tag and minimum success rate. |
143
149
  | `cascrow_set_discoverable` | Opt into the agent marketplace, set skill tags, register a callback URL. |
144
150
 
@@ -156,7 +162,7 @@ Five AI models vote in parallel on submitted proof:
156
162
 
157
163
  | Confidence | Outcome |
158
164
  |------------|---------|
159
- | > 85% + 3/5 YES | **VERIFIED** — funds released on-chain automatically |
165
+ | > 85% + 3/5 YES | **VERIFIED** — a verification-only milestone completes; for a MilestoneEscrowV3 milestone this is a recommendation (every file needs a definite yes), and the money moves only by the parties' own on-chain calls |
160
166
  | 60–85% | **PENDING_REVIEW** — human review triggered |
161
167
  | < 60% | **REJECTED** — submitter can improve and resubmit |
162
168
 
@@ -182,28 +188,25 @@ Claude will:
182
188
 
183
189
  > Set `amountUSD: 0` for **verification-only mode** — the full 5-model AI pipeline runs without locking any funds. Useful for trustless proof of work without payment.
184
190
 
185
- ## Agent-to-Agent Payment Flow
191
+ ## Agent-to-Agent Payment Flow (MilestoneEscrowV3, testnet)
186
192
 
187
- Cascrow supports fully autonomous agent-to-agent payments:
193
+ 1. **Requester (payer)** creates the contract (`cascrow_create_contract`) with an `amountUSD` above 0 and hands it to the **Builder** (`cascrow_handoff`); the builder joins (`cascrow_check_invites` + `cascrow_join_contract`).
194
+ 2. The payer offers the escrow terms — amount in native XRP at a frozen XRP/USD rate, review period, optional arbiter — with `cascrow_v3_offer`.
195
+ 3. The builder reads the frozen terms (`cascrow_v3_acceptance_get`), checks them and signs the acceptance with its own key (`npx cascrow-signer@0.1.0 accept`, or `cascrow_v3_acceptance_submit` with a signature made elsewhere). The signature binds until `acceptBy`.
196
+ 4. The payer funds the escrow with **one transaction from its own machine**: `npx cascrow-signer@0.1.0 fund --milestone <id>` (or the signer's MCP tool `cascrow_signer_fund`).
197
+ 5. The builder delivers → `cascrow_submit_proof` → `cascrow_watch_verification`. The AI result is a signed, non-binding recommendation (`cascrow_v3_attestation`).
198
+ 6. The builder claims on chain with the submission's `manifestHash`. The payer releases, or objects before the objection deadline (`cascrow_v3_claims` lists open claims); without an objection the builder can take the money after the review period.
188
199
 
189
- 1. **Requester** creates contract (`cascrow_create_contract`) + funds it (`cascrow_escrow_fund` with EVM private key)
190
- 2. Requester sends work to **Builder** via `cascrow_handoff` using the Builder's Agent ID
191
- 3. Builder accepts via `cascrow_check_invites` + `cascrow_join_contract`
192
- 4. Builder completes work → `cascrow_submit_proof` → `cascrow_watch_verification`
193
- 5. On 3/5 YES: RLUSD released to Builder's wallet + NFT certificate minted on XRP Ledger
194
-
195
- No humans required at any step.
200
+ cascrow never accepts, creates, stores or signs with a private key and is never the arbiter (`cascrow_escrow_fund`, which took a key, was removed in 2.0.0 — if you ever passed a key to it, treat that key as exposed and move its funds). Each agent links its own wallet once — `GET /api/user/wallet/challenge` → sign → `POST /api/user/wallet`. A contract without an amount needs none of this: it is active once the builder has joined.
196
201
 
197
202
  ## Blockchain Architecture
198
203
 
199
204
  | Chain | Purpose |
200
205
  |-------|---------|
201
- | XRPL EVM Sidechain (Chain ID 1449000) | RLUSD escrow smart contract |
206
+ | XRPL EVM Sidechain (Chain ID 1449000, testnet) | MilestoneEscrowV3 — non-custodial escrow in native XRP, no platform role |
202
207
  | XRP Ledger Mainnet | NFT completion certificates + audit trail |
203
208
 
204
- Every verified milestone produces:
205
- - An on-chain RLUSD transfer to the recipient
206
- - A non-transferable NFT certificate on XRP Ledger Mainnet as permanent proof of completion
209
+ Every completed milestone produces a non-transferable NFT certificate on XRP Ledger Mainnet as permanent proof of completion; a MilestoneEscrowV3 payout is the escrow contract's own on-chain transfer to the builder.
207
210
 
208
211
  ## Links
209
212
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  **Status: Done.** The HTTP transport described below is live at
4
4
  `src/app/api/mcp/route.ts` (Streamable HTTP, `@modelcontextprotocol/sdk`,
5
- `WebStandardStreamableHTTPServerTransport`), serving the same 18 tools as this
5
+ `WebStandardStreamableHTTPServerTransport`), serving the same tools as this
6
6
  package's stdio server. `smithery.yaml` already lists both the remote URL
7
7
  (`https://cascrow.com/api/mcp`) and the local `npx cascrow-mcp` stdio fallback.
8
8
  The plan below is kept as a historical record of what was built and why —
@@ -46,11 +46,11 @@ Replace stdio transport with HTTP transport using `@modelcontextprotocol/sdk`:
46
46
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
47
47
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
48
48
 
49
- // Same 16 tools as cascrow-mcp/index.js but served over HTTP
49
+ // Same tools as cascrow-mcp/index.js but served over HTTP
50
50
  // Auth: API key from Authorization header or ?api_key= query param
51
51
  ```
52
52
 
53
- The 16 tools already exist in `cascrow-mcp/index.js` — they just need to be
53
+ The tools already exist in `cascrow-mcp/index.js` — they just need to be
54
54
  ported from stdio to HTTP transport. All business logic (`/api/mcp/submit`,
55
55
  `/api/contracts`, etc.) stays unchanged.
56
56
 
package/index.js CHANGED
@@ -1,8 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
3
  * Cascrow MCP Server
4
- * Gives Claude the ability to create contracts, fund milestones,
5
- * submit proofs, and verify deliverables — all via the Cascrow API.
4
+ * Gives an agent the ability to verify work with cascrow's AI panel, create
5
+ * contracts, submit proofs, read on-chain reputation and — for a milestone with
6
+ * an amount — take part in a non-custodial MilestoneEscrowV3 escrow (offer,
7
+ * acceptance, unsigned transactions). It never signs or sends a transaction:
8
+ * signing happens in the separate local signer, cascrow-signer.
6
9
  */
7
10
 
8
11
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
@@ -28,6 +31,106 @@ function xrplTxUrl(hash) {
28
31
  // IP-per-day tier as the web page). Every other tool needs a real account.
29
32
  const ANONYMOUS_MODE = !API_KEY;
30
33
 
34
+ // Tools removed because they took a private key (2.0.0, cascrow Phase 5). The
35
+ // words are the server's own refusal (src/lib/custody-path-removed.ts), copied
36
+ // because a published package cannot import from the app;
37
+ // custody-removed-guards.test.ts holds the copy equal to the owner.
38
+ const REMOVED_CUSTODY_TOOLS = ["cascrow_escrow_fund"];
39
+ const CUSTODY_TOOL_REMOVED =
40
+ "Error: This way of funding has been removed: cascrow never accepts, creates, stores or signs with private keys. " +
41
+ "A $0 verification contract needs no funding — it is active once the builder has joined. " +
42
+ "A milestone with an amount is a MilestoneEscrowV3 escrow in native XRP, funded by the payer with one transaction " +
43
+ "from their own machine: first link the wallet you will fund from (GET /api/user/wallet/challenge, sign the message, " +
44
+ "POST /api/user/wallet), then run the local signer: npx cascrow-signer@0.1.0 fund --milestone <milestoneId>. " +
45
+ "If your request contained a private key, treat it as exposed and move its funds to a new wallet. " +
46
+ "[CUSTODY_PATH_REMOVED]";
47
+
48
+ // What every cascrow client says about MilestoneEscrowV3 — a JSON copy of
49
+ // V3_CLIENT_TEXT in src/lib/v3-client-text.ts (this package cannot import the
50
+ // app). v3-client-text-parity.test.ts holds the copy equal to the owner.
51
+ // BEGIN v3-client-text
52
+ const V3_CLIENT_TEXT = {
53
+ "signerPin": "cascrow-signer@0.1.0",
54
+ "remoteTools": [
55
+ "cascrow_v3_offer",
56
+ "cascrow_v3_acceptance_get",
57
+ "cascrow_v3_acceptance_submit",
58
+ "cascrow_v3_prepare",
59
+ "cascrow_v3_report_tx",
60
+ "cascrow_v3_attestation",
61
+ "cascrow_v3_claims"
62
+ ],
63
+ "codeHints": {
64
+ "V3_DISABLED": "MilestoneEscrowV3 is switched off for NEW money right now (offers, acceptances, funding). An escrow that already exists is never locked: object, release, refund, claim and withdraw keep working (cascrow_v3_prepare, or npx cascrow-signer@0.1.0 without cascrow at all).",
65
+ "RATE_UNAVAILABLE": "cascrow could not get a consistent XRP/USD rate from its two sources, so no offer can be priced. Nothing was created; retry in a minute.",
66
+ "V3_NOT_OFFERED": "The payer has not offered MilestoneEscrowV3 terms for this milestone yet — only the payer can (cascrow_v3_offer). The payer has been notified.",
67
+ "OFFER_EXISTS": "An open offer already exists for this milestone and stays valid until it expires. Wait for the payee's acceptance or for the offer to lapse; sending the same inputs again returns the same offer.",
68
+ "V3_OFFER_OPEN": "An open MilestoneEscrowV3 offer, or a signed acceptance that is not funded yet, is bound to this milestone or wallet, so it cannot change now. Wait until the payer funds the escrow from their own machine (npx cascrow-signer@0.1.0 fund --milestone <milestoneId>) or the offer or acceptance lapses.",
69
+ "ATTESTATION_PENDING": "The AI panel has not finished on this claim. Its result is a non-binding recommendation — as payer you can object NOW, before the objection deadline, without waiting for it.",
70
+ "CLAIM_PROOF_UNKNOWN": "This claim's proofHash matches no evidence submitted to cascrow, so there is no AI recommendation for it. As payer, judge the delivery yourself and object before the objection deadline if it is not what was agreed.",
71
+ "V3_ESCROW": "This milestone is held in a MilestoneEscrowV3 escrow: only its payer, payee or arbiter moves that money, on chain, with their own key. Get the unsigned transaction with cascrow_v3_prepare, or sign locally with npx cascrow-signer@0.1.0.",
72
+ "V2_ESCROW_PRESENT": "This milestone or its contract still carries data of the retired v2 (RLUSD) escrow, so it cannot become a MilestoneEscrowV3 escrow. Nothing was changed; contact support.",
73
+ "INVALID_SIGNATURE": "The acceptance signature does not verify for your linked wallet over these exact terms (EIP-712, low-s, from an ordinary account). Sign the terms returned by cascrow_v3_acceptance_get again — npx cascrow-signer@0.1.0 accept does it and checks them first.",
74
+ "V3_INDEX_STALE": "cascrow's index of the chain is behind. Read the escrow from the chain directly (npx cascrow-signer@0.1.0 claims, or getEscrow / ClaimSubmitted); every deadline, above all an objection window, runs on chain regardless.",
75
+ "ACCEPTANCE_LAPSED": "The acceptance expired without funding, so the milestone is closed — this is not a failure of either party. A new attempt is a new milestone.",
76
+ "NOT_COMPLETABLE_WITHOUT_ESCROW": "The work was approved, but this milestone carries an amount without a MilestoneEscrowV3 escrow, and the previous escrow system has been removed — nothing can complete or pay it, so it was left as it was and NO money moved. cascrow has opened a case for it; contact support@cascrow.com with the contract ID. Do not resubmit: your evidence is recorded.",
77
+ "RELEASE_HOLD_REMOVED": "releaseHoldHours was removed with the previous escrow system; send the contract without it. A MilestoneEscrowV3 escrow's review period is agreed in the payer's offer (cascrow_v3_offer), and a $0 verification contract has no payout to delay.",
78
+ "AMOUNT_VIA_MILESTONES": "This draft has milestones, and their amounts are the contract's amount — it cannot be set on the contract. Nothing was changed. To change an amount, delete the draft and create the contract again; before joining, the invited builder can propose other amounts in a counter-proposal, which the requester can accept.",
79
+ "DEADLINE_VIA_MILESTONES": "This draft has milestones, and their deadlines are the ones enforced — a contract-level deadline changes nothing, so it is refused. Nothing was changed. To change a deadline, delete the draft and create the contract again; before joining, the invited builder can propose other deadlines in a counter-proposal, which the requester can accept.",
80
+ "MIXED_AMOUNTS": "A contract is either verification-only (every milestone $0) or escrowed (every milestone above $0), never both. Nothing was created or changed. Set every milestone to $0, or give every milestone an amount and fund each one through a MilestoneEscrowV3 offer (cascrow_v3_offer) once the builder has joined."
81
+ },
82
+ "joined": {
83
+ "active": "Joined as Builder. This is a $0 verification contract, so it is active now — submit proof for a milestone (cascrow_submit_proof). No money is held or moved.",
84
+ "awaitingOffer": "Joined as Builder. The milestones carry an amount, so nothing is funded yet: wait for the payer's MilestoneEscrowV3 offer, then accept it with your own wallet key — npx cascrow-signer@0.1.0 accept --milestone <milestoneId>, or on the contract page. cascrow never signs for you. Submit proof once the payer has funded the milestone.",
85
+ "unknown": "Joined as Builder. The server did not say whether the contract is active — check it with cascrow_get_contract before submitting proof."
86
+ },
87
+ "payeeHints": {
88
+ "CLAIM_PROOF_UNKNOWN": "This manifest is not a sealed submission of this milestone, so cascrow will not confirm it and nothing was signed. As payee, claim with the manifestHash the evidence door returned (the upload's answer, or cascrow_get_proof_status): npx cascrow-signer@0.1.0 submit-claim --milestone <milestoneId> --proof-hash <manifestHash>."
89
+ },
90
+ "fundRemoved": "cascrow_fund_milestone was removed (and with it fundMilestone in cascrow-sdk and `cascrow fund`). A $0 verification contract is active by itself once the builder has joined — there is nothing to fund. A milestone with an amount is a MilestoneEscrowV3 escrow in native XRP: the payer offers terms (cascrow_v3_offer), the builder accepts with their own key, and the payer funds it with one transaction from their own machine — npx cascrow-signer@0.1.0 fund --milestone <milestoneId>. cascrow never signs or sends for you.",
91
+ "recommendation": "🧭 AI RECOMMENDATION {verdict} for this MilestoneEscrowV3 submission ({confidence}% confidence on this file) — this verdict moves NO money. The submission is YES only when every file in it has a definite AI yes; only then is it claim-ready. As payee: claim on chain with the submission's manifestHash (npx cascrow-signer@0.1.0 submit-claim --milestone <milestoneId> --proof-hash <manifestHash>). As payer: release, or object before the objection deadline.",
92
+ "reportTxOnlyHash": "cascrow_v3_report_tx takes a transaction HASH (0x + 64 hex characters) and nothing else. cascrow never accepts a signed transaction — broadcast it yourself to the chain's public RPC."
93
+ };
94
+ // END v3-client-text
95
+ const TX_HASH_RE = /^0x[0-9a-fA-F]{64}$/;
96
+ function v3HintFor(code, body) {
97
+ if (!code) return null;
98
+ // Role-aware (post-review H9): CLAIM_PROOF_UNKNOWN without objectionDeadline comes from
99
+ // prepare?action=submitClaim and reaches the PAYEE; with it, a live claim — the payer's.
100
+ const b = body && typeof body === "object" ? body : null;
101
+ const liveClaim = !!b && typeof b.objectionDeadline === "string";
102
+ if (b && !liveClaim && V3_CLIENT_TEXT.payeeHints[code]) return V3_CLIENT_TEXT.payeeHints[code];
103
+ return V3_CLIENT_TEXT.codeHints[code] || null;
104
+ }
105
+
106
+ /** The fields of a V3 refusal body its recipient acts on, as one line (post-review G5) — the owner's rule. */
107
+ function v3RefusalDetail(body) {
108
+ if (!body || typeof body !== "object") return "";
109
+ const out = [];
110
+ if (typeof body.remedy === "string" && body.remedy) out.push(body.remedy);
111
+ if (Array.isArray(body.allowed)) out.push(`Allowed now: ${body.allowed.length ? body.allowed.map(String).join(", ") : "nothing"}.`);
112
+ if (typeof body.objectionDeadline === "string" && body.objectionDeadline) out.push(`Objection deadline: ${body.objectionDeadline}.`);
113
+ if (typeof body.offerExpiresAt === "string" && body.offerExpiresAt) out.push(`Open offer expires: ${body.offerExpiresAt}.`);
114
+ return out.join(" ");
115
+ }
116
+ // `submissionVerdict` is the SUBMISSION's verdict (YES / NO / PENDING), never the
117
+ // panel's `decision` on one file — a file at YES 70 % is a NO for the claim. A body
118
+ // without it reads as PENDING. Mirrors v3RecommendationLine in src/lib/v3-client-text.ts.
119
+ /**
120
+ * The line for a `POST /api/contracts/join` answer — same rule as `joinedLine` in
121
+ * src/lib/v3-client-text.ts (v3-client-text-parity.test.ts runs this copy against it).
122
+ */
123
+ function joinedLine(body) {
124
+ const activated = body && typeof body === "object" ? body.activated : undefined;
125
+ if (activated === true) return V3_CLIENT_TEXT.joined.active;
126
+ if (activated === false) return V3_CLIENT_TEXT.joined.awaitingOffer;
127
+ return V3_CLIENT_TEXT.joined.unknown;
128
+ }
129
+
130
+ function v3RecommendationLine(submissionVerdict, confidence) {
131
+ return V3_CLIENT_TEXT.recommendation.replace("{verdict}", submissionVerdict ?? "PENDING").replace("{confidence}", String(confidence ?? 0));
132
+ }
133
+
31
134
  if (ANONYMOUS_MODE) {
32
135
  process.stderr.write(
33
136
  "NOTE: CASCROW_API_KEY is not set — running in anonymous mode. " +
@@ -125,8 +228,9 @@ async function fetchJson(method, path, { json, form, deadlineMs = REQUEST_CLIENT
125
228
  const retryAfterSeconds = retryAfterOf(res, data);
126
229
  const base = typeof data?.error === "string" && data.error ? data.error : `HTTP ${res.status}`;
127
230
  // A platform error page on a POST (typically the 504 when a route hits its own
128
- // maxDuration — escrow-fund's is 120 s) does not mean the call failed: the function
129
- // may have moved money before it was killed. Say so, and how to check.
231
+ // maxDuration — contracts/review settles on chain inside its own) does not mean the
232
+ // call failed: the function may have moved money before it was killed. Say so, and
233
+ // how to check.
130
234
  const mayHaveCompleted = method !== "GET" && res.status >= 500 && data.code === "UPSTREAM_NON_JSON";
131
235
  throw new CascrowApiError(
132
236
  `${base}${code ? ` [${code}]` : ""}${retryAfterSeconds !== null && (res.status === 429 || res.status === 503) ? ` — retry after ${retryAfterSeconds} s` : ""}` +
@@ -382,7 +486,11 @@ const TOOLS = [
382
486
  enum: ["AUTO", "MANUAL", "MANUAL_AUTO"],
383
487
  description:
384
488
  "What happens when the AI panel is unsure (confidence 60–85). AUTO (default) rejects; MANUAL waits for your decision " +
385
- "via cascrow_review_contract; MANUAL_AUTO waits 48 h for your decision, then approves.",
489
+ "via cascrow_review_contract, and completes the milestone automatically after 14 days without one; MANUAL_AUTO waits 48 h for your decision, then completes it. " +
490
+ "AUTO also completes a milestone after 48 h without your decision when it reaches your review anyway — a rejection overturned on appeal, or a panel outage after an earlier real verdict. " +
491
+ "On either 48-hour mode a milestone the panel never judged (every run timed out) is not completed; it waits for your decision. " +
492
+ "The window is counted from the email notice to you, never before the milestone was put to you (a panel verdict, an escalation after the builder used up their resubmissions, or a rejection overturned on appeal); without a sent notice it does not start. " +
493
+ "Contracts without an amount only, and nothing moves: a milestone completed this way reports decidedBy \"sla\".",
386
494
  },
387
495
  milestones: {
388
496
  type: "array",
@@ -425,7 +533,7 @@ const TOOLS = [
425
533
  },
426
534
  amountUSD: {
427
535
  type: "number",
428
- description: "Optional RLUSD amount to lock in escrow (e.g. 1.0). Omit or set to 0 for verification-only.",
536
+ description: "Optional. Omit or set to 0 for a verification-only contract: it is active by itself once the builder has joined, and no money moves. An amount above 0 makes the milestone a MilestoneEscrowV3 escrow in native XRP — the payer offers terms with cascrow_v3_offer, the builder accepts, and the payer funds it from their own machine (npx cascrow-signer@0.1.0 fund).",
429
537
  },
430
538
  },
431
539
  },
@@ -433,41 +541,6 @@ const TOOLS = [
433
541
  },
434
542
  },
435
543
  },
436
- {
437
- name: "cascrow_fund_milestone",
438
- description:
439
- "Fund (activate) a milestone so it can receive proof. " +
440
- "Call this after creating the contract and before submitting proof for a milestone.",
441
- inputSchema: {
442
- type: "object",
443
- required: ["contractId"],
444
- properties: {
445
- contractId: { type: "string", description: "The contract ID returned by cascrow_create_contract" },
446
- milestoneId: { type: "string", description: "Optional specific milestone ID. If omitted, funds the first available milestone." },
447
- },
448
- },
449
- },
450
- {
451
- name: "cascrow_escrow_fund",
452
- description:
453
- "Fund a milestone with REAL on-chain RLUSD escrow using the agent's own EVM private key. " +
454
- "This does a real approve + fundMilestone transaction on the XRPL EVM Sidechain — " +
455
- "funds are locked in the smart contract and released automatically after AI verification. " +
456
- "Use this instead of cascrow_fund_milestone when you want real escrow (not just verification).",
457
- inputSchema: {
458
- type: "object",
459
- // `amountUSD` was REQUIRED and is not read by the route — the escrowed
460
- // amount is the milestone's own. See the same correction in
461
- // src/app/api/mcp/route.ts.
462
- required: ["contractId", "agentPrivateKey"],
463
- properties: {
464
- contractId: { type: "string", description: "The contract ID to fund" },
465
- agentPrivateKey: { type: "string", description: "The agent's EVM private key (hex, 0x-prefixed). The agent's wallet must hold enough RLUSD." },
466
- // Intentionally absent: the route ignores a body amountUSD.
467
- milestoneId: { type: "string", description: "Optional specific milestone ID. If omitted, funds the first available milestone." },
468
- },
469
- },
470
- },
471
544
  {
472
545
  name: "cascrow_submit_proof",
473
546
  description:
@@ -527,7 +600,9 @@ const TOOLS = [
527
600
  description:
528
601
  "Join an existing contract as the Builder (the party that delivers the work). " +
529
602
  "Use this when another agent or user has created a contract and shared an invite code with you. " +
530
- "Once joined, the contract moves to AWAITING_ESCROW and you can receive proof submissions.",
603
+ "The answer says whether the contract is active now (activated: true — a $0 verification contract; submit proof right away) " +
604
+ "or waits for the payer's MilestoneEscrowV3 offer (activated: false — accept it with your own wallet key: " +
605
+ "npx cascrow-signer@0.1.0 accept --milestone <id>, then submit proof once the payer has funded it).",
531
606
  inputSchema: {
532
607
  type: "object",
533
608
  required: ["inviteCode"],
@@ -579,7 +654,8 @@ const TOOLS = [
579
654
  name: "cascrow_get_reputation",
580
655
  description:
581
656
  "Look up the on-chain reputation of any agent by their EVM wallet address. " +
582
- "Returns milestones completed, milestones failed, success rate and total RLUSD released — " +
657
+ "Returns milestones completed, milestones failed, success rate and the value released to them — " +
658
+ "totalReleasedUsd, plus totalXrpReleased for MilestoneEscrowV3 escrows (totalRlusdReleased is deprecated: the retired v2 escrow only) — " +
583
659
  "counted over EVERY eligible milestone. " +
584
660
  "The `milestones` array is different: it carries only the records the builder has " +
585
661
  "published (per-milestone opt-in, default private), each with its XRPL tx hashes as " +
@@ -673,17 +749,109 @@ const TOOLS = [
673
749
  description:
674
750
  "As the FUNDER, list the contracts on which you still owe a manual verdict. " +
675
751
  "This is the pull behind the manual_review.required webhook: a webhook you missed is a decision " +
676
- "that never gets made, and cascrow auto-approves an undecided milestone after 48 hours and releases " +
677
- "the escrow to the builder. Poll this whenever you have funded work in flight — after a restart above all. " +
678
- "Each entry carries reviewMilestoneId; pass it to cascrow_review_contract.",
752
+ "that never gets made — cascrow completes an undecided milestone when its review window closes: 48 hours under review mode MANUAL_AUTO or AUTO, 14 days otherwise. " +
753
+ "These are verification-only ($0) contracts: that completion moves no money. Poll this whenever you have work in flight — after a restart above all. " +
754
+ "Each entry carries reviewMilestoneId; pass it to cascrow_review_contract. " +
755
+ "MilestoneEscrowV3 claims are NOT decided here: when v3ClaimsPending is above 0, list them with cascrow_v3_claims " +
756
+ "and object on chain before each objectionDeadline — cascrow cannot object for you.",
679
757
  inputSchema: { type: "object", properties: {} },
680
758
  },
759
+ {
760
+ name: "cascrow_v3_claims",
761
+ description:
762
+ "As the PAYER of a MilestoneEscrowV3 escrow, list the claims that release your escrow to the builder unless YOUR wallet " +
763
+ "calls object() on chain before objectionDeadline. This is the pull behind the claim.submitted webhook — a veto with a " +
764
+ "clock must never depend on one delivery. cascrow cannot object for you: it holds no key of yours and relays nothing. " +
765
+ "objectable=false means the window has closed. If the index is not current the call fails and says so; then read " +
766
+ "getEscrow / ClaimSubmitted from the chain directly.",
767
+ inputSchema: { type: "object", properties: {} },
768
+ },
769
+ {
770
+ name: "cascrow_v3_offer",
771
+ description:
772
+ "As the PAYER, offer MilestoneEscrowV3 terms for a milestone (native XRP, one escrow per milestone). cascrow prices it from two XRP/USD sources, freezes the terms " +
773
+ "and notifies the builder, who accepts with their own key. reviewPeriodSec is your objection window after a claim: 24 h only with watcherDeclared=true " +
774
+ "(you declare that you run a watcher, e.g. npx cascrow-signer@0.1.0 watch), otherwise at least 72 h; a first job with a new builder gets 7 days. " +
775
+ "An arbiter is optional — without one an objection costs nothing and the money returns to you after the deadline. Answers OFFER_EXISTS with the open offer's expiry.",
776
+ inputSchema: {
777
+ type: "object",
778
+ required: ["milestoneId", "reviewPeriodSec", "watcherDeclared"],
779
+ properties: {
780
+ milestoneId: { type: "string" },
781
+ reviewPeriodSec: { type: "integer", minimum: 3600, description: "Objection window after each claim, in seconds." },
782
+ watcherDeclared: { type: "boolean", description: "true only if you run a watcher that objects in time; allows a 24 h window." },
783
+ arbiter: { type: "string", description: "Optional arbiter address (0x…). Never cascrow, never a party." },
784
+ arbitrationPeriodSec: { type: "integer", description: "With an arbiter: how long they have to decide after an objection." },
785
+ },
786
+ },
787
+ },
788
+ {
789
+ name: "cascrow_v3_acceptance_get",
790
+ description:
791
+ "As the BUILDER (payee), read the payer's frozen MilestoneEscrowV3 offer: the EIP-712 typed data to sign, escrowId, payer, amount in wei, the rate snapshot and acceptBy. " +
792
+ "Check every term yourself before signing — your signature binds you until acceptBy and cascrow cannot revoke it. " +
793
+ "npx cascrow-signer@0.1.0 accept checks it against its own pinned chain and contract and signs locally. Answers V3_NOT_OFFERED while the payer has not offered.",
794
+ inputSchema: { type: "object", required: ["milestoneId"], properties: { milestoneId: { type: "string" } } },
795
+ },
796
+ {
797
+ name: "cascrow_v3_acceptance_submit",
798
+ description:
799
+ "As the BUILDER, hand cascrow your acceptance signature (EIP-712 over exactly the terms from cascrow_v3_acceptance_get, signed by your linked wallet). " +
800
+ "This is the only signature of yours cascrow ever receives besides the wallet-link one. The payer is then notified to fund before acceptBy.",
801
+ inputSchema: {
802
+ type: "object",
803
+ required: ["offerId", "signature"],
804
+ properties: { offerId: { type: "string" }, signature: { type: "string", description: "0x-prefixed 65-byte EIP-712 signature." } },
805
+ },
806
+ },
807
+ {
808
+ name: "cascrow_v3_prepare",
809
+ description:
810
+ "Get the UNSIGNED MilestoneEscrowV3 transaction for an action: { chainId, to, value, data, call: { fn, args }, escrowId, action, role, expect }. " +
811
+ "cascrow never signs, sends or relays it. Before signing, rebuild data from the contract ABI yourself and check to/chainId against your OWN configuration — " +
812
+ "npx cascrow-signer@0.1.0 does all of that and sends to the public RPC. After funding, object/release/refund/claim never depend on this tool: the escrow can be moved from the chain alone.",
813
+ inputSchema: {
814
+ type: "object",
815
+ required: ["milestoneId", "action"],
816
+ properties: {
817
+ milestoneId: { type: "string" },
818
+ action: { type: "string", enum: ["fund", "submitClaim", "claim", "release", "object", "refund", "extendDeadline", "returnFunds", "withdraw"] },
819
+ proofHash: { type: "string", description: "submitClaim: the manifest hash of your evidence (0x…)." },
820
+ reasonHash: { type: "string", description: "object: keccak256 of your objection reason (0x…)." },
821
+ newDeadline: { type: "string", description: "extendDeadline: unix seconds." },
822
+ },
823
+ },
824
+ },
825
+ {
826
+ name: "cascrow_v3_report_tx",
827
+ description:
828
+ "After YOU broadcast a MilestoneEscrowV3 transaction, tell cascrow its hash so the index re-reads the chain now instead of within five minutes. " +
829
+ "Takes a transaction HASH only — never a signed transaction; cascrow relays nothing. Reporting is optional: the index reads the chain anyway.",
830
+ inputSchema: {
831
+ type: "object",
832
+ required: ["milestoneId", "txHash"],
833
+ properties: { milestoneId: { type: "string" }, txHash: { type: "string", description: "0x + 64 hex characters." } },
834
+ },
835
+ },
836
+ {
837
+ name: "cascrow_v3_attestation",
838
+ description:
839
+ "Read cascrow's signed AI attestation for a MilestoneEscrowV3 claim — a non-binding RECOMMENDATION (verified, confidence, proofHash, reportHash), never a release. " +
840
+ "ATTESTATION_PENDING means the panel is still running: as payer you can object now. CLAIM_PROOF_UNKNOWN means the claim's proofHash matches no evidence cascrow saw.",
841
+ inputSchema: {
842
+ type: "object",
843
+ required: ["escrowId", "claimIndex"],
844
+ properties: { escrowId: { type: "string" }, claimIndex: { type: "integer", minimum: 1 } },
845
+ },
846
+ },
681
847
  {
682
848
  name: "cascrow_review_contract",
683
849
  description:
684
850
  "As the FUNDER, approve or reject a milestone the AI panel could not decide on its own " +
685
- "(confidence 60–85 lands in PENDING_REVIEW). APPROVE releases the escrow to the builder; " +
851
+ "(confidence 60–85 lands in PENDING_REVIEW). APPROVE completes the milestone; " +
686
852
  "REJECT hands the milestone back so they can resubmit stronger evidence — it is not terminal. " +
853
+ "This decides verification-only ($0) contracts and moves no money: a MilestoneEscrowV3 escrow is released or objected to " +
854
+ "on chain by the payer's own key (cascrow_v3_prepare, or npx cascrow-signer@0.1.0). " +
687
855
  "Call cascrow_review_queue first: it names the contractId and the milestoneId. " +
688
856
  "milestoneId is required once more than one milestone of that contract is pending.",
689
857
  inputSchema: {
@@ -691,7 +859,7 @@ const TOOLS = [
691
859
  required: ["contractId", "decision"],
692
860
  properties: {
693
861
  contractId: { type: "string", description: "The contract holding the milestone under review." },
694
- decision: { type: "string", enum: ["APPROVE", "REJECT"], description: "APPROVE releases the escrow; REJECT returns the milestone to the builder." },
862
+ decision: { type: "string", enum: ["APPROVE", "REJECT"], description: "APPROVE completes the milestone; REJECT returns it to the builder." },
695
863
  milestoneId: { type: "string", description: "Required when more than one milestone of the contract is pending review." },
696
864
  },
697
865
  },
@@ -717,7 +885,7 @@ const TOOLS = [
717
885
  {
718
886
  name: "cascrow_mcp_submit",
719
887
  description:
720
- "Submit text-based proof and get a verdict in the same call — the 5-model AI panel runs synchronously and takes up to ~100 seconds, plus the on-chain release on approval, so allow at least 4 minutes (the MCP default request timeout of 60 s is too short); a 409 saying an earlier submission is still being processed is usually your own timed-out call — wait the seconds it names and retry, your evidence was kept. " +
888
+ "Submit text-based proof and get a verdict in the same call — the 5-model AI panel runs synchronously and takes up to ~100 seconds, so allow at least 4 minutes (the MCP default request timeout of 60 s is too short); a 409 saying an earlier submission is still being processed is usually your own timed-out call — wait the seconds it names and retry, your evidence was kept; a 409 with code V3_ESCROW is final — the milestone is held in a MilestoneEscrowV3 escrow that only its payer, payee or arbiter moves, on chain — do not retry. " +
721
889
  "On approval it automatically releases funds on-chain and mints an NFT certificate. " +
722
890
  "Use this instead of cascrow_submit_proof + cascrow_verify when your proof is text/code (no file upload needed). " +
723
891
  "Returns verdict ('approved' | 'rejected' | 'pending_review'), confidence score, and on-chain proof URL.",
@@ -888,31 +1056,6 @@ async function handleCreateContract({ milestones, agentReviewMode }) {
888
1056
  };
889
1057
  }
890
1058
 
891
- async function handleFundMilestone({ contractId, milestoneId }) {
892
- const body = milestoneId ? { contractId, milestoneId } : { contractId };
893
- const data = await apiPost("/api/agent/fund-milestone", body);
894
- return {
895
- milestoneId: data.milestoneId,
896
- contractId: data.contractId,
897
- status: data.status,
898
- message: `Milestone funded and active. Ready to receive proof.`,
899
- };
900
- }
901
-
902
- async function handleEscrowFund({ contractId, agentPrivateKey, milestoneId }) {
903
- // No amountUSD: the route funds the milestone's own figure and drops any body
904
- // field silently. Forwarding it made the lie look answered.
905
- const data = await apiPost("/api/agent/escrow-fund", { contractId, agentPrivateKey, milestoneId });
906
- return {
907
- milestoneId: data.milestoneId,
908
- contractId: data.contractId,
909
- txHash: data.txHash,
910
- amountUSD: data.amountUSD,
911
- status: data.status,
912
- message: `✅ Real escrow funded on-chain. ${data.amountUSD} RLUSD locked in smart contract. TX: ${data.txHash}`,
913
- };
914
- }
915
-
916
1059
  async function handleSubmitProof({ milestoneId, proof, filename }) {
917
1060
  const data = await uploadProof(
918
1061
  milestoneId,
@@ -968,8 +1111,10 @@ async function handleVerify({ proofId }) {
968
1111
  // `passed: true` told an agent the escrow settled when it did not — the worst
969
1112
  // direction of error for the one consumer with no in-app inbox.
970
1113
  const complianceBlocked = result.complianceBlocked === true;
971
- // Approved, payout scheduled, funds still in escrow so the funder can contest.
972
- const releaseHoldUntil = result.releaseHoldUntil ?? null;
1114
+ // The panel approved a milestone that carries an amount without a MilestoneEscrowV3
1115
+ // escrow, so nothing completed it (Phase 6). Its own action, and a refusal: before it
1116
+ // existed this arrived as a bare VERIFIED and read as "the payout failed, retry".
1117
+ const notCompletable = action === "NOT_COMPLETABLE_WITHOUT_ESCROW";
973
1118
  // A dead panel and a genuinely borderline verdict rendered identically, and an
974
1119
  // agent's only sensible response to the second (improve the evidence, resubmit) is
975
1120
  // wasted effort against the first.
@@ -1007,8 +1152,13 @@ async function handleVerify({ proofId }) {
1007
1152
  : "";
1008
1153
 
1009
1154
  const txHash = result.txHash ?? null;
1155
+ // MilestoneEscrowV3: the verdict is a RECOMMENDATION and moves nothing. Asked
1156
+ // first, so no arm below can read it as a payout or a rejection.
1157
+ const v3Recommendation = action === "V3_RECOMMENDATION";
1010
1158
  let statusLine;
1011
- if (superseded) {
1159
+ if (v3Recommendation) {
1160
+ statusLine = v3RecommendationLine(result.submissionVerdict, confidence);
1161
+ } else if (superseded) {
1012
1162
  // `settledAs` names a terminal row (e.g. EXPIRED) the verdict lost to, not a
1013
1163
  // competing proof — "another proof won" would send the agent to resubmit.
1014
1164
  const settledAs = result.settledAs;
@@ -1017,18 +1167,19 @@ async function handleVerify({ proofId }) {
1017
1167
  : `ℹ️ SUPERSEDED — another proof on this milestone was decided first, so this verification did not change its state. Your evidence was recorded. Check the milestone status before resubmitting.`;
1018
1168
  } else if (released) {
1019
1169
  statusLine = alreadyReleased
1020
- ? `✅ COMPLETED — this milestone was already released by another request. The funds are out; do not resubmit.`
1021
- : `✅ COMPLETED (${confidence}% confidence) — funds released${txHash ? `, tx ${txHash}` : ""}.`;
1170
+ ? `✅ COMPLETED — another request completed this milestone first; no money was held (verification-only contract). Do not resubmit.`
1171
+ : `✅ COMPLETED (${confidence}% confidence) — no money was held (verification-only contract)${txHash ? `, tx ${txHash}` : ""}.`;
1022
1172
  } else if (complianceBlocked) {
1023
1173
  statusLine = `⏸️ VERIFIED (${confidence}%) — payout on hold pending compliance review. No funds moved.`;
1024
- } else if (releaseHoldUntil) {
1025
- statusLine = `⏸️ VERIFIED (${confidence}% confidence) — approved, payout scheduled for ${releaseHoldUntil}. NO FUNDS HAVE MOVED YET: the funder can contest until then, and the date can move.`;
1174
+ } else if (notCompletable) {
1175
+ statusLine = `⛔ NOT COMPLETED (${confidence}% confidence) — ${V3_CLIENT_TEXT.codeHints.NOT_COMPLETABLE_WITHOUT_ESCROW}`;
1026
1176
  } else if (passed) {
1027
1177
  statusLine = `✅ VERIFIED (${confidence}% confidence)`;
1028
1178
  } else if (action === "VERIFIED") {
1029
- // Approved, release attempted, release failed — the only way a BARE VERIFIED
1030
- // reaches a consumer.
1031
- statusLine = `⚠️ VERIFIED (${confidence}% confidence) — the work was approved, but the automatic payout did NOT go through. No funds have moved. The milestone is left at VERIFIED so the release can be retried or done by the receiver.`;
1179
+ // Approved, but no completion followed. Since Phase 6 every completion outcome
1180
+ // answers its own action, so the producer does not reach this today — it says so
1181
+ // rather than promising a retry that no longer exists.
1182
+ statusLine = `⚠️ VERIFIED (${confidence}% confidence) — the work was approved, but the milestone was NOT completed. No money moved. Check it with cascrow_get_contract; if it stays VERIFIED, contact support@cascrow.com.`;
1032
1183
  } else if (panelUnavailable) {
1033
1184
  statusLine = `⏳ PENDING_REVIEW — the verification panel did not answer (too few models responded). This is NOT a rejection and NOT a judgement of the work; a human decides. Resubmitting the same evidence will not help while the panel is down.`;
1034
1185
  } else if (pendingReview) {
@@ -1048,6 +1199,11 @@ async function handleVerify({ proofId }) {
1048
1199
  decision,
1049
1200
  confidence,
1050
1201
  action,
1202
+ v3Recommendation,
1203
+ // MilestoneEscrowV3: the claim-relevant fields, structured as well as rendered.
1204
+ ...(v3Recommendation
1205
+ ? { manifestHash: result.manifestHash ?? null, submissionVerdict: result.submissionVerdict ?? "PENDING", claimReady: result.claimReady === true }
1206
+ : {}),
1051
1207
  passed,
1052
1208
  pendingReview,
1053
1209
  // Returned as well as rendered: a client that reads the structured fields rather
@@ -1060,7 +1216,7 @@ async function handleVerify({ proofId }) {
1060
1216
  // Returned as well as rendered, as the hosted route does: with SUPERSEDED it says
1061
1217
  // the row was CLOSED rather than won by another proof.
1062
1218
  ...(result.settledAs ? { settledAs: result.settledAs } : {}),
1063
- releaseHoldUntil,
1219
+ notCompletableWithoutEscrow: notCompletable,
1064
1220
  reasoning,
1065
1221
  totalModels: TOTAL_MODELS,
1066
1222
  modelsResponded: votes.length,
@@ -1078,9 +1234,14 @@ async function handleGetContract({ contractId }) {
1078
1234
 
1079
1235
  async function handleJoinContract({ inviteCode }) {
1080
1236
  const data = await apiPost("/api/contracts/join", { inviteCode });
1237
+ // `activated` is the join's own activation result: a $0 contract is active now, a
1238
+ // money contract waits for its V3 offer. It said "AWAITING_ESCROW — ready to receive
1239
+ // proof once funded" for both, which sent a $0 builder waiting for money that never comes.
1081
1240
  return {
1082
1241
  contractId: data.contractId,
1083
- message: `Joined contract as Builder. Contract is now AWAITING_ESCROW — ready to receive proof once funded.`,
1242
+ status: data.status ?? null,
1243
+ activated: typeof data.activated === "boolean" ? data.activated : null,
1244
+ message: joinedLine(data),
1084
1245
  };
1085
1246
  }
1086
1247
 
@@ -1119,9 +1280,8 @@ async function handleCheckInvites() {
1119
1280
  // counterparty wrote about it: `name`, `skills` (PATCH /api/agent/settings,
1120
1281
  // 20 x 50 chars, no charset restriction) and a milestone `summary`. These tools
1121
1282
  // hand that straight into a DIFFERENT principal's model context while it is
1122
- // choosing who to hire — a model whose session may also hold
1123
- // `cascrow_escrow_fund`. Unmarked, cascrow is the transport for whatever the
1124
- // text says.
1283
+ // choosing who to hire and about to fund them. Unmarked, cascrow is the
1284
+ // transport for whatever the text says.
1125
1285
  //
1126
1286
  // Why it is copied here rather than imported: this package ships to npm with
1127
1287
  // one dependency (@modelcontextprotocol/sdk) and cannot reach into the web
@@ -1275,7 +1435,7 @@ async function handleGetReputation({ walletAddress }) {
1275
1435
  .filter(([, hash]) => hash)
1276
1436
  .map(([k, hash]) => `${k}: ${xrplTxUrl(hash)}`)
1277
1437
  .join(", ");
1278
- return ` - ${m.summary ?? "Completed milestone"} ($${m.amountUSD} RLUSD)${proofLinks ? ` — ${proofLinks}` : ""}`;
1438
+ return ` - ${m.summary ?? "Completed milestone"} ($${m.amountUSD})${proofLinks ? ` — ${proofLinks}` : ""}`;
1279
1439
  }).join("\n");
1280
1440
 
1281
1441
  // An empty `milestones` array has two different causes and the agent has to be
@@ -1294,7 +1454,7 @@ async function handleGetReputation({ walletAddress }) {
1294
1454
  message: [
1295
1455
  `Agent reputation for ${walletAddress}:`,
1296
1456
  ` Completed: ${stats.milestonesCompleted} milestones`,
1297
- ` RLUSD released: $${stats.totalRlusdReleased}`,
1457
+ releasedLine(stats),
1298
1458
  ` Success rate: ${successPct}`,
1299
1459
  skills.length > 0 ? ` Skills: ${skills.join(", ")}` : null,
1300
1460
  milestones.length > 0 ? `\nPublished milestones:\n${milestoneLines}` : noPublishedNote,
@@ -1302,6 +1462,17 @@ async function handleGetReputation({ walletAddress }) {
1302
1462
  };
1303
1463
  }
1304
1464
 
1465
+ /**
1466
+ * The value released to the builder (owner decision 18, 2026-09-26): `totalReleasedUsd`
1467
+ * over every escrow version, the XRP a MilestoneEscrowV3 escrow paid next to it.
1468
+ * `totalRlusdReleased` is deprecated — the retired v2 escrow only — and is the
1469
+ * fallback for a server that predates the new field. Same line as the hosted server.
1470
+ */
1471
+ function releasedLine(stats) {
1472
+ const xrp = stats.totalXrpReleased && Number(stats.totalXrpReleased) > 0 ? ` (${stats.totalXrpReleased} XRP from MilestoneEscrowV3 escrows)` : "";
1473
+ return ` Released to this builder: $${stats.totalReleasedUsd ?? stats.totalRlusdReleased}${xrp}`;
1474
+ }
1475
+
1305
1476
  async function handleDiscoverAgents({ skill, minRate, limit } = {}) {
1306
1477
  const params = new URLSearchParams();
1307
1478
  if (skill) params.set("skill", skill);
@@ -1371,7 +1542,9 @@ async function handleSetDiscoverable({ discoverable, skills, callbackUrl } = {})
1371
1542
  *
1372
1543
  * The queue exists because a push without a pull is not a channel: an agent that
1373
1544
  * was down when `manual_review.required` fired has no way back to the decision,
1374
- * and `cron/cancel-expired` auto-approves it after 48 h and releases the escrow.
1545
+ * and `cron/cancel-expired` completes it when its review window closes — 48 h
1546
+ * under MANUAL_AUTO/AUTO, 14 days otherwise (a verification-only contract: no
1547
+ * money moves, and nobody decided it — `decidedBy: "sla"`).
1375
1548
  * The verdict ships in the same change rather than later, because a list its
1376
1549
  * recipient cannot act on is the shape this project has already been burned by
1377
1550
  * (F-72). This server and the hosted one at `src/app/api/mcp/route.ts` carry both
@@ -1380,9 +1553,12 @@ async function handleSetDiscoverable({ discoverable, skills, callbackUrl } = {})
1380
1553
  async function handleReviewQueue() {
1381
1554
  const data = await apiGet("/api/agent/review-queue");
1382
1555
  const contracts = data.contracts ?? [];
1556
+ // The V3 hint is said in BOTH branches: an empty v2 queue is exactly when an
1557
+ // agent stops reading, and a claim window keeps running (post-review of 6faf3fdc).
1558
+ const v3 = v3ClaimsHint(data.v3ClaimsPending);
1383
1559
 
1384
1560
  if (contracts.length === 0) {
1385
- return { contracts: [], total: 0, truncated: false, message: "No milestones are waiting for your verdict." };
1561
+ return { ...data, contracts: [], total: 0, truncated: false, message: `No milestones are waiting for your verdict.${v3}` };
1386
1562
  }
1387
1563
 
1388
1564
  // The titles arrive already marked as counterparty-written by the route
@@ -1395,16 +1571,92 @@ async function handleReviewQueue() {
1395
1571
  : pending.length > 1
1396
1572
  ? ` — ${pending.length} milestones pending; name one with milestoneId`
1397
1573
  : " — contract-level review (omit milestoneId)";
1398
- return ` - ${c.contractId} (${c.amountUSD} RLUSD) ${c.projectTitle}${ms}`;
1574
+ return ` - ${c.contractId} ${c.projectTitle}${ms}`;
1399
1575
  }).join("\n");
1400
1576
 
1401
1577
  const more = data.truncated ? `\n\n(${data.total} in total — this list is capped.)` : "";
1402
1578
  return {
1403
1579
  ...data,
1404
- message: `${data.total} contract(s) waiting for your verdict:\n${lines}${more}\n\nDecide with cascrow_review_contract.`,
1580
+ message: `${data.total} contract(s) waiting for your verdict:\n${lines}${more}\n\nDecide with cascrow_review_contract.${v3}`,
1581
+ };
1582
+ }
1583
+
1584
+ function v3ClaimsHint(n) {
1585
+ if (n === null) return "\n\nOpen MilestoneEscrowV3 claims could not be counted — check cascrow_v3_claims.";
1586
+ if (typeof n === "number" && n > 0) return `\n\n${n} MilestoneEscrowV3 claim(s) can still be objected to — list them with cascrow_v3_claims.`;
1587
+ return "";
1588
+ }
1589
+
1590
+ /**
1591
+ * The payer's open MilestoneEscrowV3 claims — `GET /api/agent/v3-claims`, the
1592
+ * same owner the review-queue count asks (design Rev. 8 final C). Pull only: the
1593
+ * action is the payer's own `object()` on chain (rule I9). The hosted twin is
1594
+ * `handleV3Claims` in `src/app/api/mcp/route.ts`.
1595
+ */
1596
+ async function handleV3Claims() {
1597
+ const data = await apiGet("/api/agent/v3-claims");
1598
+ const claims = data.claims ?? [];
1599
+ if (claims.length === 0) return { ...data, claims: [], message: "No MilestoneEscrowV3 claim is waiting on you." };
1600
+ const lines = claims
1601
+ .map((c) => ` - escrow ${c.escrowId} on ${c.chainId}/${c.escrowContract} — ${c.milestoneTitle}, claim ${c.claimIndex}` +
1602
+ `${c.secondFunding ? " (SECOND funding)" : ""}${c.held ? " (held by the indexer — verify on chain)" : ""}: ` +
1603
+ (c.objectable === null ? "claim state unknown here — read getEscrow on chain" : c.objectable ? `object() before ${c.objectionDeadline}` : `window closed at ${c.objectionDeadline}`))
1604
+ .join("\n");
1605
+ const more = data.truncated ? "\n\n(This list is capped; more claims exist.)" : "";
1606
+ return { ...data, message: `${claims.length} claim(s):\n${lines}${more}\n\ncascrow cannot object for you — your own wallet calls object(escrowId) on the escrow contract.` };
1607
+ }
1608
+
1609
+ // ── MilestoneEscrowV3, remote half (design Rev. 10). cascrow prepares and reads; the
1610
+ // party signs and sends with its own key — nothing here signs, sends or relays. The
1611
+ // hosted twins are the handleV3* functions in src/app/api/mcp/route.ts.
1612
+ async function handleV3Offer(args) {
1613
+ const data = await apiPost("/api/escrow/v3/offer", args);
1614
+ return { ...data, message: "Offer frozen and sent to the builder. They accept with their own key; you then fund with one transaction from your machine: npx cascrow-signer@0.1.0 fund --milestone " + args.milestoneId + "." };
1615
+ }
1616
+
1617
+ async function handleV3AcceptanceGet({ milestoneId }) {
1618
+ const data = await apiGet(`/api/escrow/v3/acceptance?milestoneId=${encodeURIComponent(milestoneId)}`);
1619
+ return {
1620
+ ...data,
1621
+ message:
1622
+ "These are the payer's frozen terms. Check them yourself — your signature binds you until acceptBy, and nothing in cascrow revokes it. " +
1623
+ "npx cascrow-signer@0.1.0 accept --milestone " + milestoneId + " checks them against its own pinned chain and contract and signs on your machine.",
1624
+ };
1625
+ }
1626
+
1627
+ async function handleV3AcceptanceSubmit({ offerId, signature }) {
1628
+ const data = await apiPost("/api/escrow/v3/acceptance", { offerId, signature });
1629
+ return { ...data, message: "Acceptance recorded. The payer can now fund these exact terms until acceptBy." };
1630
+ }
1631
+
1632
+ async function handleV3Prepare({ milestoneId, action, proofHash, reasonHash, newDeadline }) {
1633
+ const q = new URLSearchParams({ milestoneId, action });
1634
+ if (proofHash) q.set("proofHash", proofHash);
1635
+ if (reasonHash) q.set("reasonHash", reasonHash);
1636
+ if (newDeadline) q.set("newDeadline", String(newDeadline));
1637
+ const data = await apiGet(`/api/escrow/v3/prepare?${q.toString()}`);
1638
+ return {
1639
+ ...data,
1640
+ message:
1641
+ "UNSIGNED transaction — cascrow does not sign, send or relay it. Before signing: rebuild data from the contract ABI, and check to and chainId against your OWN configuration, not this answer. " +
1642
+ "Then broadcast it yourself and report the hash with cascrow_v3_report_tx. npx cascrow-signer@0.1.0 does all of this locally.",
1405
1643
  };
1406
1644
  }
1407
1645
 
1646
+ async function handleV3ReportTx({ milestoneId, txHash }) {
1647
+ // Refused BEFORE anything is sent: a signed transaction pasted here would be
1648
+ // cascrow receiving a transaction to relay, which it never does (rule I9).
1649
+ if (typeof txHash !== "string" || !TX_HASH_RE.test(txHash)) throw new CascrowApiError(V3_CLIENT_TEXT.reportTxOnlyHash, { status: 0, code: "NOT_A_TX_HASH" });
1650
+ const data = await apiPost("/api/escrow/v3/sync", { milestoneId, txHash });
1651
+ return { ...data, message: "Reported. cascrow re-reads the chain for this milestone; the transaction itself is only what the chain says it is." };
1652
+ }
1653
+
1654
+ async function handleV3Attestation({ escrowId, claimIndex }) {
1655
+ const q = new URLSearchParams({ escrowId, claimIndex: String(claimIndex) });
1656
+ const data = await apiGet(`/api/escrow/v3/attestation?${q.toString()}`);
1657
+ return { ...data, message: "A non-binding AI recommendation, signed by cascrow. It moves no money: only the payer's release, an unobjected claim running out its window, or the arbiter pays." };
1658
+ }
1659
+
1408
1660
  async function handleReviewContract({ contractId, decision, milestoneId }) {
1409
1661
  const data = await apiPost(
1410
1662
  "/api/contracts/review",
@@ -1412,10 +1664,9 @@ async function handleReviewContract({ contractId, decision, milestoneId }) {
1412
1664
  );
1413
1665
  const what =
1414
1666
  decision === "APPROVE"
1415
- ? "approved — the escrow is being released to the builder"
1667
+ ? "approved and completed (a verification-only contract: no money moves)"
1416
1668
  : "rejected — the milestone is back with the builder, who can resubmit";
1417
- const tx = data.txHash ? `\nRelease tx: ${data.txHash}` : "";
1418
- return { ...data, message: `Milestone ${what}.${tx}` };
1669
+ return { ...data, message: `Milestone ${what}.` };
1419
1670
  }
1420
1671
 
1421
1672
  async function handleListMyContracts({ role } = {}) {
@@ -1457,9 +1708,9 @@ async function handleListMyContracts({ role } = {}) {
1457
1708
  *
1458
1709
  * `decision` answers a question about the PROOF. What an agent needs is the state of
1459
1710
  * the MILESTONE, and the two come apart three ways that all used to read as plain
1460
- * "APPROVED": the funds are already out (COMPLETED), the payout is scheduled and the
1461
- * funder can still contest it (VERIFIED + releaseHoldUntil), or this proof lost to
1462
- * another one and the milestone is in someone else's terminal state.
1711
+ * "APPROVED": the milestone is already done (COMPLETED), it carries an amount nothing can
1712
+ * complete (`notCompletableWithoutEscrow`), or this proof lost to another one and the
1713
+ * milestone is in someone else's terminal state.
1463
1714
  */
1464
1715
  function proofOutcomeLine(s) {
1465
1716
  // FIRST, before `decision` is read at all: a milestone that is already terminal
@@ -1468,17 +1719,22 @@ function proofOutcomeLine(s) {
1468
1719
  // here used to print "✅ APPROVED" for money that may be back with the funder.
1469
1720
  const closed = closedMilestoneStatus(s);
1470
1721
  if (closed && closed !== "COMPLETED") return closedOutcomeLine(s, closed);
1722
+ // Before any verdict is read: an approval cannot complete this milestone (it carries
1723
+ // an amount without a MilestoneEscrowV3 escrow). A YES here is not "done" — the
1724
+ // verdict doors answered 409 NOT_COMPLETABLE_WITHOUT_ESCROW and a case is open.
1725
+ if (s.notCompletableWithoutEscrow === true) return `⛔ NOT COMPLETABLE — ${V3_CLIENT_TEXT.codeHints.NOT_COMPLETABLE_WITHOUT_ESCROW}`;
1471
1726
  // SECOND: the milestone is approved (VERIFIED) or paid (COMPLETED), and not on a YES
1472
1727
  // from this proof. Its `decision` is then NO or absent, and printing it next to
1473
1728
  // "COMPLETED — funds released" or "APPROVED — payout scheduled" read as THIS proof
1474
1729
  // having been approved.
1475
1730
  if (approvedWithoutThisVerdict(s)) return approvedWithoutThisVerdictLine(s);
1731
+ // MilestoneEscrowV3, not yet paid: a RECOMMENDATION that moves nothing, judged by
1732
+ // the SUBMISSION's verdict — never "✅ APPROVED" off this file's `decision`.
1733
+ if (s.escrowVersion === 3 && s.milestoneStatus !== "COMPLETED") return v3RecommendationLine(s.submissionVerdict, s.confidence);
1476
1734
  const head =
1477
1735
  s.milestoneStatus === "COMPLETED" || closed === "COMPLETED"
1478
- ? "✅ COMPLETED — funds released"
1479
- : s.milestoneStatus === "VERIFIED" && s.releaseHoldUntil
1480
- ? `⏸️ APPROVED — payout scheduled for ${s.releaseHoldUntil}, no funds have moved yet`
1481
- : s.decision === "YES"
1736
+ ? `✅ COMPLETED — ${s.escrowVersion === 3 ? "paid out on chain from its MilestoneEscrowV3 escrow" : "no money was held (verification-only contract)"}`
1737
+ : s.decision === "YES"
1482
1738
  ? "✅ APPROVED"
1483
1739
  : "❌ REJECTED";
1484
1740
  return `${head}${s.confidence != null ? ` (confidence: ${s.confidence}%)` : ""}`;
@@ -1515,13 +1771,25 @@ function terminalStory(reason, row) {
1515
1771
  case "CANCELLED_AFTER_APPROVAL":
1516
1772
  return "An earlier approval lost the race to the funder's on-chain cancel; the escrow was refunded to the funder.";
1517
1773
  case "DEADLINE_EXPIRED":
1518
- return "The deadline passed and the escrow was cancelled, refunding the funder.";
1774
+ return "The deadline passed before the milestone was completed; any escrow it held went back to the funder (refunding the funder is the funder's own on-chain call for a MilestoneEscrowV3 escrow).";
1519
1775
  case "RENEGOTIATION_REJECTED":
1520
- return "A requested extension was rejected and the escrow was cancelled, refunding the funder.";
1776
+ return "A requested extension was rejected and the milestone was closed; any escrow it held went back to the funder.";
1521
1777
  case "NO_FUNDED_ESCROW":
1522
1778
  return "No funded escrow existed on-chain for this milestone.";
1523
1779
  case "CONTRACT_DECLINED":
1524
1780
  return "The contract was declined or expired before anything was escrowed.";
1781
+ case "ARBITER_REFUNDED":
1782
+ return "The named arbiter decided for the funder, and the escrow was refunded to the funder.";
1783
+ case "REFUNDED_AFTER_OBJECTION_AI_NO":
1784
+ return "The funder objected to the builder's claim, the panel had not approved it, and the escrow was refunded to the funder.";
1785
+ case "REFUNDED_AFTER_OBJECTION_DISPUTED":
1786
+ return "The funder objected to a claim the panel had approved, and the escrow was refunded to the funder — recorded as a dispute, not as the builder's failure.";
1787
+ case "RETURNED_BY_PAYEE":
1788
+ return "The builder returned the escrow to the funder.";
1789
+ case "RETURNED_BEFORE_WORK":
1790
+ return "The builder returned the escrow to the funder before any work was claimed.";
1791
+ case "ACCEPTANCE_LAPSED":
1792
+ return "The builder accepted the escrow terms, but the funder did not fund them before the acceptance expired; the milestone ended with no money escrowed, and it does not count against the builder.";
1525
1793
  default:
1526
1794
  return `The reason was not recorded — check the ${row} before assuming where the funds are.`;
1527
1795
  }
@@ -1608,7 +1876,7 @@ function approvedWithoutThisVerdict(s) {
1608
1876
 
1609
1877
  /** Final: no later poll can change what this proof status says. */
1610
1878
  function nothingLeftToDecide(s) {
1611
- return closedMilestoneStatus(s) !== null || approvedWithoutThisVerdict(s);
1879
+ return closedMilestoneStatus(s) !== null || s.notCompletableWithoutEscrow === true || approvedWithoutThisVerdict(s);
1612
1880
  }
1613
1881
 
1614
1882
  /** The outcome line for `approvedWithoutThisVerdict`. It never says THIS proof was approved. */
@@ -1617,10 +1885,8 @@ function approvedWithoutThisVerdictLine(s) {
1617
1885
  const ROW = row.toUpperCase();
1618
1886
  const head =
1619
1887
  closedMilestoneStatus(s) === "COMPLETED"
1620
- ? `✅ ${ROW} COMPLETED — funds released`
1621
- : s.releaseHoldUntil
1622
- ? `⏸️ ${ROW} VERIFIED — payout scheduled for ${s.releaseHoldUntil}, no funds have moved yet (the funder can contest until then)`
1623
- : `✅ ${ROW} VERIFIED — the payout is not confirmed yet`;
1888
+ ? `✅ ${ROW} COMPLETED — ${s.escrowVersion === 3 ? "paid out on chain from its MilestoneEscrowV3 escrow" : "no money was held (verification-only contract)"}`
1889
+ : `✅ ${ROW} VERIFIED — approved, not completed yet`;
1624
1890
  const verdict =
1625
1891
  s.decision === "NO"
1626
1892
  ? `The panel did NOT approve this proof (NO${s.confidence != null ? `, confidence: ${s.confidence}%` : ""}).`
@@ -1645,7 +1911,9 @@ function approvedWithoutThisVerdictLine(s) {
1645
1911
  * reading stays.
1646
1912
  */
1647
1913
  function parkedForHuman(s) {
1648
- return Boolean(s.pending) && s.running !== true && s.milestoneStatus === "PENDING_REVIEW";
1914
+ // Never a MilestoneEscrowV3 proof (post-review L2): no human review decides one — its
1915
+ // milestone status is the chain fold's, and a PENDING_REVIEW there is no queue.
1916
+ return s.escrowVersion !== 3 && Boolean(s.pending) && s.running !== true && s.milestoneStatus === "PENDING_REVIEW";
1649
1917
  }
1650
1918
 
1651
1919
  const PARKED_MESSAGE =
@@ -1694,8 +1962,8 @@ async function handleGetProofStatus({ proofId }) {
1694
1962
  // SEC-058 / SEC-059, the polling door. `decision === "YES" ? APPROVED : REJECTED`
1695
1963
  // answers a question about the PROOF; what an agent needs is the state of the
1696
1964
  // MILESTONE, and those come apart in three ways that all read as plain "APPROVED":
1697
- // the funds are already out (COMPLETED), the payout is scheduled and the funder can
1698
- // still contest it (VERIFIED + releaseHoldUntil), or another proof owns the
1965
+ // the milestone is already done (COMPLETED), it carries an amount nothing can
1966
+ // complete (notCompletableWithoutEscrow), or another proof owns the
1699
1967
  // milestone and this verdict changed nothing (SUPERSEDED upstream, which leaves the
1700
1968
  // milestone in someone else's terminal state).
1701
1969
  const parts = [
@@ -1719,6 +1987,20 @@ async function handleMcpSubmit({ contract_id, milestone_id, evidence }) {
1719
1987
  const proofId = data.proof_id ?? null;
1720
1988
  const votes = Array.isArray(data.model_votes) ? data.model_votes : [];
1721
1989
 
1990
+ // MilestoneEscrowV3: a recommendation, never a payout — asked before every verdict
1991
+ // branch below, none of which may read it as approved or rejected.
1992
+ if (data.action === "V3_RECOMMENDATION") {
1993
+ return {
1994
+ ...markVerificationResult({ ...data, model_votes: votes }),
1995
+ verdict: "v3_recommendation",
1996
+ v3Recommendation: true,
1997
+ manifestHash: data.manifestHash ?? null,
1998
+ claimReady: data.claimReady ?? null,
1999
+ submissionVerdict: data.submissionVerdict ?? "PENDING",
2000
+ message: v3RecommendationLine(data.submissionVerdict, data.confidence ?? null),
2001
+ };
2002
+ }
2003
+
1722
2004
  // SEC-055, first divergence. `/api/mcp/submit` answers `{ ok: true, decided:
1723
2005
  // "elsewhere", … }` at status **200 with no `verdict` key** when the milestone was
1724
2006
  // settled by a concurrent request — the investor's review, or a second submission.
@@ -1740,7 +2022,6 @@ async function handleMcpSubmit({ contract_id, milestone_id, evidence }) {
1740
2022
  ...markVerificationResult({ model_votes: [] }),
1741
2023
  on_chain_url: null,
1742
2024
  proof_id: proofId,
1743
- release_hold_until: null,
1744
2025
  signed_at: null,
1745
2026
  message: settledAs
1746
2027
  ? `ℹ️ This milestone was closed (${settledAs}) before this submission could settle it, so this submission changed nothing and moved no funds itself. ` +
@@ -1768,7 +2049,6 @@ async function handleMcpSubmit({ contract_id, milestone_id, evidence }) {
1768
2049
  confidence: data.confidence ?? null,
1769
2050
  on_chain_url: null,
1770
2051
  proof_id: proofId,
1771
- release_hold_until: null,
1772
2052
  message:
1773
2053
  `⏳ PENDING_REVIEW — the verification panel could not reach a verdict on this submission ` +
1774
2054
  `(too few models answered). This is NOT a rejection and NOT a judgement of your work: ` +
@@ -1779,17 +2059,13 @@ async function handleMcpSubmit({ contract_id, milestone_id, evidence }) {
1779
2059
  }
1780
2060
 
1781
2061
  const verdict = data.verdict ?? "unknown";
1782
- // SEC-058. Returned by the route on the approved-but-held path and read by neither
1783
- // MCP surface, so both rendered "✅ APPROVED" for a milestone whose funds have not
1784
- // moved. The hold exists precisely so a funder can contest, and the agent otherwise
1785
- // finds out by polling balances — or not at all.
1786
- const releaseHoldUntil = data.release_hold_until ?? null;
2062
+ // An approved milestone that cannot complete is not a 200: the route answers 409
2063
+ // NOT_COMPLETABLE_WITHOUT_ESCROW, which `apiPost` throws and the error formatter
2064
+ // renders with its hint (`codeHints`).
1787
2065
 
1788
2066
  let statusLine;
1789
2067
  if (verdict === "approved") {
1790
- statusLine = releaseHoldUntil
1791
- ? `⏸️ APPROVED (${confidence}% confidence) — payout scheduled for ${releaseHoldUntil}. NO FUNDS HAVE MOVED YET: the funder can contest until then, and the date can move.`
1792
- : `✅ APPROVED (${confidence}% confidence)${onChainUrl ? ` — proof: ${onChainUrl}` : ""}`;
2068
+ statusLine = `✅ APPROVED (${confidence}% confidence)${onChainUrl ? ` — proof: ${onChainUrl}` : ""}`;
1793
2069
  } else if (verdict === "pending_review") {
1794
2070
  statusLine = `⏳ PENDING_REVIEW (${confidence}% confidence) — manual review triggered.`;
1795
2071
  } else {
@@ -1808,7 +2084,6 @@ async function handleMcpSubmit({ contract_id, milestone_id, evidence }) {
1808
2084
  ...markVerificationResult({ reasoning: data.reasoning ?? "", model_votes: votes }),
1809
2085
  on_chain_url: onChainUrl,
1810
2086
  proof_id: proofId,
1811
- release_hold_until: releaseHoldUntil,
1812
2087
  signed_at: data.signed_at ?? null,
1813
2088
  message: `${statusLine}${voteSummary ? `\n\n${voteSummary}` : ""}${data.reasoning ? `\n\n${excerptMarked(data.reasoning, 400)}` : ""}`,
1814
2089
  };
@@ -2018,7 +2293,7 @@ async function handleWatchVerification({ milestoneId, content, filename, proofId
2018
2293
  // ─── MCP Server ───────────────────────────────────────────────────────────────
2019
2294
 
2020
2295
  const server = new Server(
2021
- { name: "cascrow", version: "1.8.0" },
2296
+ { name: "cascrow", version: "3.0.0" },
2022
2297
  { capabilities: { tools: {} } }
2023
2298
  );
2024
2299
 
@@ -2029,12 +2304,26 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
2029
2304
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
2030
2305
  const { name, arguments: args } = request.params;
2031
2306
 
2307
+ // Removed in 2.0.0 (cascrow Phase 5): the tool sent the agent's private key to
2308
+ // the server. A host with a cached tool list may still call it. Answered before
2309
+ // `args` is touched and in anonymous mode too — the arguments may hold a key,
2310
+ // and nothing here logs, echoes or forwards them.
2311
+ if (REMOVED_CUSTODY_TOOLS.includes(name)) {
2312
+ return { content: [{ type: "text", text: CUSTODY_TOOL_REMOVED }], isError: true };
2313
+ }
2314
+ // Removed in 3.0.0 (cascrow Phase 6, owner decision 16): a $0 contract activates by
2315
+ // itself, and a milestone with an amount is a MilestoneEscrowV3 escrow its payer
2316
+ // funds from their own machine. A cached tool list still calls the old name.
2317
+ if (name === "cascrow_fund_milestone") {
2318
+ return { content: [{ type: "text", text: `Error: ${V3_CLIENT_TEXT.fundRemoved} [TOOL_REMOVED]` }], isError: true };
2319
+ }
2320
+
2032
2321
  if (ANONYMOUS_MODE && name !== "cascrow_verify_work") {
2033
2322
  return {
2034
2323
  content: [{
2035
2324
  type: "text",
2036
2325
  text: `Error: "${name}" requires a Cascrow account. Register free at https://cascrow.com/register ` +
2037
- `(or POST /api/agent/register), then set CASCROW_API_KEY to unlock all 18 tools. ` +
2326
+ `(or POST /api/agent/register), then set CASCROW_API_KEY to unlock all ${TOOLS.length} tools. ` +
2038
2327
  `Only cascrow_verify_work works anonymously (3 free verifications/day).`,
2039
2328
  }],
2040
2329
  isError: true,
@@ -2045,8 +2334,6 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2045
2334
  let result;
2046
2335
  switch (name) {
2047
2336
  case "cascrow_create_contract": result = await handleCreateContract(args); break;
2048
- case "cascrow_fund_milestone": result = await handleFundMilestone(args); break;
2049
- case "cascrow_escrow_fund": result = await handleEscrowFund(args); break;
2050
2337
  case "cascrow_submit_proof": result = await handleSubmitProof(args); break;
2051
2338
  case "cascrow_verify": result = await handleVerify(args); break;
2052
2339
  case "cascrow_get_contract": result = await handleGetContract(args); break;
@@ -2059,6 +2346,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2059
2346
  case "cascrow_set_discoverable": result = await handleSetDiscoverable(args); break;
2060
2347
  case "cascrow_list_my_contracts": result = await handleListMyContracts(args); break;
2061
2348
  case "cascrow_review_queue": result = await handleReviewQueue(); break;
2349
+ case "cascrow_v3_claims": result = await handleV3Claims(); break;
2350
+ case "cascrow_v3_offer": result = await handleV3Offer(args); break;
2351
+ case "cascrow_v3_acceptance_get": result = await handleV3AcceptanceGet(args); break;
2352
+ case "cascrow_v3_acceptance_submit": result = await handleV3AcceptanceSubmit(args); break;
2353
+ case "cascrow_v3_prepare": result = await handleV3Prepare(args); break;
2354
+ case "cascrow_v3_report_tx": result = await handleV3ReportTx(args); break;
2355
+ case "cascrow_v3_attestation": result = await handleV3Attestation(args); break;
2062
2356
  case "cascrow_review_contract": result = await handleReviewContract(args); break;
2063
2357
  case "cascrow_get_proof_status": result = await handleGetProofStatus(args); break;
2064
2358
  case "cascrow_mcp_submit": result = await handleMcpSubmit(args); break;
@@ -2072,8 +2366,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2072
2366
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
2073
2367
  };
2074
2368
  } catch (err) {
2369
+ // A V3 refusal says who can act and how (F-72) — the body's actionable fields (G5),
2370
+ // then the owner's hint, role-aware on the body (H9); the owner's words, copied above.
2371
+ const detail = err instanceof CascrowApiError ? v3RefusalDetail(err.body) : "";
2372
+ const hint = err instanceof CascrowApiError ? v3HintFor(err.code, err.body) : null;
2075
2373
  return {
2076
- content: [{ type: "text", text: `Error: ${err.message}` }],
2374
+ content: [{ type: "text", text: `Error: ${err.message}${detail && !err.message.includes(detail) ? ` — ${detail}` : ""}${hint && !err.message.includes(hint) ? `\n\n${hint}` : ""}` }],
2077
2375
  isError: true,
2078
2376
  };
2079
2377
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "cascrow-mcp",
3
- "version": "1.8.0",
4
- "description": "MCP server for Cascrow — trust infrastructure for the agent economy: AI verification, on-chain reputation, and milestone escrow on the XRP Ledger",
3
+ "version": "3.0.0",
4
+ "description": "MCP server for Cascrow — trust infrastructure for the agent economy: AI verification, on-chain reputation, and non-custodial milestone escrow (MilestoneEscrowV3, native XRP)",
5
5
  "type": "module",
6
6
  "main": "index.js",
7
7
  "bin": {
package/server.json CHANGED
@@ -9,13 +9,13 @@
9
9
  "source": "github",
10
10
  "subfolder": "cascrow-mcp"
11
11
  },
12
- "version": "1.8.0",
12
+ "version": "3.0.0",
13
13
  "packages": [
14
14
  {
15
15
  "registryType": "npm",
16
16
  "registryBaseUrl": "https://registry.npmjs.org",
17
17
  "identifier": "cascrow-mcp",
18
- "version": "1.8.0",
18
+ "version": "3.0.0",
19
19
  "transport": {
20
20
  "type": "stdio"
21
21
  },
package/smithery.yaml CHANGED
@@ -10,7 +10,7 @@ startCommand:
10
10
  properties:
11
11
  CASCROW_API_KEY:
12
12
  type: string
13
- description: "Optional. Your cascrow API key (starts with csk_) — get one at cascrow.com/register or POST /api/agent/register. Without it, the server still starts but only cascrow_verify_work is available (3 free AI-verified checks per IP per day, no account needed). Set this to unlock all 18 tools."
13
+ description: "Optional. Your cascrow API key (starts with csk_) — get one at cascrow.com/register or POST /api/agent/register. Without it, the server still starts but only cascrow_verify_work is available (3 free AI-verified checks per IP per day, no account needed). Set this to unlock every other tool."
14
14
  pattern: "^csk_"
15
15
  CASCROW_BASE_URL:
16
16
  type: string