arcbounty-agent-sdk 0.1.0 → 0.3.1

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/dist/index.js CHANGED
@@ -30,15 +30,18 @@ __export(index_exports, {
30
30
  fetchIpfsJson: () => fetchIpfsJson,
31
31
  fetchIpfsText: () => fetchIpfsText,
32
32
  isPinningConfigured: () => isPinningConfigured,
33
+ matchesBountyFilter: () => matchesBountyFilter,
34
+ parseUsdc: () => parseUsdc,
33
35
  pinAgentMetadata: () => pinAgentMetadata,
34
36
  pinText: () => pinText,
35
- validateAgentMetadata: () => validateAgentMetadata
37
+ resolveDeadline: () => resolveDeadline,
38
+ validateAgentMetadata: () => validateAgentMetadata,
39
+ workerBondFor: () => workerBondFor
36
40
  });
37
41
  module.exports = __toCommonJS(index_exports);
38
42
 
39
43
  // src/ArcBountyAgent.ts
40
- var import_viem = require("viem");
41
- var import_accounts = require("viem/accounts");
44
+ var import_viem3 = require("viem");
42
45
 
43
46
  // src/abi.ts
44
47
  var BOUNTY_META_TUPLE = {
@@ -68,7 +71,9 @@ var BOUNTY_META_TUPLE = {
68
71
  { name: "disputeRaisedAt", type: "uint256" },
69
72
  { name: "disputeReasonHash", type: "string" },
70
73
  { name: "disputeResponseHash", type: "string" },
71
- { name: "disputeRulingHash", type: "string" }
74
+ { name: "disputeRulingHash", type: "string" },
75
+ { name: "requireWorkerBond", type: "bool" },
76
+ { name: "workerBond", type: "uint256" }
72
77
  ]
73
78
  };
74
79
  var BOUNTY_ADAPTER_ABI = [
@@ -88,7 +93,8 @@ var BOUNTY_ADAPTER_ABI = [
88
93
  { name: "category", type: "string" },
89
94
  { name: "tags", type: "string[]" },
90
95
  { name: "agentOnly", type: "bool" },
91
- { name: "humanOnly", type: "bool" }
96
+ { name: "humanOnly", type: "bool" },
97
+ { name: "requireWorkerBond", type: "bool" }
92
98
  ]
93
99
  }],
94
100
  outputs: [{ name: "jobId", type: "uint256" }]
@@ -210,7 +216,27 @@ var BOUNTY_ADAPTER_ABI = [
210
216
  inputs: [{ name: "jobId", type: "uint256" }],
211
217
  outputs: []
212
218
  },
219
+ {
220
+ // V3.3 — permissionless neutral 50/50 split if the arbitrator never rules
221
+ // after both parties have submitted evidence (claimDefaultRuling doesn't
222
+ // apply once a response exists). See ARBITRATOR_TIMEOUT (30d).
223
+ name: "claimArbitratorTimeout",
224
+ type: "function",
225
+ stateMutability: "nonpayable",
226
+ inputs: [{ name: "jobId", type: "uint256" }],
227
+ outputs: []
228
+ },
213
229
  // ── Read ──
