arc402-cli 0.2.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 (308) hide show
  1. package/README.md +245 -0
  2. package/dist/abis.d.ts +19 -0
  3. package/dist/abis.d.ts.map +1 -0
  4. package/dist/abis.js +177 -0
  5. package/dist/abis.js.map +1 -0
  6. package/dist/bundler.d.ts +65 -0
  7. package/dist/bundler.d.ts.map +1 -0
  8. package/dist/bundler.js +181 -0
  9. package/dist/bundler.js.map +1 -0
  10. package/dist/client.d.ts +14 -0
  11. package/dist/client.d.ts.map +1 -0
  12. package/dist/client.js +24 -0
  13. package/dist/client.js.map +1 -0
  14. package/dist/coinbase-smart-wallet.d.ts +28 -0
  15. package/dist/coinbase-smart-wallet.d.ts.map +1 -0
  16. package/dist/coinbase-smart-wallet.js +38 -0
  17. package/dist/coinbase-smart-wallet.js.map +1 -0
  18. package/dist/commands/accept.d.ts +3 -0
  19. package/dist/commands/accept.d.ts.map +1 -0
  20. package/dist/commands/accept.js +26 -0
  21. package/dist/commands/accept.js.map +1 -0
  22. package/dist/commands/agent-handshake.d.ts +3 -0
  23. package/dist/commands/agent-handshake.d.ts.map +1 -0
  24. package/dist/commands/agent-handshake.js +61 -0
  25. package/dist/commands/agent-handshake.js.map +1 -0
  26. package/dist/commands/agent.d.ts +3 -0
  27. package/dist/commands/agent.d.ts.map +1 -0
  28. package/dist/commands/agent.js +417 -0
  29. package/dist/commands/agent.js.map +1 -0
  30. package/dist/commands/agreements.d.ts +3 -0
  31. package/dist/commands/agreements.d.ts.map +1 -0
  32. package/dist/commands/agreements.js +344 -0
  33. package/dist/commands/agreements.js.map +1 -0
  34. package/dist/commands/arbitrator.d.ts +3 -0
  35. package/dist/commands/arbitrator.d.ts.map +1 -0
  36. package/dist/commands/arbitrator.js +157 -0
  37. package/dist/commands/arbitrator.js.map +1 -0
  38. package/dist/commands/arena-handshake.d.ts +3 -0
  39. package/dist/commands/arena-handshake.d.ts.map +1 -0
  40. package/dist/commands/arena-handshake.js +187 -0
  41. package/dist/commands/arena-handshake.js.map +1 -0
  42. package/dist/commands/cancel.d.ts +3 -0
  43. package/dist/commands/cancel.d.ts.map +1 -0
  44. package/dist/commands/cancel.js +30 -0
  45. package/dist/commands/cancel.js.map +1 -0
  46. package/dist/commands/channel.d.ts +3 -0
  47. package/dist/commands/channel.d.ts.map +1 -0
  48. package/dist/commands/channel.js +238 -0
  49. package/dist/commands/channel.js.map +1 -0
  50. package/dist/commands/coldstart.d.ts +3 -0
  51. package/dist/commands/coldstart.d.ts.map +1 -0
  52. package/dist/commands/coldstart.js +148 -0
  53. package/dist/commands/coldstart.js.map +1 -0
  54. package/dist/commands/config.d.ts +3 -0
  55. package/dist/commands/config.d.ts.map +1 -0
  56. package/dist/commands/config.js +40 -0
  57. package/dist/commands/config.js.map +1 -0
  58. package/dist/commands/contract-interaction.d.ts +3 -0
  59. package/dist/commands/contract-interaction.d.ts.map +1 -0
  60. package/dist/commands/contract-interaction.js +165 -0
  61. package/dist/commands/contract-interaction.js.map +1 -0
  62. package/dist/commands/daemon.d.ts +3 -0
  63. package/dist/commands/daemon.d.ts.map +1 -0
  64. package/dist/commands/daemon.js +891 -0
  65. package/dist/commands/daemon.js.map +1 -0
  66. package/dist/commands/deliver.d.ts +3 -0
  67. package/dist/commands/deliver.d.ts.map +1 -0
  68. package/dist/commands/deliver.js +156 -0
  69. package/dist/commands/deliver.js.map +1 -0
  70. package/dist/commands/discover.d.ts +3 -0
  71. package/dist/commands/discover.d.ts.map +1 -0
  72. package/dist/commands/discover.js +224 -0
  73. package/dist/commands/discover.js.map +1 -0
  74. package/dist/commands/dispute.d.ts +3 -0
  75. package/dist/commands/dispute.d.ts.map +1 -0
  76. package/dist/commands/dispute.js +348 -0
  77. package/dist/commands/dispute.js.map +1 -0
  78. package/dist/commands/endpoint.d.ts +3 -0
  79. package/dist/commands/endpoint.d.ts.map +1 -0
  80. package/dist/commands/endpoint.js +604 -0
  81. package/dist/commands/endpoint.js.map +1 -0
  82. package/dist/commands/hire.d.ts +3 -0
  83. package/dist/commands/hire.d.ts.map +1 -0
  84. package/dist/commands/hire.js +189 -0
  85. package/dist/commands/hire.js.map +1 -0
  86. package/dist/commands/migrate.d.ts +3 -0
  87. package/dist/commands/migrate.d.ts.map +1 -0
  88. package/dist/commands/migrate.js +163 -0
  89. package/dist/commands/migrate.js.map +1 -0
  90. package/dist/commands/negotiate.d.ts +3 -0
  91. package/dist/commands/negotiate.d.ts.map +1 -0
  92. package/dist/commands/negotiate.js +247 -0
  93. package/dist/commands/negotiate.js.map +1 -0
  94. package/dist/commands/openshell.d.ts +3 -0
  95. package/dist/commands/openshell.d.ts.map +1 -0
  96. package/dist/commands/openshell.js +952 -0
  97. package/dist/commands/openshell.js.map +1 -0
  98. package/dist/commands/owner.d.ts +3 -0
  99. package/dist/commands/owner.d.ts.map +1 -0
  100. package/dist/commands/owner.js +32 -0
  101. package/dist/commands/owner.js.map +1 -0
  102. package/dist/commands/policy.d.ts +4 -0
  103. package/dist/commands/policy.d.ts.map +1 -0
  104. package/dist/commands/policy.js +248 -0
  105. package/dist/commands/policy.js.map +1 -0
  106. package/dist/commands/relay.d.ts +3 -0
  107. package/dist/commands/relay.d.ts.map +1 -0
  108. package/dist/commands/relay.js +279 -0
  109. package/dist/commands/relay.js.map +1 -0
  110. package/dist/commands/remediate.d.ts +3 -0
  111. package/dist/commands/remediate.d.ts.map +1 -0
  112. package/dist/commands/remediate.js +42 -0
  113. package/dist/commands/remediate.js.map +1 -0
  114. package/dist/commands/reputation.d.ts +4 -0
  115. package/dist/commands/reputation.d.ts.map +1 -0
  116. package/dist/commands/reputation.js +72 -0
  117. package/dist/commands/reputation.js.map +1 -0
  118. package/dist/commands/setup.d.ts +3 -0
  119. package/dist/commands/setup.d.ts.map +1 -0
  120. package/dist/commands/setup.js +332 -0
  121. package/dist/commands/setup.js.map +1 -0
  122. package/dist/commands/trust.d.ts +3 -0
  123. package/dist/commands/trust.d.ts.map +1 -0
  124. package/dist/commands/trust.js +23 -0
  125. package/dist/commands/trust.js.map +1 -0
  126. package/dist/commands/verify.d.ts +3 -0
  127. package/dist/commands/verify.d.ts.map +1 -0
  128. package/dist/commands/verify.js +88 -0
  129. package/dist/commands/verify.js.map +1 -0
  130. package/dist/commands/wallet.d.ts +3 -0
  131. package/dist/commands/wallet.d.ts.map +1 -0
  132. package/dist/commands/wallet.js +2520 -0
  133. package/dist/commands/wallet.js.map +1 -0
  134. package/dist/commands/watchtower.d.ts +3 -0
  135. package/dist/commands/watchtower.d.ts.map +1 -0
  136. package/dist/commands/watchtower.js +238 -0
  137. package/dist/commands/watchtower.js.map +1 -0
  138. package/dist/commands/workroom.d.ts +3 -0
  139. package/dist/commands/workroom.d.ts.map +1 -0
  140. package/dist/commands/workroom.js +855 -0
  141. package/dist/commands/workroom.js.map +1 -0
  142. package/dist/config.d.ts +62 -0
  143. package/dist/config.d.ts.map +1 -0
  144. package/dist/config.js +141 -0
  145. package/dist/config.js.map +1 -0
  146. package/dist/daemon/config.d.ts +74 -0
  147. package/dist/daemon/config.d.ts.map +1 -0
  148. package/dist/daemon/config.js +271 -0
  149. package/dist/daemon/config.js.map +1 -0
  150. package/dist/daemon/hire-listener.d.ts +31 -0
  151. package/dist/daemon/hire-listener.d.ts.map +1 -0
  152. package/dist/daemon/hire-listener.js +207 -0
  153. package/dist/daemon/hire-listener.js.map +1 -0
  154. package/dist/daemon/index.d.ts +29 -0
  155. package/dist/daemon/index.d.ts.map +1 -0
  156. package/dist/daemon/index.js +535 -0
  157. package/dist/daemon/index.js.map +1 -0
  158. package/dist/daemon/job-lifecycle.d.ts +62 -0
  159. package/dist/daemon/job-lifecycle.d.ts.map +1 -0
  160. package/dist/daemon/job-lifecycle.js +201 -0
  161. package/dist/daemon/job-lifecycle.js.map +1 -0
  162. package/dist/daemon/notify.d.ts +22 -0
  163. package/dist/daemon/notify.d.ts.map +1 -0
  164. package/dist/daemon/notify.js +148 -0
  165. package/dist/daemon/notify.js.map +1 -0
  166. package/dist/daemon/token-metering.d.ts +42 -0
  167. package/dist/daemon/token-metering.d.ts.map +1 -0
  168. package/dist/daemon/token-metering.js +178 -0
  169. package/dist/daemon/token-metering.js.map +1 -0
  170. package/dist/daemon/userops.d.ts +21 -0
  171. package/dist/daemon/userops.d.ts.map +1 -0
  172. package/dist/daemon/userops.js +88 -0
  173. package/dist/daemon/userops.js.map +1 -0
  174. package/dist/daemon/wallet-monitor.d.ts +16 -0
  175. package/dist/daemon/wallet-monitor.d.ts.map +1 -0
  176. package/dist/daemon/wallet-monitor.js +57 -0
  177. package/dist/daemon/wallet-monitor.js.map +1 -0
  178. package/dist/drain-v4.d.ts +2 -0
  179. package/dist/drain-v4.d.ts.map +1 -0
  180. package/dist/drain-v4.js +167 -0
  181. package/dist/drain-v4.js.map +1 -0
  182. package/dist/endpoint-config.d.ts +36 -0
  183. package/dist/endpoint-config.d.ts.map +1 -0
  184. package/dist/endpoint-config.js +96 -0
  185. package/dist/endpoint-config.js.map +1 -0
  186. package/dist/index.d.ts +3 -0
  187. package/dist/index.d.ts.map +1 -0
  188. package/dist/index.js +79 -0
  189. package/dist/index.js.map +1 -0
  190. package/dist/openshell-runtime.d.ts +55 -0
  191. package/dist/openshell-runtime.d.ts.map +1 -0
  192. package/dist/openshell-runtime.js +268 -0
  193. package/dist/openshell-runtime.js.map +1 -0
  194. package/dist/signing.d.ts +2 -0
  195. package/dist/signing.d.ts.map +1 -0
  196. package/dist/signing.js +23 -0
  197. package/dist/signing.js.map +1 -0
  198. package/dist/telegram-notify.d.ts +23 -0
  199. package/dist/telegram-notify.d.ts.map +1 -0
  200. package/dist/telegram-notify.js +106 -0
  201. package/dist/telegram-notify.js.map +1 -0
  202. package/dist/ui/banner.d.ts +7 -0
  203. package/dist/ui/banner.d.ts.map +1 -0
  204. package/dist/ui/banner.js +37 -0
  205. package/dist/ui/banner.js.map +1 -0
  206. package/dist/ui/colors.d.ts +14 -0
  207. package/dist/ui/colors.d.ts.map +1 -0
  208. package/dist/ui/colors.js +29 -0
  209. package/dist/ui/colors.js.map +1 -0
  210. package/dist/ui/format.d.ts +26 -0
  211. package/dist/ui/format.d.ts.map +1 -0
  212. package/dist/ui/format.js +77 -0
  213. package/dist/ui/format.js.map +1 -0
  214. package/dist/ui/spinner.d.ts +8 -0
  215. package/dist/ui/spinner.d.ts.map +1 -0
  216. package/dist/ui/spinner.js +43 -0
  217. package/dist/ui/spinner.js.map +1 -0
  218. package/dist/utils/format.d.ts +10 -0
  219. package/dist/utils/format.d.ts.map +1 -0
  220. package/dist/utils/format.js +61 -0
  221. package/dist/utils/format.js.map +1 -0
  222. package/dist/utils/hash.d.ts +3 -0
  223. package/dist/utils/hash.d.ts.map +1 -0
  224. package/dist/utils/hash.js +43 -0
  225. package/dist/utils/hash.js.map +1 -0
  226. package/dist/utils/time.d.ts +3 -0
  227. package/dist/utils/time.d.ts.map +1 -0
  228. package/dist/utils/time.js +21 -0
  229. package/dist/utils/time.js.map +1 -0
  230. package/dist/wallet-router.d.ts +25 -0
  231. package/dist/wallet-router.d.ts.map +1 -0
  232. package/dist/wallet-router.js +153 -0
  233. package/dist/wallet-router.js.map +1 -0
  234. package/dist/walletconnect-session.d.ts +12 -0
  235. package/dist/walletconnect-session.d.ts.map +1 -0
  236. package/dist/walletconnect-session.js +26 -0
  237. package/dist/walletconnect-session.js.map +1 -0
  238. package/dist/walletconnect.d.ts +46 -0
  239. package/dist/walletconnect.d.ts.map +1 -0
  240. package/dist/walletconnect.js +267 -0
  241. package/dist/walletconnect.js.map +1 -0
  242. package/package.json +38 -0
  243. package/scripts/authorize-machine-key.ts +43 -0
  244. package/scripts/drain-wallet.ts +149 -0
  245. package/scripts/execute-spend-only.ts +81 -0
  246. package/scripts/register-agent-userop.ts +186 -0
  247. package/src/abis.ts +187 -0
  248. package/src/bundler.ts +235 -0
  249. package/src/client.ts +34 -0
  250. package/src/coinbase-smart-wallet.ts +51 -0
  251. package/src/commands/accept.ts +25 -0
  252. package/src/commands/agent-handshake.ts +67 -0
  253. package/src/commands/agent.ts +458 -0
  254. package/src/commands/agreements.ts +324 -0
  255. package/src/commands/arbitrator.ts +129 -0
  256. package/src/commands/arena-handshake.ts +217 -0
  257. package/src/commands/cancel.ts +26 -0
  258. package/src/commands/channel.ts +208 -0
  259. package/src/commands/coldstart.ts +156 -0
  260. package/src/commands/config.ts +35 -0
  261. package/src/commands/contract-interaction.ts +166 -0
  262. package/src/commands/daemon.ts +971 -0
  263. package/src/commands/deliver.ts +116 -0
  264. package/src/commands/discover.ts +295 -0
  265. package/src/commands/dispute.ts +373 -0
  266. package/src/commands/endpoint.ts +619 -0
  267. package/src/commands/hire.ts +200 -0
  268. package/src/commands/migrate.ts +175 -0
  269. package/src/commands/negotiate.ts +270 -0
  270. package/src/commands/openshell.ts +1053 -0
  271. package/src/commands/owner.ts +30 -0
  272. package/src/commands/policy.ts +252 -0
  273. package/src/commands/relay.ts +272 -0
  274. package/src/commands/remediate.ts +22 -0
  275. package/src/commands/reputation.ts +71 -0
  276. package/src/commands/setup.ts +343 -0
  277. package/src/commands/trust.ts +15 -0
  278. package/src/commands/verify.ts +88 -0
  279. package/src/commands/wallet.ts +2892 -0
  280. package/src/commands/watchtower.ts +232 -0
  281. package/src/commands/workroom.ts +889 -0
  282. package/src/config.ts +153 -0
  283. package/src/daemon/config.ts +308 -0
  284. package/src/daemon/hire-listener.ts +226 -0
  285. package/src/daemon/index.ts +609 -0
  286. package/src/daemon/job-lifecycle.ts +215 -0
  287. package/src/daemon/notify.ts +157 -0
  288. package/src/daemon/token-metering.ts +183 -0
  289. package/src/daemon/userops.ts +119 -0
  290. package/src/daemon/wallet-monitor.ts +90 -0
  291. package/src/drain-v4.ts +159 -0
  292. package/src/endpoint-config.ts +83 -0
  293. package/src/index.ts +75 -0
  294. package/src/openshell-runtime.ts +277 -0
  295. package/src/signing.ts +28 -0
  296. package/src/telegram-notify.ts +88 -0
  297. package/src/ui/banner.ts +41 -0
  298. package/src/ui/colors.ts +30 -0
  299. package/src/ui/format.ts +77 -0
  300. package/src/ui/spinner.ts +46 -0
  301. package/src/utils/format.ts +48 -0
  302. package/src/utils/hash.ts +5 -0
  303. package/src/utils/time.ts +15 -0
  304. package/src/wallet-router.ts +178 -0
  305. package/src/walletconnect-session.ts +27 -0
  306. package/src/walletconnect.ts +294 -0
  307. package/test/time.test.js +11 -0
  308. package/tsconfig.json +19 -0
