@absol-labs/agent 0.10.0 → 0.11.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.
Files changed (141) hide show
  1. package/README.md +107 -23
  2. package/dist/capability/invocation-capability.d.ts +18 -6
  3. package/dist/capability/invocation-capability.d.ts.map +1 -1
  4. package/dist/capability/invocation-capability.js.map +1 -1
  5. package/dist/cli/commands/claim.d.ts.map +1 -1
  6. package/dist/cli/commands/claim.js +20 -4
  7. package/dist/cli/commands/claim.js.map +1 -1
  8. package/dist/cli/commands/close.d.ts.map +1 -1
  9. package/dist/cli/commands/close.js +16 -4
  10. package/dist/cli/commands/close.js.map +1 -1
  11. package/dist/cli/commands/invoke.d.ts.map +1 -1
  12. package/dist/cli/commands/invoke.js +1 -0
  13. package/dist/cli/commands/invoke.js.map +1 -1
  14. package/dist/cli/commands/open.d.ts.map +1 -1
  15. package/dist/cli/commands/open.js +63 -11
  16. package/dist/cli/commands/open.js.map +1 -1
  17. package/dist/cli/commands/reclaim-unverified.d.ts.map +1 -1
  18. package/dist/cli/commands/reclaim-unverified.js +12 -4
  19. package/dist/cli/commands/reclaim-unverified.js.map +1 -1
  20. package/dist/cli/commands/reclaim.d.ts.map +1 -1
  21. package/dist/cli/commands/reclaim.js +16 -5
  22. package/dist/cli/commands/reclaim.js.map +1 -1
  23. package/dist/cli/commands/streams.js +1 -1
  24. package/dist/cli/commands/streams.js.map +1 -1
  25. package/dist/cli/deps.d.ts +53 -7
  26. package/dist/cli/deps.d.ts.map +1 -1
  27. package/dist/cli/deps.js +65 -15
  28. package/dist/cli/deps.js.map +1 -1
  29. package/dist/cli/privy.d.ts +83 -0
  30. package/dist/cli/privy.d.ts.map +1 -0
  31. package/dist/cli/privy.js +230 -0
  32. package/dist/cli/privy.js.map +1 -0
  33. package/dist/cli/spend-policy.d.ts +60 -0
  34. package/dist/cli/spend-policy.d.ts.map +1 -0
  35. package/dist/cli/spend-policy.js +65 -0
  36. package/dist/cli/spend-policy.js.map +1 -0
  37. package/dist/cli/wallet.d.ts +108 -39
  38. package/dist/cli/wallet.d.ts.map +1 -1
  39. package/dist/cli/wallet.js +108 -43
  40. package/dist/cli/wallet.js.map +1 -1
  41. package/dist/gateway/caller-auth-gateway.d.ts +38 -3
  42. package/dist/gateway/caller-auth-gateway.d.ts.map +1 -1
  43. package/dist/gateway/caller-auth-gateway.js +25 -1
  44. package/dist/gateway/caller-auth-gateway.js.map +1 -1
  45. package/dist/gateway/http-server.d.ts +53 -0
  46. package/dist/gateway/http-server.d.ts.map +1 -1
  47. package/dist/gateway/http-server.js +189 -4
  48. package/dist/gateway/http-server.js.map +1 -1
  49. package/dist/gateway/x402-mode.d.ts +123 -0
  50. package/dist/gateway/x402-mode.d.ts.map +1 -0
  51. package/dist/gateway/x402-mode.js +212 -0
  52. package/dist/gateway/x402-mode.js.map +1 -0
  53. package/dist/index.d.ts +6 -4
  54. package/dist/index.d.ts.map +1 -1
  55. package/dist/index.js +8 -3
  56. package/dist/index.js.map +1 -1
  57. package/dist/mandates/env.d.ts +3 -2
  58. package/dist/mandates/env.d.ts.map +1 -1
  59. package/dist/mandates/env.js +3 -2
  60. package/dist/mandates/env.js.map +1 -1
  61. package/dist/mcp/server.d.ts +49 -0
  62. package/dist/mcp/server.d.ts.map +1 -1
  63. package/dist/mcp/server.js +513 -24
  64. package/dist/mcp/server.js.map +1 -1
  65. package/dist/mcp/service-invoker.d.ts +97 -0
  66. package/dist/mcp/service-invoker.d.ts.map +1 -0
  67. package/dist/mcp/service-invoker.js +167 -0
  68. package/dist/mcp/service-invoker.js.map +1 -0
  69. package/dist/sdk/invoke.d.ts +19 -4
  70. package/dist/sdk/invoke.d.ts.map +1 -1
  71. package/dist/sdk/invoke.js +69 -5
  72. package/dist/sdk/invoke.js.map +1 -1
  73. package/dist/x402/delivery-oracle.d.ts +108 -0
  74. package/dist/x402/delivery-oracle.d.ts.map +1 -0
  75. package/dist/x402/delivery-oracle.js +174 -0
  76. package/dist/x402/delivery-oracle.js.map +1 -0
  77. package/dist/x402/facilitator-server-entry.d.ts +3 -0
  78. package/dist/x402/facilitator-server-entry.d.ts.map +1 -0
  79. package/dist/x402/facilitator-server-entry.js +27 -0
  80. package/dist/x402/facilitator-server-entry.js.map +1 -0
  81. package/dist/x402/facilitator-server.d.ts +73 -0
  82. package/dist/x402/facilitator-server.d.ts.map +1 -0
  83. package/dist/x402/facilitator-server.js +305 -0
  84. package/dist/x402/facilitator-server.js.map +1 -0
  85. package/dist/x402/facilitator.d.ts +69 -17
  86. package/dist/x402/facilitator.d.ts.map +1 -1
  87. package/dist/x402/facilitator.js +76 -33
  88. package/dist/x402/facilitator.js.map +1 -1
  89. package/dist/x402/payment-requirements.d.ts +106 -0
  90. package/dist/x402/payment-requirements.d.ts.map +1 -0
  91. package/dist/x402/payment-requirements.js +138 -0
  92. package/dist/x402/payment-requirements.js.map +1 -0
  93. package/dist/x402/relayer.d.ts +41 -0
  94. package/dist/x402/relayer.d.ts.map +1 -0
  95. package/dist/x402/relayer.js +64 -0
  96. package/dist/x402/relayer.js.map +1 -0
  97. package/dist/x402/resource-index.d.ts +75 -0
  98. package/dist/x402/resource-index.d.ts.map +1 -0
  99. package/dist/x402/resource-index.js +126 -0
  100. package/dist/x402/resource-index.js.map +1 -0
  101. package/dist/x402/verified-delivery-facilitator.d.ts +285 -0
  102. package/dist/x402/verified-delivery-facilitator.d.ts.map +1 -0
  103. package/dist/x402/verified-delivery-facilitator.js +423 -0
  104. package/dist/x402/verified-delivery-facilitator.js.map +1 -0
  105. package/dist/zktls/t2-delivery-proof.d.ts +6 -6
  106. package/docs/agent-layer.md +12 -8
  107. package/docs/cli.md +158 -22
  108. package/docs/mcp-hosted.md +4 -3
  109. package/docs/mcp-reference.md +175 -0
  110. package/docs/quickstart.md +18 -4
  111. package/docs/threat-model.md +4 -3
  112. package/docs/x402.md +648 -0
  113. package/package.json +18 -7
  114. package/src/capability/invocation-capability.ts +17 -1
  115. package/src/cli/commands/claim.ts +30 -4
  116. package/src/cli/commands/close.ts +20 -4
  117. package/src/cli/commands/invoke.ts +1 -0
  118. package/src/cli/commands/open.ts +76 -10
  119. package/src/cli/commands/reclaim-unverified.ts +16 -4
  120. package/src/cli/commands/reclaim.ts +22 -5
  121. package/src/cli/commands/streams.ts +1 -1
  122. package/src/cli/deps.ts +115 -24
  123. package/src/cli/privy.ts +396 -0
  124. package/src/cli/spend-policy.ts +139 -0
  125. package/src/cli/wallet.ts +188 -57
  126. package/src/gateway/caller-auth-gateway.ts +77 -6
  127. package/src/gateway/http-server.ts +280 -6
  128. package/src/gateway/x402-mode.ts +350 -0
  129. package/src/index.ts +40 -5
  130. package/src/mandates/env.ts +3 -2
  131. package/src/mcp/server.ts +681 -29
  132. package/src/mcp/service-invoker.ts +285 -0
  133. package/src/sdk/invoke.ts +90 -12
  134. package/src/x402/delivery-oracle.ts +285 -0
  135. package/src/x402/facilitator-server-entry.ts +35 -0
  136. package/src/x402/facilitator-server.ts +445 -0
  137. package/src/x402/facilitator.ts +125 -46
  138. package/src/x402/payment-requirements.ts +231 -0
  139. package/src/x402/relayer.ts +122 -0
  140. package/src/x402/resource-index.ts +228 -0
  141. package/src/x402/verified-delivery-facilitator.ts +744 -0