230
+ {
231
+ // Public array getter — needed to scan the full bounty set (expireStale,
232
+ // keeper-style scripts). Not paginated on-chain; callers should bound
233
+ // their own scan via totalBounties().
234
+ name: "allJobIds",
235
+ type: "function",
236
+ stateMutability: "view",
237
+ inputs: [{ name: "", type: "uint256" }],
238
+ outputs: [{ name: "", type: "uint256" }]
239
+ },
214
240
  {
215
241
  name: "getOpenBounties",
216
242
  type: "function",
@@ -300,6 +326,34 @@ var BOUNTY_ADAPTER_ABI = [
300
326
  inputs: [],
301
327
  outputs: [{ name: "", type: "uint256" }]
302
328
  },
329
+ {
330
+ name: "ARBITRATOR_TIMEOUT",
331
+ type: "function",
332
+ stateMutability: "view",
333
+ inputs: [],
334
+ outputs: [{ name: "", type: "uint256" }]
335
+ },
336
+ {
337
+ name: "uniquePosterCount",
338
+ type: "function",
339
+ stateMutability: "view",
340
+ inputs: [{ name: "agentId", type: "uint256" }],
341
+ outputs: [{ name: "", type: "uint256" }]
342
+ },
343
+ {
344
+ name: "WORKER_BOND_BPS",
345
+ type: "function",
346
+ stateMutability: "view",
347
+ inputs: [],
348
+ outputs: [{ name: "", type: "uint256" }]
349
+ },
350
+ {
351
+ name: "MIN_WORKER_BOND",
352
+ type: "function",
353
+ stateMutability: "view",
354
+ inputs: [],
355
+ outputs: [{ name: "", type: "uint256" }]
356
+ },
303
357
  // ── Events ──
304
358
  {
305
359
  name: "BountyCreated",
@@ -366,6 +420,42 @@ var BOUNTY_ADAPTER_ABI = [
366
420
  { name: "rulingHash", type: "string", indexed: false },
367
421
  { name: "defaultRuling", type: "bool", indexed: false }
368
422
  ]
423
+ },
424
+ {
425
+ name: "ArbitratorTimeoutClaimed",
426
+ type: "event",
427
+ inputs: [
428
+ { name: "jobId", type: "uint256", indexed: true },
429
+ { name: "posterAmount", type: "uint256", indexed: false },
430
+ { name: "providerAmount", type: "uint256", indexed: false }
431
+ ]
432
+ },
433
+ {
434
+ name: "WorkerBondPosted",
435
+ type: "event",
436
+ inputs: [
437
+ { name: "jobId", type: "uint256", indexed: true },
438
+ { name: "worker", type: "address", indexed: true },
439
+ { name: "amount", type: "uint256", indexed: false }
440
+ ]
441
+ },
442
+ {
443
+ name: "WorkerBondRefunded",
444
+ type: "event",
445
+ inputs: [
446
+ { name: "jobId", type: "uint256", indexed: true },
447
+ { name: "worker", type: "address", indexed: true },
448
+ { name: "amount", type: "uint256", indexed: false }
449
+ ]
450
+ },
451
+ {
452
+ name: "WorkerBondForfeited",
453
+ type: "event",
454
+ inputs: [
455
+ { name: "jobId", type: "uint256", indexed: true },
456
+ { name: "poster", type: "address", indexed: true },
457
+ { name: "amount", type: "uint256", indexed: false }
458
+ ]
369
459
  }
370
460
  ];
371
461
  var IDENTITY_REGISTRY_ABI = [
@@ -423,6 +513,61 @@ var ERC20_ABI = [
423
513
  }
424
514
  ];
425
515
 
516
+ // src/signers/viemSigner.ts
517
+ var import_viem = require("viem");
518
+ var import_accounts = require("viem/accounts");
519
+ var ViemSigner = class {
520
+ constructor(privateKey, chain, rpcUrl) {
521
+ this.account = (0, import_accounts.privateKeyToAccount)(privateKey);
522
+ this.address = this.account.address;
523
+ this.walletClient = (0, import_viem.createWalletClient)({ account: this.account, chain, transport: (0, import_viem.http)(rpcUrl) });
524
+ }
525
+ async writeContract(params) {
526
+ return this.walletClient.writeContract({
527
+ address: params.address,
528
+ abi: params.abi,
529
+ functionName: params.functionName,
530
+ args: params.args,
531
+ chain: null,
532
+ account: this.account
533
+ });
534
+ }
535
+ };
536
+
537
+ // src/signers/circleSigner.ts
538
+ var import_viem2 = require("viem");
539
+ var import_developer_controlled_wallets = require("@circle-fin/developer-controlled-wallets");
540
+ var CircleSigner = class {
541
+ constructor(config) {
542
+ this.address = config.address;
543
+ this.walletId = config.walletId;
544
+ this.client = (0, import_developer_controlled_wallets.initiateDeveloperControlledWalletsClient)({
545
+ apiKey: config.apiKey,
546
+ entitySecret: config.entitySecret,
547
+ baseUrl: config.baseUrl
548
+ });
549
+ }
550
+ async writeContract(params) {
551
+ const callData = (0, import_viem2.encodeFunctionData)({
552
+ abi: params.abi,
553
+ functionName: params.functionName,
554
+ args: params.args
555
+ });
556
+ const created = await this.client.createContractExecutionTransaction({
557
+ walletId: this.walletId,
558
+ contractAddress: params.address,
559
+ callData,
560
+ fee: { type: "level", config: { feeLevel: "MEDIUM" } }
561
+ });
562
+ const txId = created.data?.id;
563
+ if (!txId) {
564
+ throw new Error("CircleSigner: createContractExecutionTransaction did not return a transaction id");
565
+ }
566
+ const result = await this.client.getTransaction({ id: txId, waitForTxHash: true });
567
+ return result.data.transaction.txHash;
568
+ }
569
+ };
570
+
426
571
  // src/constants.ts
427
572
  var ARC_TESTNET_RPC = "https://rpc.testnet.arc.network";
428
573
  var ARC_TESTNET_CHAIN_ID = 5042002;
@@ -503,8 +648,46 @@ async function pinText(content, filename = "result.md") {
503
648
  throw new Error("Set PINATA_JWT (preferred) or PINATA_API_KEY + PINATA_SECRET to pin to IPFS");
504
649
  }
505
650
 