package/src/config.ts ADDED
@@ -0,0 +1,153 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import * as os from "os";
4
+
5
+ export interface Arc402Config {
6
+ network: "base-mainnet" | "base-sepolia";
7
+ rpcUrl: string;
8
+ privateKey?: string;
9
+ guardianPrivateKey?: string;
10
+ guardianAddress?: string;
11
+ walletConnectProjectId?: string;
12
+ ownerAddress?: string;
13
+ agentRegistryAddress?: string;
14
+ agentRegistryV2Address?: string;
15
+ serviceAgreementAddress?: string;
16
+ disputeArbitrationAddress?: string;
17
+ disputeModuleAddress?: string;
18
+ trustRegistryAddress: string;
19
+ trustRegistryV2Address?: string;
20
+ intentAttestationAddress?: string;
21
+ settlementCoordinatorAddress?: string;
22
+ sessionChannelsAddress?: string;
23
+ reputationOracleAddress?: string;
24
+ sponsorshipAttestationAddress?: string;
25
+ capabilityRegistryAddress?: string;
26
+ governanceAddress?: string;
27
+ agreementTreeAddress?: string;
28
+ policyEngineAddress?: string;
29
+ walletFactoryAddress?: string;
30
+ walletContractAddress?: string;
31
+ watchtowerRegistryAddress?: string;
32
+ governedTokenWhitelistAddress?: string;
33
+ vouchingRegistryAddress?: string;
34
+ migrationRegistryAddress?: string;
35
+ handshakeAddress?: string;
36
+ paymasterUrl?: string; // CDP paymaster endpoint
37
+ cdpKeyName?: string; // CDP API key name (org/.../apiKeys/...)
38
+ cdpPrivateKey?: string; // CDP EC private key — base64 DER SEC1 (store in CDP_PRIVATE_KEY env var)
39
+ subdomainApi?: string; // defaults to https://api.arc402.xyz
40
+ telegramBotToken?: string;
41
+ telegramChatId?: string;
42
+ telegramThreadId?: number;
43
+ wcSession?: {
44
+ topic: string;
45
+ expiry: number; // Unix timestamp
46
+ account: string; // Phone wallet address
47
+ chainId: number;
48
+ };
49
+ }
50
+
51
+ const CONFIG_DIR = path.join(os.homedir(), ".arc402");
52
+ const CONFIG_PATH = process.env.ARC402_CONFIG || path.join(CONFIG_DIR, "config.json");
53
+
54
+ export const getConfigPath = () => CONFIG_PATH;
55
+
56
+ export function loadConfig(): Arc402Config {
57
+ if (!fs.existsSync(CONFIG_PATH)) {
58
+ console.error(`No config found at ${CONFIG_PATH}. Run \`arc402 config init\` to set up your configuration.`);
59
+ process.exit(1);
60
+ }
61
+ return JSON.parse(fs.readFileSync(CONFIG_PATH, "utf-8")) as Arc402Config;
62
+ }
63
+
64
+ export function saveConfig(config: Arc402Config): void {
65
+ const configDir = path.dirname(CONFIG_PATH);
66
+ fs.mkdirSync(configDir, { recursive: true, mode: 0o700 });
67
+ fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), { mode: 0o600 });
68
+ }
69
+
70
+ export const configExists = () => fs.existsSync(CONFIG_PATH);
71
+
72
+ // Public Base RPC — stale state, do not use for production. Alchemy recommended.
73
+ export const PUBLIC_BASE_RPC = "https://mainnet.base.org";
74
+ export const ALCHEMY_BASE_RPC = "https://base-mainnet.g.alchemy.com/v2/YIA2uRCsFI-j5pqH-aRzflrACSlV1Qrs";
75
+
76
+ /**
77
+ * Warn at runtime if the configured RPC is the public Base endpoint.
78
+ * Public Base RPC has delayed state propagation — use Alchemy for production.
79
+ */
80
+ export function warnIfPublicRpc(config: Arc402Config): void {
81
+ if (config.rpcUrl === PUBLIC_BASE_RPC || config.rpcUrl === "https://sepolia.base.org") {
82
+ console.warn("WARN: Using public Base RPC — state reads may be stale. Set rpcUrl to an Alchemy endpoint for production.");
83
+ console.warn(` Recommended: arc402 config set rpcUrl ${ALCHEMY_BASE_RPC}`);
84
+ }
85
+ }
86
+
87
+ export const NETWORK_DEFAULTS: Record<string, Partial<Arc402Config> & { usdcAddress: string }> = {
88
+ "base-mainnet": {
89
+ rpcUrl: ALCHEMY_BASE_RPC,
90
+ usdcAddress: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
91
+ paymasterUrl: "https://api.developer.coinbase.com/rpc/v1/base/dca85088-a2ac-4ec3-8647-5154b150e7a9",
92
+ // Base Mainnet deployments — v2 deployed 2026-03-15
93
+ policyEngineAddress: "0xAA5Ef3489C929bFB3BFf5D5FE15aa62d3763c847",
94
+ trustRegistryAddress: "0x22366D6dabb03062Bc0a5E893EfDff15D8E329b1", // TrustRegistryV3 — v2
95
+ trustRegistryV2Address: "0xdA1D377991B2E580991B0DD381CdD635dd71aC39", // old v2, kept for reference
96
+ intentAttestationAddress: "0x7ad8db6C5f394542E8e9658F86C85cC99Cf6D460",
97
+ settlementCoordinatorAddress: "0xd52d8Be9728976E0D70C89db9F8ACeb5B5e97cA2", // SettlementCoordinatorV2
98
+ agentRegistryAddress: "0xcc0D8731ccCf6CFfF4e66F6d68cA86330Ea8B622", // ARC402RegistryV2
99
+ agentRegistryV2Address: "0xD5c2851B00090c92Ba7F4723FB548bb30C9B6865", // AgentRegistry
100
+ walletFactoryAddress: "0xcB52B5d746eEc05e141039E92e3dBefeAe496051", // WalletFactoryV5 — redeployed 2026-03-19 (optimized bytecode, FOUNDRY_PROFILE=deploy)
101
+ sponsorshipAttestationAddress: "0xD6c2edE89Ea71aE19Db2Be848e172b444Ed38f22",
102
+ serviceAgreementAddress: "0xC98B402CAB9156da68A87a69E3B4bf167A3CCcF6",
103
+ sessionChannelsAddress: "0x578f8d1bd82E8D6268E329d664d663B4d985BE61",
104
+ disputeModuleAddress: "0x5ebd301cEF0C908AB17Fd183aD9c274E4B34e9d6",
105
+ reputationOracleAddress: "0x359F76a54F9A345546E430e4d6665A7dC9DaECd4",
106
+ governanceAddress: "0xE931DD2EEb9Af9353Dd5E2c1250492A0135E0EC4", // ARC402Governance
107
+ guardianAddress: "0xED0A033B79626cdf9570B6c3baC7f699cD0032D8", // ARC402Guardian
108
+ walletContractAddress: "0xfd5C8c0a08fDcdeD2fe03e0DC9FA55595667F313", // ARC402Wallet instance
109
+ agreementTreeAddress: "0x6a82240512619B25583b9e95783410cf782915b1",
110
+ capabilityRegistryAddress: "0x7becb642668B80502dD957A594E1dD0aC414c1a3",
111
+ disputeArbitrationAddress: "0xF61b75E4903fbC81169FeF8b7787C13cB7750601",
112
+ governedTokenWhitelistAddress: "0xeB58896337244Bb408362Fea727054f9e7157451",
113
+ watchtowerRegistryAddress: "0xbC811d1e3c5C5b67CA57df1DFb08847b1c8c458A",
114
+ vouchingRegistryAddress: "0x94519194Bf17865770faD59eF581feC512Ae99c9",
115
+ migrationRegistryAddress: "0xb60B62357b90F254f555f03B162a30E22890e3B5",
116
+ handshakeAddress: "0x4F5A38Bb746d7E5d49d8fd26CA6beD141Ec2DDb3",
117
+ },
118
+ "base-sepolia": {
119
+ rpcUrl: "https://sepolia.base.org",
120
+ usdcAddress: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
121
+ // v2 deployment — Base Sepolia (chain 84532) — deployed 2026-03-15
122
+ // Unchanged v1 contracts:
123
+ policyEngineAddress: "0x44102e70c2A366632d98Fe40d892a2501fC7fFF2",
124
+ intentAttestationAddress: "0x942c807Cc6E0240A061e074b61345618aBadc457",
125
+ settlementCoordinatorAddress: "0x52b565797975781f069368Df40d6633b2aD03390",
126
+ agentRegistryV2Address: "0x07D526f8A8e148570509aFa249EFF295045A0cc9", // AgentRegistry
127
+ reputationOracleAddress: "0x410e650113fd163389C956BC7fC51c5642617187",
128
+ walletFactoryAddress: "0xD560C22aD5372Aa830ee5ffBFa4a5D9f528e7B87",
129
+ sponsorshipAttestationAddress:"0xc0d927745AcF8DEeE551BE11A12c97c492DDC989",
130
+ governanceAddress: "0x504b3D73A8dFbcAB9551d8a11Bb0B07C90C4c926",
131
+ guardianAddress: "0x5c1D2cD6B9B291b436BF1b109A711F0E477EB6fe",
132
+ walletContractAddress: "0xc77854f9091A25eD1f35EA24E9bdFb64d0850E45",
133
+ agreementTreeAddress: "0x8F46F31FcEbd60f526308AD20e4a008887709720",
134
+ capabilityRegistryAddress: "0x6a413e74b65828A014dD8DA61861Bf9E1b6372D2",
135
+ governedTokenWhitelistAddress:"0x64C15CA701167C7c901a8a5575a5232b37CAF213",
136
+ watchtowerRegistryAddress: "0x70c4E53E3A916eB8A695630f129B943af9C61C57",
137
+ // v2 contracts (new/redeployed 2026-03-15):
138
+ trustRegistryAddress: "0xf2aE072BB8575c23B0efbF44bDc8188aA900cA7a", // TrustRegistryV3
139
+ agentRegistryAddress: "0x0461b2b7A1E50866962CB07326000A94009c58Ff", // ARC402RegistryV2
140
+ serviceAgreementAddress: "0xbbb1DA355D810E9baEF1a7D072B2132E4755976B",
141
+ sessionChannelsAddress: "0x5EF144AE2C8456d014e6E3F293c162410C043564",
142
+ disputeModuleAddress: "0x01866144495fBBbBB7aaD81605de051B2A62594A",
143
+ disputeArbitrationAddress: "0xa4f6F77927Da53a25926A5f0bffBEB0210108cA8",
144
+ vouchingRegistryAddress: "0x96432aDc7aC06256297AdF11B94C47f68b2F13A2",
145
+ migrationRegistryAddress: "0x3aeAaD32386D6fC40eeb5c2C27a5aCFE6aDf9ABD",
146
+ },
147
+ };
148
+
149
+ export const getUsdcAddress = (config: Arc402Config) => NETWORK_DEFAULTS[config.network]?.usdcAddress ?? "";
150
+
151
+ export function getSubdomainApi(config: Arc402Config): string {
152
+ return config.subdomainApi ?? "https://api.arc402.xyz";
153
+ }
@@ -0,0 +1,308 @@
1
+ /**
2
+ * Daemon configuration loader.
3
+ * Parses ~/.arc402/daemon.toml, enforces env: prefix for secrets,
4
+ * resolves env: values from the environment.
5
+ */
6
+ import * as fs from "fs";
7
+ import * as path from "path";
8
+ import * as os from "os";
9
+ import { parse as parseToml } from "smol-toml";
10
+
11
+ export const DAEMON_DIR = path.join(os.homedir(), ".arc402");
12
+ export const DAEMON_TOML = path.join(DAEMON_DIR, "daemon.toml");
13
+ export const DAEMON_PID = path.join(DAEMON_DIR, "daemon.pid");
14
+ export const DAEMON_LOG = path.join(DAEMON_DIR, "daemon.log");
15
+ export const DAEMON_DB = path.join(DAEMON_DIR, "daemon.db");
16
+ export const DAEMON_SOCK = path.join(DAEMON_DIR, "daemon.sock");
17
+
18
+ export interface DaemonConfig {
19
+ wallet: {
20
+ contract_address: string;
21
+ owner_address: string;
22
+ machine_key: string; // must be "env:VAR_NAME"
23
+ };
24
+ network: {
25
+ rpc_url: string;
26
+ chain_id: number;
27
+ entry_point: string;
28
+ };
29
+ bundler: {
30
+ mode: "external" | "arc402" | "self";
31
+ endpoint: string;
32
+ earn_fees: boolean;
33
+ eth_float: string;
34
+ sweep_threshold: string;
35
+ sweep_to: string;
36
+ rpc_url: string;
37
+ };
38
+ relay: {
39
+ enabled: boolean;
40
+ listen_port: number;
41
+ endpoint: string;
42
+ max_concurrent_agreements: number;
43
+ poll_interval_seconds: number;
44
+ relay_url: string;
45
+ };
46
+ watchtower: {
47
+ enabled: boolean;
48
+ poll_interval_seconds: number;
49
+ challenge_confirmation_blocks: number;
50
+ external_watchtower_url: string;
51
+ update_interval_states: number;
52
+ };
53
+ policy: {
54
+ auto_accept: boolean;
55
+ max_price_eth: string;
56
+ allowed_capabilities: string[];
57
+ require_min_trust_score: number;
58
+ min_hire_lead_time_seconds: number;
59
+ };
60
+ notifications: {
61
+ telegram_bot_token: string;
62
+ telegram_chat_id: string;
63
+ notify_on_hire_request: boolean;
64
+ notify_on_hire_accepted: boolean;
65
+ notify_on_hire_rejected: boolean;
66
+ notify_on_delivery: boolean;
67
+ notify_on_dispute: boolean;
68
+ notify_on_channel_challenge: boolean;
69
+ notify_on_low_balance: boolean;
70
+ low_balance_threshold_eth: string;
71
+ };
72
+ work: {
73
+ handler: "exec" | "http" | "noop";
74
+ exec_command: string;
75
+ http_url: string;
76
+ http_auth_token: string;
77
+ };
78
+ }
79
+
80
+ function resolveEnvValue(value: string, field: string): string {
81
+ if (!value.startsWith("env:")) return value;
82
+ const varName = value.slice(4);
83
+ const resolved = process.env[varName];
84
+ if (!resolved) {
85
+ throw new Error(`Environment variable ${varName} is not set (required for ${field})`);
86
+ }
87
+ return resolved;
88
+ }
89
+
90
+ function tryResolveEnvValue(value: string): string {
91
+ if (!value.startsWith("env:")) return value;
92
+ const varName = value.slice(4);
93
+ return process.env[varName] ?? "";
94
+ }
95
+
96
+ function str(v: unknown, def = ""): string {
97
+ return typeof v === "string" ? v : def;
98
+ }
99
+ function num(v: unknown, def: number): number {
100
+ return typeof v === "number" ? v : def;
101
+ }
102
+ function bool(v: unknown, def: boolean): boolean {
103
+ return typeof v === "boolean" ? v : def;
104
+ }
105
+ function strArr(v: unknown): string[] {
106
+ return Array.isArray(v) ? (v as string[]) : [];
107
+ }
108
+
109
+ function withDefaults(raw: Record<string, unknown>): DaemonConfig {
110
+ const w = (raw.wallet as Record<string, unknown>) ?? {};
111
+ const n = (raw.network as Record<string, unknown>) ?? {};
112
+ const b = (raw.bundler as Record<string, unknown>) ?? {};
113
+ const r = (raw.relay as Record<string, unknown>) ?? {};
114
+ const wt = (raw.watchtower as Record<string, unknown>) ?? {};
115
+ const p = (raw.policy as Record<string, unknown>) ?? {};
116
+ const notif = (raw.notifications as Record<string, unknown>) ?? {};
117
+ const work = (raw.work as Record<string, unknown>) ?? {};
118
+
119
+ return {
120
+ wallet: {
121
+ contract_address: str(w.contract_address),
122
+ owner_address: str(w.owner_address),
123
+ machine_key: str(w.machine_key, "env:ARC402_MACHINE_KEY"),
124
+ },
125
+ network: {
126
+ rpc_url: str(n.rpc_url, "https://base-mainnet.g.alchemy.com/v2/YIA2uRCsFI-j5pqH-aRzflrACSlV1Qrs"),
127
+ chain_id: num(n.chain_id, 8453),
128
+ entry_point: str(n.entry_point, "0x0000000071727De22E5E9d8BAf0edAc6f37da032"),
129
+ },
130
+ bundler: {
131
+ mode: (str(b.mode, "external")) as "external" | "arc402" | "self",
132
+ endpoint: str(b.endpoint),
133
+ earn_fees: bool(b.earn_fees, false),
134
+ eth_float: str(b.eth_float, "0.01"),
135
+ sweep_threshold: str(b.sweep_threshold, "0.005"),
136
+ sweep_to: str(b.sweep_to),
137
+ rpc_url: str(b.rpc_url),
138
+ },
139
+ relay: {
140
+ enabled: bool(r.enabled, true),
141
+ listen_port: num(r.listen_port, 4402),
142
+ endpoint: str(r.endpoint),
143
+ max_concurrent_agreements: num(r.max_concurrent_agreements, 10),
144
+ poll_interval_seconds: num(r.poll_interval_seconds, 2),
145
+ relay_url: str(r.relay_url),
146
+ },
147
+ watchtower: {
148
+ enabled: bool(wt.enabled, true),
149
+ poll_interval_seconds: num(wt.poll_interval_seconds, 60),
150
+ challenge_confirmation_blocks: num(wt.challenge_confirmation_blocks, 2),
151
+ external_watchtower_url: str(wt.external_watchtower_url),
152
+ update_interval_states: num(wt.update_interval_states, 10),
153
+ },
154
+ policy: {
155
+ auto_accept: bool(p.auto_accept, false),
156
+ max_price_eth: str(p.max_price_eth, "0.1"),
157
+ allowed_capabilities: strArr(p.allowed_capabilities),
158
+ require_min_trust_score: num(p.require_min_trust_score, 50),
159
+ min_hire_lead_time_seconds: num(p.min_hire_lead_time_seconds, 300),
160
+ },
161
+ notifications: {
162
+ telegram_bot_token: str(notif.telegram_bot_token, "env:TELEGRAM_BOT_TOKEN"),
163
+ telegram_chat_id: str(notif.telegram_chat_id, "env:TELEGRAM_CHAT_ID"),
164
+ notify_on_hire_request: bool(notif.notify_on_hire_request, true),
165
+ notify_on_hire_accepted: bool(notif.notify_on_hire_accepted, true),
166
+ notify_on_hire_rejected: bool(notif.notify_on_hire_rejected, true),
167
+ notify_on_delivery: bool(notif.notify_on_delivery, true),
168
+ notify_on_dispute: bool(notif.notify_on_dispute, true),
169
+ notify_on_channel_challenge: bool(notif.notify_on_channel_challenge, true),
170
+ notify_on_low_balance: bool(notif.notify_on_low_balance, true),
171
+ low_balance_threshold_eth: str(notif.low_balance_threshold_eth, "0.005"),
172
+ },
173
+ work: {
174
+ handler: (str(work.handler, "noop")) as "exec" | "http" | "noop",
175
+ exec_command: str(work.exec_command),
176
+ http_url: str(work.http_url),
177
+ http_auth_token: str(work.http_auth_token),
178
+ },
179
+ };
180
+ }
181
+
182
+ export function loadDaemonConfig(configPath = DAEMON_TOML): DaemonConfig {
183
+ if (!fs.existsSync(configPath)) {
184
+ throw new Error(`daemon.toml not found at ${configPath}. Run: arc402 daemon init`);
185
+ }
186
+
187
+ const raw = fs.readFileSync(configPath, "utf-8");
188
+ let parsed: Record<string, unknown>;
189
+ try {
190
+ parsed = parseToml(raw) as Record<string, unknown>;
191
+ } catch (err) {
192
+ throw new Error(`Failed to parse daemon.toml: ${err instanceof Error ? err.message : String(err)}`);
193
+ }
194
+
195
+ const config = withDefaults(parsed);
196
+
197
+ // Required fields
198
+ if (!config.wallet.contract_address) {
199
+ throw new Error("daemon.toml: wallet.contract_address is required");
200
+ }
201
+ if (!config.network.rpc_url) {
202
+ throw new Error("daemon.toml: network.rpc_url is required");
203
+ }
204
+ if (!config.network.chain_id) {
205
+ throw new Error("daemon.toml: network.chain_id is required");
206
+ }
207
+
208
+ // Machine key MUST use env: prefix — never hardcoded
209
+ if (!config.wallet.machine_key.startsWith("env:")) {
210
+ throw new Error("ERROR: machine_key must use env: prefix — never hardcode keys");
211
+ }
212
+
213
+ // Resolve optional env: values silently (missing = disabled feature)
214
+ config.notifications.telegram_bot_token = tryResolveEnvValue(config.notifications.telegram_bot_token);
215
+ config.notifications.telegram_chat_id = tryResolveEnvValue(config.notifications.telegram_chat_id);
216
+ config.work.http_auth_token = tryResolveEnvValue(config.work.http_auth_token);
217
+
218
+ return config;
219
+ }
220
+
221
+ export function loadMachineKey(config: DaemonConfig): { privateKey: string; address: string } {
222
+ const envVarName = config.wallet.machine_key.startsWith("env:")
223
+ ? config.wallet.machine_key.slice(4)
224
+ : "ARC402_MACHINE_KEY";
225
+
226
+ const privateKey = process.env[envVarName];
227
+ if (!privateKey) {
228
+ throw new Error(`Machine key not found. Set environment variable: ${envVarName}`);
229
+ }
230
+
231
+ const { ethers } = require("ethers") as typeof import("ethers");
232
+ let address: string;
233
+ try {
234
+ const w = new ethers.Wallet(privateKey);
235
+ address = w.address;
236
+ } catch {
237
+ throw new Error(`Invalid machine key format in ${envVarName}`);
238
+ }
239
+
240
+ return { privateKey, address };
241
+ }
242
+
243
+ export const TEMPLATE_DAEMON_TOML = `# ~/.arc402/daemon.toml
244
+ # ARC-402 Daemon Configuration
245
+ # Generated by: arc402 daemon init
246
+ #
247
+ # SECURITY: Never put private keys here. Use environment variables.
248
+
249
+ [wallet]
250
+ contract_address = "" # ARC402Wallet contract address (required)
251
+ owner_address = "" # Owner EOA address — for display and verification only
252
+ machine_key = "env:ARC402_MACHINE_KEY" # Machine key loaded from environment. NEVER hardcode here.
253
+
254
+ [network]
255
+ rpc_url = "https://base-mainnet.g.alchemy.com/v2/YIA2uRCsFI-j5pqH-aRzflrACSlV1Qrs" # Alchemy Base RPC (recommended)
256
+ chain_id = 8453 # Base mainnet. Use 84532 for Base Sepolia.
257
+ entry_point = "0x0000000071727De22E5E9d8BAf0edAc6f37da032" # ERC-4337 EntryPoint v0.7
258
+
259
+ [bundler]
260
+ mode = "external" # external | arc402 | self
261
+ endpoint = "" # Required when mode = external. Pimlico, Alchemy, etc.
262
+ earn_fees = false # self mode only: bundle for other network agents
263
+ eth_float = "0.01" # Minimum ETH to maintain in bundler EOA for gas fronting
264
+ sweep_threshold = "0.005" # Sweep fees to wallet when bundler EOA exceeds this (ETH)
265
+ sweep_to = "" # Sweep destination. Defaults to wallet.contract_address.
266
+ rpc_url = "" # self mode: private RPC. Defaults to network.rpc_url if empty.
267
+
268
+ [relay]
269
+ enabled = true
270
+ listen_port = 4402 # Port for incoming relay messages
271
+ endpoint = "" # Your public URL — run: arc402 setup endpoint
272
+ # Example: https://gigabrain.arc402.xyz
273
+ max_concurrent_agreements = 10 # Refuse new hire requests when this many are in-flight
274
+ poll_interval_seconds = 2 # How often to poll relay for incoming messages
275
+ relay_url = "" # The relay to poll. Defaults to agent metadata relay if empty.
276
+
277
+ [watchtower]
278
+ enabled = true
279
+ poll_interval_seconds = 60 # How often to poll chain for stale-close events
280
+ challenge_confirmation_blocks = 2 # Wait N block confirmations before accepting close as final
281
+ external_watchtower_url = "" # Register open channels here as backup (Tier 2 watchtower)
282
+ update_interval_states = 10 # Forward state to external watchtower every N state changes
283
+
284
+ [policy]
285
+ auto_accept = false # If true: auto-accept all hire requests within policy bounds
286
+ max_price_eth = "0.1" # Refuse any hire priced above this (ETH)
287
+ allowed_capabilities = [] # Empty list = accept any capability. Non-empty = whitelist.
288
+ require_min_trust_score = 50 # Refuse hirers whose wallet trust score is below this (0–100)
289
+ min_hire_lead_time_seconds = 300 # Refuse hires with delivery deadline < this many seconds away
290
+
291
+ [notifications]
292
+ telegram_bot_token = "env:TELEGRAM_BOT_TOKEN" # Load from env, not hardcoded
293
+ telegram_chat_id = "env:TELEGRAM_CHAT_ID" # Load from env, not hardcoded
294
+ notify_on_hire_request = true # Notify when a hire request arrives (pending approval)
295
+ notify_on_hire_accepted = true # Notify when daemon accepts a hire
296
+ notify_on_hire_rejected = true # Notify when daemon rejects a hire
297
+ notify_on_delivery = true # Notify when work is delivered and fulfill() submitted
298
+ notify_on_dispute = true # Notify when a dispute is raised (by either party)
299
+ notify_on_channel_challenge = true # Notify when watchtower submits a channel challenge
300
+ notify_on_low_balance = false # Disabled by default — enable if you want balance alerts
301
+ low_balance_threshold_eth = "0.005" # Balance alert threshold
302
+
303
+ [work]
304
+ handler = "noop" # exec | http | noop
305
+ exec_command = "" # called with agreementId and spec as args (exec mode)
306
+ http_url = "" # POST {agreementId, specHash, deadline} as JSON (http mode)
307
+ http_auth_token = "env:WORKER_AUTH_TOKEN"
308
+ `;
@@ -0,0 +1,226 @@
1
+ /**
2
+ * Hire listener — polls relay for incoming hire proposals,
3
+ * evaluates against policy, and queues for approval or auto-accepts.
4
+ */
5
+ import * as http from "http";
6
+ import * as https from "https";
7
+ import { ethers } from "ethers";
8
+ import type { DaemonConfig } from "./config";
9
+ import type { DaemonDB } from "./index";
10
+ import type { Notifier } from "./notify";
11
+
12
+ export interface HireProposal {
13
+ messageId: string;
14
+ hirerAddress: string;
15
+ capability: string;
16
+ priceEth: string;
17
+ deadlineUnix: number;
18
+ specHash: string;
19
+ agreementId?: string;
20
+ signature?: string;
21
+ }
22
+
23
+ export interface PolicyResult {
24
+ allowed: boolean;
25
+ reason?: string;
26
+ }
27
+
28
+ function relayGet(
29
+ relayUrl: string,
30
+ urlPath: string
31
+ ): Promise<{ status: number; data: unknown }> {
32
+ return new Promise((resolve, reject) => {
33
+ const parsed = new URL(urlPath, relayUrl);
34
+ const isHttps = parsed.protocol === "https:";
35
+ const mod = isHttps ? https : http;
36
+ const options: http.RequestOptions = {
37
+ hostname: parsed.hostname,
38
+ port: parsed.port || (isHttps ? 443 : 80),
39
+ path: parsed.pathname + (parsed.search || ""),
40
+ method: "GET",
41
+ headers: { "Content-Type": "application/json" },
42
+ };
43
+ const req = mod.request(options, (res) => {
44
+ let raw = "";
45
+ res.on("data", (c: Buffer) => { raw += c.toString(); });
46
+ res.on("end", () => {
47
+ try {
48
+ resolve({ status: res.statusCode ?? 0, data: JSON.parse(raw) });
49
+ } catch {
50
+ resolve({ status: res.statusCode ?? 0, data: raw });
51
+ }
52
+ });
53
+ });
54
+ req.on("error", reject);
55
+ req.end();
56
+ });
57
+ }
58
+
59
+ export function evaluatePolicy(
60
+ proposal: HireProposal,
61
+ config: DaemonConfig,
62
+ activeCount: number
63
+ ): PolicyResult {
64
+ const policy = config.policy;
65
+
66
+ // Concurrency check
67
+ if (activeCount >= config.relay.max_concurrent_agreements) {
68
+ return { allowed: false, reason: "at_capacity" };
69
+ }
70
+
71
+ // Price check
72
+ try {
73
+ const priceWei = ethers.parseEther(proposal.priceEth || "0");
74
+ const maxWei = ethers.parseEther(policy.max_price_eth);
75
+ if (priceWei > maxWei) {
76
+ return { allowed: false, reason: "price_exceeds_policy" };
77
+ }
78
+ } catch {
79
+ return { allowed: false, reason: "invalid_price" };
80
+ }
81
+
82
+ // Capability check (empty list = accept all)
83
+ if (policy.allowed_capabilities.length > 0 && proposal.capability) {
84
+ if (!policy.allowed_capabilities.includes(proposal.capability)) {
85
+ return { allowed: false, reason: "capability_not_allowed" };
86
+ }
87
+ }
88
+
89
+ // Deadline check
90
+ const now = Math.floor(Date.now() / 1000);
91
+ if (proposal.deadlineUnix > 0) {
92
+ if (proposal.deadlineUnix < now + policy.min_hire_lead_time_seconds) {
93
+ return { allowed: false, reason: "deadline_too_soon" };
94
+ }
95
+ }
96
+
97
+ return { allowed: true };
98
+ }
99
+
100
+ function parseProposal(msg: Record<string, unknown>): HireProposal | null {
101
+ const payload = (msg.payload ?? msg) as Record<string, unknown>;
102
+ if (!payload.hirerAddress && !payload.hirer_address && !payload.from) return null;
103
+
104
+ return {
105
+ messageId: String(msg.messageId ?? msg.id ?? `msg_${Date.now()}`),
106
+ hirerAddress: String(payload.hirerAddress ?? payload.hirer_address ?? msg.from ?? ""),
107
+ capability: String(payload.capability ?? ""),
108
+ priceEth: String(payload.priceEth ?? payload.price_eth ?? "0"),
109
+ deadlineUnix: Number(payload.deadlineUnix ?? payload.deadline ?? 0),
110
+ specHash: String(payload.specHash ?? payload.spec_hash ?? ""),
111
+ agreementId: payload.agreementId ? String(payload.agreementId) : undefined,
112
+ signature: payload.signature ? String(payload.signature) : undefined,
113
+ };
114
+ }
115
+
116
+ export class HireListener {
117
+ private config: DaemonConfig;
118
+ private db: DaemonDB;
119
+ private notifier: Notifier;
120
+ private walletAddress: string;
121
+ private lastSeenMessageId: string | null = null;
122
+ private onApprove: ((hireId: string) => Promise<void>) | null = null;
123
+
124
+ constructor(
125
+ config: DaemonConfig,
126
+ db: DaemonDB,
127
+ notifier: Notifier,
128
+ walletAddress: string
129
+ ) {
130
+ this.config = config;
131
+ this.db = db;
132
+ this.notifier = notifier;
133
+ this.walletAddress = walletAddress;
134
+ }
135
+
136
+ setApproveCallback(cb: (hireId: string) => Promise<void>): void {
137
+ this.onApprove = cb;
138
+ }
139
+
140
+ async poll(): Promise<void> {
141
+ const relayUrl = this.config.relay.relay_url;
142
+ if (!relayUrl) return;
143
+
144
+ try {
145
+ const qs =
146
+ `?address=${encodeURIComponent(this.walletAddress)}` +
147
+ (this.lastSeenMessageId ? `&since=${encodeURIComponent(this.lastSeenMessageId)}` : "");
148
+
149
+ const result = await relayGet(relayUrl, `/poll${qs}`);
150
+ const data = result.data as { messages?: Array<Record<string, unknown>> };
151
+ const messages = data.messages ?? [];
152
+
153
+ for (const msg of messages) {
154
+ this.lastSeenMessageId = String(msg.messageId ?? "");
155
+ await this.handleMessage(msg);
156
+ }
157
+ } catch {
158
+ // Transient relay failure — retry next poll
159
+ }
160
+ }
161
+
162
+ private async handleMessage(msg: Record<string, unknown>): Promise<void> {
163
+ const proposal = parseProposal(msg);
164
+ if (!proposal) return;
165
+
166
+ // Dedup — skip if already in DB
167
+ const existing = this.db.getHireRequest(proposal.messageId);
168
+ if (existing) return;
169
+
170
+ // Count active agreements
171
+ const activeCount = this.db.countActiveHireRequests();
172
+
173
+ // Policy evaluation
174
+ const policyResult = evaluatePolicy(proposal, this.config, activeCount);
175
+
176
+ if (!policyResult.allowed) {
177
+ // Reject
178
+ const hireId = proposal.messageId;
179
+ this.db.insertHireRequest({
180
+ id: hireId,
181
+ agreement_id: proposal.agreementId ?? null,
182
+ hirer_address: proposal.hirerAddress,
183
+ capability: proposal.capability,
184
+ price_eth: proposal.priceEth,
185
+ deadline_unix: proposal.deadlineUnix,
186
+ spec_hash: proposal.specHash,
187
+ status: "rejected",
188
+ reject_reason: policyResult.reason ?? "policy_violation",
189
+ });
190
+
191
+ if (this.config.notifications.notify_on_hire_rejected) {
192
+ await this.notifier.notifyHireRejected(hireId, policyResult.reason ?? "policy_violation");
193
+ }
194
+ return;
195
+ }
196
+
197
+ // Insert as pending_approval or auto-accept
198
+ const hireId = proposal.messageId;
199
+ const status = this.config.policy.auto_accept ? "accepted" : "pending_approval";
200
+
201
+ this.db.insertHireRequest({
202
+ id: hireId,
203
+ agreement_id: proposal.agreementId ?? null,
204
+ hirer_address: proposal.hirerAddress,
205
+ capability: proposal.capability,
206
+ price_eth: proposal.priceEth,
207
+ deadline_unix: proposal.deadlineUnix,
208
+ spec_hash: proposal.specHash,
209
+ status,
210
+ reject_reason: null,
211
+ });
212
+
213
+ if (status === "pending_approval") {
214
+ if (this.config.notifications.notify_on_hire_request) {
215
+ await this.notifier.notifyHireRequest(
216
+ hireId,
217
+ proposal.hirerAddress,
218
+ proposal.priceEth,
219
+ proposal.capability
220
+ );
221
+ }
222
+ } else if (status === "accepted" && this.onApprove) {
223
+ await this.onApprove(hireId);
224
+ }
225
+ }
226
+ }