package/package.json CHANGED
@@ -1,12 +1,11 @@
1
1
  {
2
2
  "name": "@absol-labs/agent",
3
- "version": "0.10.0",
4
- "description": "Metrik agent layer: x402 verified-streaming payments, an MCP server, framework tools, and spend mandates so AI agents can hire and pay verified services safely.",
3
+ "version": "0.11.0",
4
+ "description": "Metrik agent layer: verified-streaming USDC payments metered by delivery, an MCP server, framework tools, and spend mandates so AI agents can hire and pay verified services safely.",
5
5
  "license": "MIT",
6
6
  "author": "Absol Labs",
7
7
  "keywords": [
8
8
  "metrik",
9
- "x402",
10
9
  "mcp",
11
10
  "ai-agents",
12
11
  "spend-mandate",
@@ -21,7 +20,8 @@
21
20
  "metrik": "dist/cli/bin.js",
22
21
  "metrik-mcp": "dist/mcp/stdio.js",
23
22
  "metrik-mcp-http": "dist/mcp/http-server.js",
24
- "metrik-gateway": "dist/gateway/server-entry.js"
23
+ "metrik-gateway": "dist/gateway/server-entry.js",
24
+ "metrik-x402-facilitator": "dist/x402/facilitator-server-entry.js"
25
25
  },
26
26
  "exports": {
27
27
  ".": {
@@ -48,6 +48,10 @@
48
48
  "types": "./dist/frameworks/crewai.d.ts",
49
49
  "import": "./dist/frameworks/crewai.js"
50
50
  },
51
+ "./x402-facilitator": {
52
+ "types": "./dist/x402/facilitator-server.d.ts",
53
+ "import": "./dist/x402/facilitator-server.js"
54
+ },
51
55
  "./package.json": "./package.json",
52
56
  "./*": {
53
57
  "types": "./dist/*.d.ts",
@@ -92,7 +96,8 @@
92
96
  "@elizaos/core": "^1.7.2",
93
97
  "@langchain/core": "^1.2.0",
94
98
  "@reclaimprotocol/js-sdk": "^5.6.0",
95
- "@reclaimprotocol/zk-fetch": "^1.0.0"
99
+ "@reclaimprotocol/zk-fetch": "^1.0.0",
100
+ "x402": "^1.2.0"
96
101
  },
97
102
  "peerDependenciesMeta": {
98
103
  "@coinbase/agentkit": {
@@ -112,6 +117,9 @@
112
117
  },
113
118
  "@reclaimprotocol/zk-fetch": {
114
119
  "optional": true
120
+ },
121
+ "x402": {
122
+ "optional": true
115
123
  }
116
124
  },
117
125
  "devDependencies": {
@@ -126,7 +134,9 @@
126
134
  "prettier": "^3.4.0",
127
135
  "tsx": "^4.19.0",
128
136
  "typescript": "^5.7.0",
129
- "vitest": "^2.1.0"
137
+ "vitest": "^2.1.0",
138
+ "x402": "1.2.0",
139
+ "x402-fetch": "1.2.0"
130
140
  },
131
141
  "scripts": {
132
142
  "build": "tsc -p tsconfig.json",
@@ -144,6 +154,7 @@
144
154
  "typecheck:examples": "tsc -p tsconfig.examples.json --noEmit",
145
155
  "typecheck:e2e": "tsc -p tsconfig.e2e.json --noEmit",
146
156
  "lint": "tsc -p tsconfig.json --noEmit",
147
- "format": "prettier --write ."
157
+ "format": "prettier --write .",
158
+ "x402:facilitator": "tsx src/x402/facilitator-server-entry.ts"
148
159
  }
149
160
  }
@@ -172,11 +172,27 @@ export function buildInvocationCapabilityTypedData(
172
172
  };
173
173
  }