651
+ // src/logic.ts
652
+ function parseUsdc(dollars) {
653
+ return BigInt(Math.round(dollars * 10 ** USDC_DECIMALS));
654
+ }
655
+ function resolveDeadline(d, nowSec = Math.floor(Date.now() / 1e3)) {
656
+ if (d instanceof Date) return BigInt(Math.floor(d.getTime() / 1e3));
657
+ if (d < 1e9) return BigInt(nowSec + d);
658
+ return BigInt(d);
659
+ }
660
+ function workerBondFor(reward, bondBps = 1500n, minBond = 500000n) {
661
+ const pct = reward * bondBps / 10000n;
662
+ return pct > minBond ? pct : minBond;
663
+ }
664
+ function matchesBountyFilter(m, f) {
665
+ if (f.category && m.category !== f.category) return false;
666
+ if (f.agentOnly === true && !m.agentOnly) return false;
667
+ if (f.humanOnly === true && !m.humanOnly) return false;
668
+ if (f.agentOnly === false && m.agentOnly) return false;
669
+ if (f.humanOnly === false && m.humanOnly) return false;
670
+ if (f.maxReward !== void 0 && m.reward > parseUsdc(f.maxReward)) return false;
671
+ if (f.minReward !== void 0 && m.reward < parseUsdc(f.minReward)) return false;
672
+ return true;
673
+ }
674
+ var TRANSFER_SIG = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
675
+ var ZERO_TOPIC = "0x" + "0".repeat(64);
676
+ function agentIdFromReceiptLogs(logs, registryAddress, selfAddress) {
677
+ const me = selfAddress.toLowerCase().slice(2).padStart(64, "0");
678
+ for (const log of logs) {
679
+ if (log.address.toLowerCase() !== registryAddress.toLowerCase()) continue;
680
+ if (log.topics.length < 4) continue;
681
+ if (log.topics[0]?.toLowerCase() !== TRANSFER_SIG) continue;
682
+ if (log.topics[1]?.toLowerCase() !== ZERO_TOPIC) continue;
683
+ if (log.topics[2]?.toLowerCase() !== "0x" + me) continue;
684
+ return BigInt(log.topics[3]);
685
+ }
686
+ return null;
687
+ }
688
+
506
689
  // src/ArcBountyAgent.ts
507
- var arcTestnet = (0, import_viem.defineChain)({
690
+ var arcTestnet = (0, import_viem3.defineChain)({
508
691
  id: ARC_TESTNET_CHAIN_ID,
509
692
  name: "Arc Testnet",
510
693
  nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 6 },
@@ -514,8 +697,8 @@ var ArcBountyAgent = class {
514
697
  constructor(config) {
515
698
  this._agentId = null;
516
699
  const rpcUrl = config.rpcUrl ?? ARC_TESTNET_RPC;
517
- this.chain = (0, import_viem.defineChain)({ ...arcTestnet, rpcUrls: { default: { http: [rpcUrl] } } });
518
- this.account = (0, import_accounts.privateKeyToAccount)(config.privateKey);
700
+ this.chain = (0, import_viem3.defineChain)({ ...arcTestnet, rpcUrls: { default: { http: [rpcUrl] } } });
701
+ this.signer = config.circleWallet ? new CircleSigner(config.circleWallet) : new ViemSigner(config.privateKey, this.chain, rpcUrl);
519
702
  this.metadataURI = config.metadataURI ?? "";
520
703
  const rawAdapter = config.bountyAdapterAddress ?? process.env["BOUNTY_ADAPTER_ADDRESS"];
521
704
  if (!rawAdapter) {
@@ -523,15 +706,14 @@ var ArcBountyAgent = class {
523
706
  "ArcBountyAgent: bountyAdapterAddress is required (constructor option or BOUNTY_ADAPTER_ADDRESS env). See agent-sdk/.env.example. Source of truth: contracts/DEPLOYMENTS.md."
524
707
  );
525
708
  }
526
- if (!(0, import_viem.isAddress)(rawAdapter) || rawAdapter.toLowerCase() === ZERO_ADDRESS.toLowerCase()) {
709
+ if (!(0, import_viem3.isAddress)(rawAdapter) || rawAdapter.toLowerCase() === ZERO_ADDRESS.toLowerCase()) {
527
710
  throw new Error(`ArcBountyAgent: invalid bountyAdapterAddress: ${rawAdapter}`);
528
711
  }
529
712
  this.bountyAdapter = rawAdapter;
530
- this.publicClient = (0, import_viem.createPublicClient)({ chain: this.chain, transport: (0, import_viem.http)(rpcUrl) });
531
- this.walletClient = (0, import_viem.createWalletClient)({ account: this.account, chain: this.chain, transport: (0, import_viem.http)(rpcUrl) });
713
+ this.publicClient = (0, import_viem3.createPublicClient)({ chain: this.chain, transport: (0, import_viem3.http)(rpcUrl) });
532
714
  }
533
715
  get address() {
534
- return this.account.address;
716
+ return this.signer.address;
535
717
  }
536
718
  // ─── Identity ───────────────────────────────────────────────────────────────
537
719
  async register() {
@@ -540,34 +722,18 @@ var ArcBountyAgent = class {
540
722
  this._agentId = existing;
541
723
  return existing;
542
724
  }
543
- const hash = await this.walletClient.writeContract({
725
+ const hash = await this.signer.writeContract({
544
726
  address: CONTRACTS.IDENTITY_REGISTRY,
545
727
  abi: IDENTITY_REGISTRY_ABI,
546
728
  functionName: "register",
547
- args: [this.metadataURI],
548
- chain: null,
549
- account: this.account
729
+ args: [this.metadataURI]
550
730
  });
551
731
  const receipt = await this.publicClient.waitForTransactionReceipt({ hash });
552
- const agentId = this._agentIdFromReceiptLogs(receipt.logs);
732
+ const agentId = agentIdFromReceiptLogs(receipt.logs, CONTRACTS.IDENTITY_REGISTRY, this.signer.address);
553
733
  if (agentId === null) throw new Error("Registration succeeded but agentId not found in events");
554
734
  this._agentId = agentId;
555
735
  return agentId;
556
736
  }
557
- /** Pull the minted tokenId from a Transfer(from=0x0, to=self) log in a receipt. */
558
- _agentIdFromReceiptLogs(logs) {
559
- const TRANSFER_SIG = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
560
- const me = this.account.address.toLowerCase().slice(2).padStart(64, "0");
561
- for (const log of logs) {
562
- if (log.address.toLowerCase() !== CONTRACTS.IDENTITY_REGISTRY.toLowerCase()) continue;
563
- if (log.topics.length < 4) continue;
564
- if (log.topics[0]?.toLowerCase() !== TRANSFER_SIG) continue;
565
- if (log.topics[1]?.toLowerCase() !== "0x" + "0".repeat(64)) continue;
566
- if (log.topics[2]?.toLowerCase() !== "0x" + me) continue;
567
- return BigInt(log.topics[3]);
568
- }
569
- return null;
570
- }
571
737
  get agentId() {
572
738
  if (this._agentId === null) throw new Error("Agent not registered. Call register() first.");
573
739
  return this._agentId;
@@ -593,15 +759,7 @@ var ArcBountyAgent = class {
593
759
  args: [category, BigInt(offset), BigInt(limit)]
594
760
  });
595
761
  const metas = await Promise.all(jobIds.map((jobId) => this.getBounty(jobId)));
596
- return metas.filter((m) => {
597
- if (agentOnly === true && !m.agentOnly) return false;
598
- if (humanOnly === true && !m.humanOnly) return false;
599
- if (agentOnly === false && m.agentOnly) return false;
600
- if (humanOnly === false && m.humanOnly) return false;
601
- if (maxReward !== void 0 && m.reward > this._parseUsdc(maxReward)) return false;
602
- if (minReward !== void 0 && m.reward < this._parseUsdc(minReward)) return false;
603
- return true;
604
- });
762
+ return metas.filter((m) => matchesBountyFilter(m, { agentOnly, humanOnly, maxReward, minReward }));
605
763
  }
606
764
  async getBounty(jobId) {
607
765
  const raw = await this.publicClient.readContract({
@@ -642,11 +800,11 @@ var ArcBountyAgent = class {
642
800
  if (!opts.descriptionCid && !opts.descriptionText) {
643
801
  throw new Error("Provide either descriptionCid or descriptionText");
644
802
  }
645
- const reward = this._parseUsdc(opts.rewardUsdc);
646
- const deadline = this._resolveDeadline(opts.deadline);
803
+ const reward = parseUsdc(opts.rewardUsdc);
804
+ const deadline = resolveDeadline(opts.deadline);
647
805
  const descCid = opts.descriptionCid ?? await pinText(opts.descriptionText);
648
806
  await this._ensureUsdcAllowance(reward);
649
- const hash = await this.walletClient.writeContract({
807
+ const hash = await this.signer.writeContract({
650
808
  address: this.bountyAdapter,
651
809
  abi: BOUNTY_ADAPTER_ABI,
652
810
  functionName: "createBounty",
@@ -658,17 +816,16 @@ var ArcBountyAgent = class {
658
816
  category: opts.category,
659
817
  tags: opts.tags ?? [],
660
818
  agentOnly: opts.agentOnly ?? false,
661
- humanOnly: opts.humanOnly ?? false
662
- }],
663
- chain: null,
664
- account: this.account
819
+ humanOnly: opts.humanOnly ?? false,
820
+ requireWorkerBond: opts.requireWorkerBond ?? false
821
+ }]
665
822
  });
666
823
  const receipt = await this.publicClient.waitForTransactionReceipt({ hash });
667
824
  let jobId;
668
825
  for (const log of receipt.logs) {
669
826
  if (log.address.toLowerCase() !== this.bountyAdapter.toLowerCase()) continue;
670
827
  try {
671
- const decoded = (0, import_viem.decodeEventLog)({
828
+ const decoded = (0, import_viem3.decodeEventLog)({
672
829
  abi: BOUNTY_ADAPTER_ABI,
673
830
  data: log.data,
674
831
  topics: log.topics
@@ -685,32 +842,30 @@ var ArcBountyAgent = class {
685
842
  // ─── Take / submit ──────────────────────────────────────────────────────────
686
843
  async takeBounty(jobId) {
687
844
  const agentId = this._agentId ?? 0n;
688
- const hash = await this.walletClient.writeContract({
689
- address: this.bountyAdapter,
690
- abi: BOUNTY_ADAPTER_ABI,
691
- functionName: "takeBounty",
692
- args: [jobId, agentId],
693
- chain: null,
694
- account: this.account
695
- });
696
- await this._waitForTx(hash);
697
- return { hash };
845
+ const meta = await this.getBounty(jobId);
846
+ if (meta.requireWorkerBond) {
847
+ const [bondBps, minBond] = await Promise.all([
848
+ this.publicClient.readContract({
849
+ address: this.bountyAdapter,
850
+ abi: BOUNTY_ADAPTER_ABI,
851
+ functionName: "WORKER_BOND_BPS"
852
+ }),
853
+ this.publicClient.readContract({
854
+ address: this.bountyAdapter,
855
+ abi: BOUNTY_ADAPTER_ABI,
856
+ functionName: "MIN_WORKER_BOND"
857
+ })
858
+ ]);
859
+ await this._ensureUsdcAllowance(workerBondFor(meta.reward, bondBps, minBond));
860
+ }
861
+ return this._writeAdapter("takeBounty", [jobId, agentId]);
698
862
  }
699
863
  async submitWork(jobId, options) {
700
864
  if (!options.text && !options.cid) {
701
865
  throw new Error("Provide either text or cid");
702
866
  }
703
867
  const cid = options.cid ?? await pinText(options.text);
704
- const hash = await this.walletClient.writeContract({
705
- address: this.bountyAdapter,
706
- abi: BOUNTY_ADAPTER_ABI,
707
- functionName: "submitWork",
708
- args: [jobId, cid],
709
- chain: null,
710
- account: this.account
711
- });
712
- await this._waitForTx(hash);
713
- return { hash };
868
+ return this._writeAdapter("submitWork", [jobId, cid]);
714
869
  }
715
870
  // ─── Poster cycle ───────────────────────────────────────────────────────────
716
871
  // These let a protocol/DAO agent run the full poster side end-to-end.
@@ -751,75 +906,67 @@ var ArcBountyAgent = class {
751
906
  async claimDefaultRuling(jobId) {
752
907
  return this._writeAdapter("claimDefaultRuling", [jobId]);
753
908
  }
909
+ /**
910
+ * V3.3 liveness fallback: if the respondent DID reply (so claimDefaultRuling
911
+ * no longer applies) but the arbitrator never called resolveDispute within
912
+ * ARBITRATOR_TIMEOUT (30d) of disputeRaisedAt, anyone may trigger a neutral
913
+ * 50/50 split between poster and worker. No reputation penalty either way.
914
+ */
915
+ async claimArbitratorTimeout(jobId) {
916
+ return this._writeAdapter("claimArbitratorTimeout", [jobId]);
917
+ }
754
918
  // ─── Dispute flow (worker-side) ─────────────────────────────────────────────
755
919
  /** Worker challenges a pending rejection — flips bounty into dispute with worker as initiator. */
756
920
  async challengeRejection(jobId, evidence) {
757
921
  const cid = await this._resolveEvidenceCid(evidence);
758
- const hash = await this.walletClient.writeContract({
759
- address: this.bountyAdapter,
760
- abi: BOUNTY_ADAPTER_ABI,
761
- functionName: "challengeRejection",
762
- args: [jobId, cid],
763
- chain: null,
764
- account: this.account
765
- });
766
- await this._waitForTx(hash);
767
- return { hash };
922
+ return this._writeAdapter("challengeRejection", [jobId, cid]);
768
923
  }
769
924
  /** Open a dispute (either party — after submission, before resolution). */
770
925
  async disputeBounty(jobId, evidence) {
771
926
  const cid = await this._resolveEvidenceCid(evidence);
772
- const hash = await this.walletClient.writeContract({
773
- address: this.bountyAdapter,
774
- abi: BOUNTY_ADAPTER_ABI,
775
- functionName: "disputeBounty",
776
- args: [jobId, cid],
777
- chain: null,
778
- account: this.account
779
- });
780
- await this._waitForTx(hash);
781
- return { hash };
927
+ return this._writeAdapter("disputeBounty", [jobId, cid]);
782
928
  }
783
929
  /** Respond to an open dispute (only the non-initiator may call). */
784
930
  async respondToDispute(jobId, evidence) {
785
931
  const cid = await this._resolveEvidenceCid(evidence);
786
- const hash = await this.walletClient.writeContract({
787
- address: this.bountyAdapter,
788
- abi: BOUNTY_ADAPTER_ABI,
789
- functionName: "respondToDispute",
790
- args: [jobId, cid],
791
- chain: null,
792
- account: this.account
793
- });
794
- await this._waitForTx(hash);
795
- return { hash };
932
+ return this._writeAdapter("respondToDispute", [jobId, cid]);
796
933
  }
797
934
  // ─── Expire stale bounties ──────────────────────────────────────────────────
935
+ /**
936
+ * Scans the full bounty set and calls expireBounty() on anything past its
937
+ * deadline with no submission and not yet resolved. Stops after finding
938
+ * `limit` candidates to expire.
939
+ *
940
+ * NOTE: `getOpenBounties` (used pre-V3.3) can NEVER return a candidate for
941
+ * this — it excludes any bounty whose deadline has already passed by
942
+ * definition (`_isOpenMatch` checks `block.timestamp <= deadline`). This
943
+ * scan walks `allJobIds` directly instead, mirroring the keeper cron route
944
+ * (`frontend/app/api/cron/keeper/route.ts`).
945
+ */
798
946
  async expireStale(category = "", limit = 100) {
799
- const jobIds = await this.publicClient.readContract({
947
+ const total = await this.publicClient.readContract({
800
948
  address: this.bountyAdapter,
801
949
  abi: BOUNTY_ADAPTER_ABI,
802
- functionName: "getOpenBounties",
803
- args: [category, 0n, BigInt(limit)]
950
+ functionName: "totalBounties"
804
951
  });
805
952
  const now = BigInt(Math.floor(Date.now() / 1e3));
806
953
  const expired = [];
807
- for (const jobId of jobIds) {
954
+ for (let i = 0n; i < total && expired.length < limit; i++) {
955
+ const jobId = await this.publicClient.readContract({
956
+ address: this.bountyAdapter,
957
+ abi: BOUNTY_ADAPTER_ABI,
958
+ functionName: "allJobIds",
959
+ args: [i]
960
+ });
808
961
  const meta = await this.getBounty(jobId);
809
- if (meta.deadline < now) {
810
- try {
811
- const hash = await this.walletClient.writeContract({
812
- address: this.bountyAdapter,
813
- abi: BOUNTY_ADAPTER_ABI,
814
- functionName: "expireBounty",
815
- args: [jobId],
816
- chain: null,
817
- account: this.account
818
- });
819
- await this._waitForTx(hash);
820
- expired.push(jobId);
821
- } catch {
822
- }
962
+ if (meta.resolved) continue;
963
+ if (meta.submittedResultHash.length > 0) continue;
964
+ if (category && meta.category !== category) continue;
965
+ if (meta.deadline >= now) continue;
966
+ try {
967
+ await this._writeAdapter("expireBounty", [jobId]);
968
+ expired.push(jobId);
969
+ } catch {
823
970
  }
824
971
  }
825
972
  return expired;
@@ -839,6 +986,21 @@ var ArcBountyAgent = class {
839
986
  return { averageScore: 0n, totalFeedbacks: 0n, totalJobs: 0n };
840
987
  }
841
988
  }
989
+ /**
990
+ * V4 anti-Sybil signal: count of distinct posters who've actually paid out
991
+ * a completed bounty to this agent. Costs N real funded wallets to fake N —
992
+ * unlike the raw ERC-8004 average score, which one alt account can inflate
993
+ * for a few cents. See V4_DESIGN_ANTI_SYBIL.md.
994
+ */
995
+ async getUniquePosterCount(agentId) {
996
+ const id = agentId ?? this.agentId;
997
+ return this.publicClient.readContract({
998
+ address: this.bountyAdapter,
999
+ abi: BOUNTY_ADAPTER_ABI,
1000
+ functionName: "uniquePosterCount",
1001
+ args: [id]
1002
+ });
1003
+ }
842
1004
  async getAgentInfo() {
843
1005
  const id = this.agentId;
844
1006
  const reputation = await this.getReputation(id);
@@ -886,7 +1048,7 @@ var ArcBountyAgent = class {
886
1048
  seen.add(key);
887
1049
  try {
888
1050
  const meta = await this.getBounty(jobId);
889
- if (!this._matchesFilter(meta, filter)) continue;
1051
+ if (!matchesBountyFilter(meta, filter)) continue;
890
1052
  await onMatch(meta);
891
1053
  } catch (err) {
892
1054
  console.error(`[ArcBountyAgent] onMatch error for #${key}:`, err);
@@ -897,15 +1059,110 @@ var ArcBountyAgent = class {
897
1059
  });
898
1060
  return unwatch;
899
1061
  }
900
- _matchesFilter(m, f) {
901
- if (f.category && m.category !== f.category) return false;
902
- if (f.agentOnly === true && !m.agentOnly) return false;
903
- if (f.humanOnly === true && !m.humanOnly) return false;
904
- if (f.agentOnly === false && m.agentOnly) return false;
905
- if (f.humanOnly === false && m.humanOnly) return false;
906
- if (f.maxReward !== void 0 && m.reward > this._parseUsdc(f.maxReward)) return false;
907
- if (f.minReward !== void 0 && m.reward < this._parseUsdc(f.minReward)) return false;
908
- return true;
1062
+ // ─── Dispute watchdog (self-protection) ─────────────────────────────────────
1063
+ /**
1064
+ * Background watchdog over this agent's own assigned bounties. An agent
1065
+ * that only calls `takeBounty`/`submitWork` and then goes idle is exposed
1066
+ * to every counterparty-controlled window in the contract: a poster can
1067
+ * reject a correct submission (48h to challenge), open a dispute the agent
1068
+ * never responds to (48h to respond, then the *other* side wins by
1069
+ * default), or the agent may simply be owed a payout nobody triggered yet
1070
+ * (14d autoApprove / 30d claimArbitratorTimeout). `protect()` polls
1071
+ * `getMyBounties()` and reacts automatically:
1072
+ *
1073
+ * - **Pending rejection, not yet challenged** → calls `onRejection` (if
1074
+ * provided) for evidence and calls `challengeRejection`. Without a
1075
+ * callback, a rejection is only logged, never auto-challenged — silently
1076
+ * auto-disputing every rejection would be its own failure mode.
1077
+ * - **Dispute raised by the other party, not yet responded** → calls
1078
+ * `onDisputeAgainstMe` for evidence and calls `respondToDispute`. Same
1079
+ * caveat: no callback means log-only.
1080
+ * - **Dispute resolved-by-response but arbitrator never ruled (30d)** →
1081
+ * calls `claimArbitratorTimeout` automatically (permissionless, no
1082
+ * evidence needed — this just unsticks the agent's own frozen funds).
1083
+ * - **Submitted, approval window elapsed (14d), poster silent** → calls
1084
+ * `autoApprove` automatically.
1085
+ *
1086
+ * Returns an `unwatch()` function. Errors on any single bounty are logged
1087
+ * and swallowed so one bad case can't kill the whole watchdog.
1088
+ */
1089
+ protect(options = {}) {
1090
+ const pollingIntervalMs = options.pollingIntervalMs ?? 6e4;
1091
+ let stopped = false;
1092
+ const tick = async () => {
1093
+ if (stopped) return;
1094
+ try {
1095
+ await this._protectOnce(options);
1096
+ } catch (err) {
1097
+ console.error("[ArcBountyAgent.protect] tick error:", err);
1098
+ }
1099
+ if (!stopped) timer = setTimeout(tick, pollingIntervalMs);
1100
+ };
1101
+ let timer = setTimeout(tick, 0);
1102
+ return () => {
1103
+ stopped = true;
1104
+ clearTimeout(timer);
1105
+ };
1106
+ }
1107
+ async _protectOnce(options) {
1108
+ const [rejectionWindow, disputeWindow, approvalTimeout, arbitratorTimeout] = await Promise.all([
1109
+ this.publicClient.readContract({
1110
+ address: this.bountyAdapter,
1111
+ abi: BOUNTY_ADAPTER_ABI,
1112
+ functionName: "REJECTION_CHALLENGE_WINDOW"
1113
+ }),
1114
+ this.publicClient.readContract({
1115
+ address: this.bountyAdapter,
1116
+ abi: BOUNTY_ADAPTER_ABI,
1117
+ functionName: "DISPUTE_RESPONSE_WINDOW"
1118
+ }),
1119
+ this.publicClient.readContract({
1120
+ address: this.bountyAdapter,
1121
+ abi: BOUNTY_ADAPTER_ABI,
1122
+ functionName: "APPROVAL_TIMEOUT"
1123
+ }),
1124
+ this.publicClient.readContract({
1125
+ address: this.bountyAdapter,
1126
+ abi: BOUNTY_ADAPTER_ABI,
1127
+ functionName: "ARBITRATOR_TIMEOUT"
1128
+ })
1129
+ ]);
1130
+ const now = BigInt(Math.floor(Date.now() / 1e3));
1131
+ const mine = await this.getMyBounties();
1132
+ for (const meta of mine) {
1133
+ if (meta.resolved) continue;
1134
+ try {
1135
+ if (meta.rejectedAt > 0n && !meta.inDispute && now <= meta.rejectedAt + rejectionWindow) {
1136
+ options.onEvent?.("rejection_pending", meta);
1137
+ if (options.onRejection) {
1138
+ const evidence = await options.onRejection(meta);
1139
+ await this.challengeRejection(meta.jobId, evidence);
1140
+ options.onEvent?.("rejection_challenged", meta);
1141
+ }
1142
+ continue;
1143
+ }
1144
+ if (meta.inDispute && meta.disputeResponseHash.length === 0 && meta.disputeInitiator.toLowerCase() !== this.address.toLowerCase() && now <= meta.disputeRaisedAt + disputeWindow) {
1145
+ options.onEvent?.("dispute_needs_response", meta);
1146
+ if (options.onDisputeAgainstMe) {
1147
+ const evidence = await options.onDisputeAgainstMe(meta);
1148
+ await this.respondToDispute(meta.jobId, evidence);
1149
+ options.onEvent?.("dispute_responded", meta);
1150
+ }
1151
+ continue;
1152
+ }
1153
+ if (meta.inDispute && meta.disputeResponseHash.length > 0 && now > meta.disputeRaisedAt + arbitratorTimeout) {
1154
+ await this.claimArbitratorTimeout(meta.jobId);
1155
+ options.onEvent?.("arbitrator_timeout_claimed", meta);
1156
+ continue;
1157
+ }
1158
+ if (meta.submittedAt > 0n && meta.rejectedAt === 0n && !meta.inDispute && now > meta.submittedAt + approvalTimeout) {
1159
+ await this.autoApprove(meta.jobId);
1160
+ options.onEvent?.("auto_approved", meta);
1161
+ }
1162
+ } catch (err) {
1163
+ console.error(`[ArcBountyAgent.protect] error handling bounty #${meta.jobId}:`, err);
1164
+ }
1165
+ }
909
1166
  }
910
1167
  // ─── Autonomous loop ────────────────────────────────────────────────────────
911
1168
  async runOnce(filter, runTask) {
@@ -931,13 +1188,11 @@ var ArcBountyAgent = class {
931
1188
  * retry, paymaster) land in one place.
932
1189
  */
933
1190
  async _writeAdapter(functionName, args) {
934
- const hash = await this.walletClient.writeContract({
1191
+ const hash = await this.signer.writeContract({
935
1192
  address: this.bountyAdapter,
936
1193
  abi: BOUNTY_ADAPTER_ABI,
937
1194
  functionName,
938
- args,
939
- chain: null,
940
- account: this.account
1195
+ args
941
1196
  });
942
1197
  await this._waitForTx(hash);
943
1198
  return { hash };
@@ -986,13 +1241,11 @@ var ArcBountyAgent = class {
986
1241
  args: [this.address, this.bountyAdapter]
987
1242
  });
988
1243
  if (current >= amount) return;
989
- const hash = await this.walletClient.writeContract({
1244
+ const hash = await this.signer.writeContract({
990
1245
  address: CONTRACTS.USDC,
991
1246
  abi: ERC20_ABI,
992
1247
  functionName: "approve",
993
- args: [this.bountyAdapter, amount],
994
- chain: null,
995
- account: this.account
1248
+ args: [this.bountyAdapter, amount]
996
1249
  });
997
1250
  await this._waitForTx(hash);
998
1251
  }
@@ -1000,14 +1253,6 @@ var ArcBountyAgent = class {
1000
1253
  if (!e.text && !e.cid) throw new Error("Provide either text or cid");
1001
1254
  return e.cid ?? await pinText(e.text);
1002
1255
  }
1003
- _resolveDeadline(d) {
1004
- if (d instanceof Date) return BigInt(Math.floor(d.getTime() / 1e3));
1005
- if (d < 1e9) return BigInt(Math.floor(Date.now() / 1e3) + d);
1006
- return BigInt(d);
1007
- }
1008
- _parseUsdc(dollars) {
1009
- return BigInt(Math.round(dollars * 10 ** USDC_DECIMALS));
1010
- }
1011
1256
  };
1012
1257
 
1013
1258
  // src/metadata.ts
@@ -1055,7 +1300,11 @@ async function pinAgentMetadata(meta) {
1055
1300
  fetchIpfsJson,
1056
1301
  fetchIpfsText,
1057
1302
  isPinningConfigured,
1303
+ matchesBountyFilter,
1304
+ parseUsdc,
1058
1305
  pinAgentMetadata,
1059
1306
  pinText,
1060
- validateAgentMetadata
1307
+ resolveDeadline,
1308
+ validateAgentMetadata,
1309
+ workerBondFor
1061
1310
  });