174
174
 
175
+ /**
176
+ * The minimum an account must expose to sign a capability: its address, and
177
+ * EIP-712 signing.
178
+ *
179
+ * Deliberately narrower than viem's `LocalAccount`. Every wallet this package
180
+ * resolves signs typed data — a raw key, a CDP server account via `toAccount()`,
181
+ * a Privy embedded EOA — but only the raw-key one is a `LocalAccount` by TYPE.
182
+ * Demanding the full interface would have meant the MCP server could only ever
183
+ * invoke with a raw key, which is the wallet an MCP host is least likely to be
184
+ * configured with. Widening a parameter type breaks no existing caller.
185
+ */
186
+ export type InvocationCapabilitySigner = Pick<
187
+ LocalAccount,
188
+ "address" | "signTypedData"
189
+ >;
190
+
175
191
  /** Signs an `InvocationCapability` with the buyer's own wallet. */
176
192
  export async function signInvocationCapability(
177
193
  capability: InvocationCapability,
178
194
  domain: InvocationCapabilityDomainInput,
179
- account: LocalAccount,
195
+ account: InvocationCapabilitySigner,
180
196
  ): Promise<Hex> {
181
197
  const typedData = buildInvocationCapabilityTypedData(capability, domain);
182
198
  return account.signTypedData(typedData);
@@ -3,7 +3,12 @@ import { assertTestnetRpc, parseBytes32 } from "../config.js";
3
3
  import { CliError, describeError, usageError } from "../errors.js";
4
4
  import { signClaimMandate } from "../mandate.js";
5
5
  import { assertCanPayGas } from "../preflight.js";
6
- import { PRIVATE_KEY_ENV, resolveCliWallet } from "../wallet.js";
6
+ import { writeClientOptions } from "../deps.js";
7
+ import {
8
+ isPolicyEnforced,
9
+ PRIVATE_KEY_ENV,
10
+ resolveCliWallet,
11
+ } from "../wallet.js";
7
12
  import type { CommandContext, CommandSpec } from "../context.js";
8
13
 
9
14
  /**
@@ -30,7 +35,21 @@ export const claimCommand: CommandSpec = {
30
35
  }
31
36
  const streamId = parseBytes32(streamIdArg, "<streamId>");
32
37
 
33
- const { account } = await resolveCliWallet(env, { note: out.note });
38
+ const wallet = await resolveCliWallet(env, {
39
+ note: out.note,
40
+ rpcUrl: config.rpcUrl,
41
+ });
42
+ const { account } = wallet;
43
+ if (isPolicyEnforced(wallet)) {
44
+ // Said plainly rather than discovered mid-command. `claim` is the
45
+ // OPERATOR's side, and a Privy broker policy allowlists the BUYER
46
+ // actions only (openStream, close, reclaim, reclaimUnverified, and a
47
+ // bounded USDC approval). That is a property of an operator using a
48
+ // buyer-scoped wallet, not a defect in this command.
49
+ out.note(
50
+ "warning: this wallet's broker policy allowlists buyer actions only, so it will refuse to sign an operator claim. Claim with the operator's own key.",
51
+ );
52
+ }
34
53
  const read = deps.createReadClient(config);
35
54
  await assertTestnetRpc(config, () => read.getChainId());
36
55
  await assertCanPayGas(read, account.address);
@@ -74,10 +93,17 @@ export const claimCommand: CommandSpec = {
74
93
  });
75
94
  }
76
95
 
77
- const write = deps.createWriteClient(config, account);
96
+ const write = deps.createWriteClient(
97
+ config,
98
+ account,
99
+ writeClientOptions(wallet),
100
+ );
78
101
  const result = await write.claimStream({
79
102
  streamId,
80
- signedMandate: await signClaimMandate(account, config.chainId),
103
+ // `null` for a policy-enforced wallet — see `../spend-policy.ts`.
104
+ signedMandate: isPolicyEnforced(wallet)
105
+ ? null
106
+ : await signClaimMandate(account, config.chainId),
81
107
  spentSoFarUsdc: 0n,
82
108
  nowSeconds: deps.nowSeconds(),
83
109
  });
@@ -2,7 +2,8 @@ import { assertTestnetRpc, parseBytes32 } from "../config.js";
2
2
  import { usageError } from "../errors.js";
3
3
  import { signRecoveryMandate } from "../mandate.js";
4
4
  import { assertCanPayGas } from "../preflight.js";
5
- import { resolveCliWallet } from "../wallet.js";
5
+ import { writeClientOptions } from "../deps.js";
6
+ import { isPolicyEnforced, resolveCliWallet } from "../wallet.js";
6
7
  import type { CommandContext, CommandSpec } from "../context.js";
7
8
 
8
9
  export const closeCommand: CommandSpec = {
@@ -19,13 +20,28 @@ export const closeCommand: CommandSpec = {
19
20
  }
20
21
  const streamId = parseBytes32(streamIdArg, "<streamId>");
21
22
 
22
- const { account } = await resolveCliWallet(env, { note: out.note });
23
+ const wallet = await resolveCliWallet(env, {
24
+ note: out.note,
25
+ rpcUrl: config.rpcUrl,
26
+ });
27
+ const { account } = wallet;
23
28
  const read = deps.createReadClient(config);
24
29
  await assertTestnetRpc(config, () => read.getChainId());
25
30
  await assertCanPayGas(read, account.address);
26
31
 
27
- const signedMandate = await signRecoveryMandate(account, config.chainId);
28
- const write = deps.createWriteClient(config, account);
32
+ // A policy-enforced wallet signs no mandate of its own: this is a
33
+ // buyer-recovery action, the escrow gates it on the buyer address and the
34
+ // broker gates it again on `stream.buyer == wallet`. A locally self-signed
35
+ // placeholder would add nothing and the broker would refuse to sign it.
36
+ // See `../spend-policy.ts`.
37
+ const signedMandate = isPolicyEnforced(wallet)
38
+ ? null
39
+ : await signRecoveryMandate(account, config.chainId);
40
+ const write = deps.createWriteClient(
41
+ config,
42
+ account,
43
+ writeClientOptions(wallet),
44
+ );
29
45
  const result = await write.closeStream({
30
46
  streamId,
31
47
  signedMandate,
@@ -61,6 +61,7 @@ export const invokeCommand: CommandSpec = {
61
61
  const { account } = await resolveCliWallet(env, {
62
62
  purpose: "capability",
63
63
  note: out.note,
64
+ rpcUrl: config.rpcUrl,
64
65
  });
65
66
  const read = deps.createReadClient(config);
66
67
  // No transaction is signed here — but the capability is bound to
@@ -8,10 +8,17 @@ import {
8
8
  parseUsdcAmount,
9
9
  } from "../amounts.js";
10
10
  import { assertTestnetRpc, parseBytes32 } from "../config.js";
11
+ import { writeClientOptions } from "../deps.js";
11
12
  import { CliError, configError } from "../errors.js";
13
+ import type { PrivySpendPolicy } from "../privy.js";
14
+ import {
15
+ assertWithinOperatorCaps,
16
+ assertWithinSpendPolicy,
17
+ } from "../spend-policy.js";
12
18
  import { cliMandateCaps, signCliMandate } from "../mandate.js";
13
19
  import { assertCanFundStream, assertCanPayGas } from "../preflight.js";
14
20
  import {
21
+ isPolicyEnforced,
15
22
  optionalCliWallet,
16
23
  resolveCliWallet,
17
24
  PRIVATE_KEY_ENV,
@@ -74,8 +81,8 @@ export const openCommand: CommandSpec = {
74
81
  // the intended call and says which parts (allowance, simulation, mandate
75
82
  // signature) could not be produced, rather than inventing them.
76
83
  const wallet = dryRun
77
- ? await optionalCliWallet(env, { note: out.note })
78
- : await resolveCliWallet(env, { note: out.note });
84
+ ? await optionalCliWallet(env, { note: out.note, rpcUrl: config.rpcUrl })
85
+ : await resolveCliWallet(env, { note: out.note, rpcUrl: config.rpcUrl });
79
86
  const account = wallet?.account ?? null;
80
87
  const read = deps.createReadClient(config);
81
88
  await assertTestnetRpc(config, () => read.getChainId());
@@ -101,16 +108,41 @@ export const openCommand: CommandSpec = {
101
108
  }
102
109
 
103
110
  const spend = { operator, budgetUsdc, ratePerSecondUsdc, durationSeconds };
111
+ const mandateCaps = cliMandateCaps(spend, env);
112
+
113
+ // A policy-enforced wallet (Privy) is capped by its broker, not by a
114
+ // mandate this process signs for itself. Read the REAL ceiling and hold it
115
+ // to that instead — see `../spend-policy.ts` for why that substitution is
116
+ // the stronger of the two, and why re-adding the self-signed mandate here
117
+ // would both weaken it and break the wallet outright.
118
+ const policy: PrivySpendPolicy | null =
119
+ wallet !== null && isPolicyEnforced(wallet)
120
+ ? await wallet.readSpendPolicy()
121
+ : null;
122
+ if (policy !== null && !dryRun) {
123
+ assertWithinSpendPolicy(policy, spend);
124
+ // The operator's own standing ceiling still applies, and may be TIGHTER
125
+ // than the broker's. Enforced here because no mandate is signed to carry
126
+ // it — "policy-enforced" retires the self-signed mandate, not the caps
127
+ // the operator deliberately configured.
128
+ if (mandateCaps.source === "env") {
129
+ assertWithinOperatorCaps(mandateCaps.caps, spend);
130
+ }
131
+ }
132
+
104
133
  const mandate =
105
- account === null
134
+ account === null || policy !== null
106
135
  ? null
107
136
  : await signCliMandate(account, spend, {
108
137
  chainId: config.chainId,
109
138
  env,
110
139
  });
111
- const mandateCaps = cliMandateCaps(spend, env);
112
140
 
113
- const write = deps.createWriteClient(config, account);
141
+ const write = deps.createWriteClient(
142
+ config,
143
+ account,
144
+ writeClientOptions(wallet),
145
+ );
114
146
  const hire = {
115
147
  operator,
116
148
  serviceRef,
@@ -145,6 +177,19 @@ export const openCommand: CommandSpec = {
145
177
  durationSeconds,
146
178
  mandateSource: mandateCaps.source,
147
179
  mandateSigned: mandate !== null,
180
+ spendPolicy:
181
+ policy === null
182
+ ? null
183
+ : {
184
+ brokerUrl: policy.brokerUrl,
185
+ maxTotalUsdc: policy.maxTotalUsdc,
186
+ maxRatePerSecondUsdc: policy.maxRatePerSecondUsdc,
187
+ maxDurationSeconds: policy.maxDurationSeconds,
188
+ fits:
189
+ budgetUsdc <= policy.maxTotalUsdc &&
190
+ ratePerSecondUsdc <= policy.maxRatePerSecondUsdc &&
191
+ durationSeconds <= policy.maxDurationSeconds,
192
+ },
148
193
  to: preview.to,
149
194
  data: preview.data,
150
195
  chainId: preview.chainId,
@@ -180,10 +225,28 @@ export const openCommand: CommandSpec = {
180
225
  ["duration", formatDuration(durationSeconds)],
181
226
  [
182
227
  "mandate",
183
- mandate === null
184
- ? `not signed (no wallet); caps would come from ${mandateCaps.source}`
185
- : `self-signed, caps from ${mandateCaps.source}`,
228
+ mandate !== null
229
+ ? `self-signed, caps from ${mandateCaps.source}`
230
+ : policy !== null
231
+ ? "not signed — this wallet's ceiling is enforced by its broker, which is stronger"
232
+ : `not signed (no wallet); caps would come from ${mandateCaps.source}`,
186
233
  ],
234
+ ...(policy === null
235
+ ? []
236
+ : ([
237
+ [
238
+ "spend policy",
239
+ `${formatUsdc(policy.maxTotalUsdc)} total, ${policy.maxRatePerSecondUsdc} atomic/second, ${formatDuration(policy.maxDurationSeconds)} max — enforced by ${policy.brokerUrl}`,
240
+ ],
241
+ [
242
+ "fits policy",
243
+ budgetUsdc <= policy.maxTotalUsdc &&
244
+ ratePerSecondUsdc <= policy.maxRatePerSecondUsdc &&
245
+ durationSeconds <= policy.maxDurationSeconds
246
+ ? "yes"
247
+ : "NO — a real open would be refused before broadcasting",
248
+ ],
249
+ ] as [string, string][])),
187
250
  ["escrow", preview.to],
188
251
  [
189
252
  "usdc balance",
@@ -219,7 +282,7 @@ export const openCommand: CommandSpec = {
219
282
  );
220
283
  // Unreachable without a wallet: `resolveCliWallet` above throws when the
221
284
  // command is not a dry run. The narrowing keeps that a type-level fact.
222
- if (account === null || mandate === null) {
285
+ if (account === null) {
223
286
  throw configError(
224
287
  "this command signs a transaction and needs a wallet, and none was resolved",
225
288
  );
@@ -233,7 +296,10 @@ export const openCommand: CommandSpec = {
233
296
  budgetUsdc,
234
297
  ratePerSecondUsdc,
235
298
  maxDurationSeconds: durationSeconds,
236
- signedMandate: mandate.signed,
299
+ // `null` for a policy-enforced wallet: the ceiling was checked against
300
+ // the broker's own policy above, and the broker checks it again on the
301
+ // transaction it signs.
302
+ signedMandate: mandate === null ? null : mandate.signed,
237
303
  spentSoFarUsdc: 0n,
238
304
  nowSeconds: deps.nowSeconds(),
239
305
  });
@@ -10,7 +10,8 @@ import {
10
10
  } from "../escape-hatch.js";
11
11
  import { signRecoveryMandate } from "../mandate.js";
12
12
  import { assertCanPayGas } from "../preflight.js";
13
- import { resolveCliWallet } from "../wallet.js";
13
+ import { writeClientOptions } from "../deps.js";
14
+ import { isPolicyEnforced, resolveCliWallet } from "../wallet.js";
14
15
  import type { CommandContext, CommandSpec } from "../context.js";
15
16
  import type { CliReadClient } from "../deps.js";
16
17
 
@@ -80,7 +81,11 @@ export const reclaimUnverifiedCommand: CommandSpec = {
80
81
  );
81
82
  }
82
83
 
83
- const { account } = await resolveCliWallet(env, { note: out.note });
84
+ const wallet = await resolveCliWallet(env, {
85
+ note: out.note,
86
+ rpcUrl: config.rpcUrl,
87
+ });
88
+ const { account } = wallet;
84
89
  const read = deps.createReadClient(config);
85
90
  await assertTestnetRpc(config, () => read.getChainId());
86
91
  await assertCanPayGas(read, account.address);
@@ -120,10 +125,17 @@ export const reclaimUnverifiedCommand: CommandSpec = {
120
125
  );
121
126
  out.note("");
122
127
 
123
- const write = deps.createWriteClient(config, account);
128
+ const write = deps.createWriteClient(
129
+ config,
130
+ account,
131
+ writeClientOptions(wallet),
132
+ );
124
133
  const result = await write.reclaimUnverifiedStream({
125
134
  streamId,
126
- signedMandate: await signRecoveryMandate(account, config.chainId),
135
+ // `null` for a policy-enforced wallet — see `../spend-policy.ts`.
136
+ signedMandate: isPolicyEnforced(wallet)
137
+ ? null
138
+ : await signRecoveryMandate(account, config.chainId),
127
139
  spentSoFarUsdc: 0n,
128
140
  nowSeconds: deps.nowSeconds(),
129
141
  });
@@ -11,7 +11,8 @@ import {
11
11
  } from "../escape-hatch.js";
12
12
  import { signRecoveryMandate } from "../mandate.js";
13
13
  import { assertCanPayGas } from "../preflight.js";
14
- import { resolveCliWallet } from "../wallet.js";
14
+ import { writeClientOptions } from "../deps.js";
15
+ import { isPolicyEnforced, resolveCliWallet } from "../wallet.js";
15
16
  import type { CommandContext, CommandSpec } from "../context.js";
16
17
  import type { CliReadClient, CliWriteClient } from "../deps.js";
17
18
  import type {
@@ -48,12 +49,28 @@ export const reclaimCommand: CommandSpec = {
48
49
  if (attempts < 1) throw usageError("--attempts must be at least 1");
49
50
  if (interval < 1) throw usageError("--interval must be at least 1 second");
50
51
 
51
- const { account } = await resolveCliWallet(env, { note: out.note });
52
+ const wallet = await resolveCliWallet(env, {
53
+ note: out.note,
54
+ rpcUrl: config.rpcUrl,
55
+ });
56
+ const { account } = wallet;
52
57
  const read = deps.createReadClient(config);
53
58
  await assertTestnetRpc(config, () => read.getChainId());
54
59
  await assertCanPayGas(read, account.address);
55
60
 
56
- const write = deps.createWriteClient(config, account);
61
+ const write = deps.createWriteClient(
62
+ config,
63
+ account,
64
+ writeClientOptions(wallet),
65
+ );
66
+
67
+ // A policy-enforced wallet (Privy) signs no mandate of its own: these are
68
+ // buyer-recovery actions, gated by the escrow on the buyer address and by
69
+ // the broker on `stream.buyer == wallet`. See `../spend-policy.ts`.
70
+ const recoveryMandate = async () =>
71
+ isPolicyEnforced(wallet)
72
+ ? null
73
+ : await signRecoveryMandate(account, config.chainId);
57
74
 
58
75
  // `--close-first` is TWO transactions, and the second one only simulates
59
76
  // successfully against post-close state. Sequencing them inside a single SDK
@@ -75,7 +92,7 @@ export const reclaimCommand: CommandSpec = {
75
92
  } else {
76
93
  closeResult = await closeBeforeReclaim(write, {
77
94
  streamId,
78
- signedMandate: await signRecoveryMandate(account, config.chainId),
95
+ signedMandate: await recoveryMandate(),
79
96
  spentSoFarUsdc: 0n,
80
97
  nowSeconds: deps.nowSeconds(),
81
98
  });
@@ -97,7 +114,7 @@ export const reclaimCommand: CommandSpec = {
97
114
  write,
98
115
  async () => ({
99
116
  streamId,
100
- signedMandate: await signRecoveryMandate(account, config.chainId),
117
+ signedMandate: await recoveryMandate(),
101
118
  spentSoFarUsdc: 0n,
102
119
  nowSeconds: deps.nowSeconds(),
103
120
  }),
@@ -234,7 +234,7 @@ async function resolveScanAddress(
234
234
  if (wallet === null) {
235
235
  throw configError(
236
236
  "metrik streams needs a wallet to know whose streams to list",
237
- `pass --address 0x… to list any address's streams without one, or set ${PRIVATE_KEY_ENV} (or the CDP variables).`,
237
+ `pass --address 0x… to list any address's streams without one, or set ${PRIVATE_KEY_ENV} (or the CDP or Privy variables).`,
238
238
  );
239
239
  }
240
240
  return wallet.account.address;