steward-arc-sdk 0.1.0 → 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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Godswill Idolor
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -5,6 +5,39 @@ expiry the agent can't exceed, an on-chain decision log, and an escalation path.
5
5
 
6
6
  Contracts + docs: https://github.com/big14way/steward (Tameion Agents Hackathon, Canteen × Circle × Arc).
7
7
 
8
+ ## Step 0: create a budget for your agent (the owner does this once)
9
+
10
+ The owner wallet creates an allowance on the deployed `AllowanceManager` (Arc Testnet) and funds it with USDC. Your agent can then pay
11
+ that one payee, within those limits, and nothing else.
12
+
13
+ ```ts
14
+ import { createPublicClient, createWalletClient, http, parseAbi } from "viem";
15
+ import { privateKeyToAccount } from "viem/accounts";
16
+ import { arcTestnet } from "steward-arc-sdk";
17
+
18
+ const AM = "0x3AAfC635a1D1391c9FD8b5B9d8A518Fe980cb7E6"; // AllowanceManager on Arc Testnet
19
+ const USDC = "0x3600000000000000000000000000000000000000"; // USDC on Arc (6 decimals)
20
+ const abi = parseAbi([
21
+ "function nextId() view returns (uint256)",
22
+ "function create(address agent, address payee, uint128 capPerPeriod, uint128 perTxCap, uint64 period, uint64 expiry) returns (uint256)",
23
+ "function fund(uint256 id, uint128 amount)",
24
+ "function approve(address spender, uint256 amount) returns (bool)",
25
+ ]);
26
+ const pub = createPublicClient({ chain: arcTestnet, transport: http() });
27
+ const owner = createWalletClient({ account: privateKeyToAccount(process.env.OWNER_PK as `0x${string}`), chain: arcTestnet, transport: http() });
28
+ const gas = { maxFeePerGas: 25_000_000_000n, maxPriorityFeePerGas: 1_000_000_000n }; // Arc's floor is 20 gwei
29
+
30
+ const id = await pub.readContract({ address: AM, abi, functionName: "nextId" });
31
+ // 50 USDC per payment, 200 USDC per week, no expiry
32
+ await pub.waitForTransactionReceipt({ hash: await owner.writeContract({ address: AM, abi, functionName: "create",
33
+ args: [AGENT_ADDRESS, PAYEE_ADDRESS, 200_000_000n, 50_000_000n, 604_800n, 0n], ...gas }) });
34
+ await pub.waitForTransactionReceipt({ hash: await owner.writeContract({ address: USDC, abi, functionName: "approve", args: [AM, 200_000_000n], ...gas }) });
35
+ await pub.waitForTransactionReceipt({ hash: await owner.writeContract({ address: AM, abi, functionName: "fund", args: [id, 200_000_000n], ...gas }) });
36
+ console.log("allowanceId", id); // pass this to steward.decide()
37
+ ```
38
+
39
+ Prefer a UI? The hosted app does the same from *Contractors → Add contractor*.
40
+
8
41
  ## TypeScript
9
42
 
10
43
  ```bash
@@ -50,6 +83,40 @@ r = s.decide(allowance_id=0, amount=150_000_000, memo="logo v2",
50
83
  print(r.action, r.hash, r.pay_tx or r.escalate_tx)
51
84
  ```
52
85
 
86
+ ## Agent on a Circle wallet (no raw key)
87
+
88
+ Most Arc agents sign with a Circle Developer-Controlled wallet. Pass the Circle client you already have instead of `account`;
89
+ STEWARD sends `record`, `pay` and `escalate` through Circle's contract-execution API and waits for the on-chain hash.
90
+ The allowance's `agent` must be that wallet's address. Same rules, same hashes.
91
+
92
+ ```ts
93
+ import { initiateDeveloperControlledWalletsClient } from "@circle-fin/developer-controlled-wallets";
94
+ import { Steward } from "steward-arc-sdk";
95
+
96
+ const client = initiateDeveloperControlledWalletsClient({ apiKey: process.env.CIRCLE_API_KEY!, entitySecret: process.env.CIRCLE_ENTITY_SECRET! });
97
+ const s = new Steward({
98
+ allowanceManager: "0x3AAfC635a1D1391c9FD8b5B9d8A518Fe980cb7E6", // Arc Testnet
99
+ auditLog: "0x89264D27AFbCb2Ac90b8a3802340C26Ea1326866",
100
+ circle: { client, walletId: process.env.AGENT_WALLET_ID! },
101
+ });
102
+ const r = await s.decide({ allowanceId: 0n, amount: 150_000_000n, memo: "logo v2", inputs: { milestone: "logo v2" } });
103
+ ```
104
+
105
+ ```python
106
+ # pip install "steward-sdk[circle]"
107
+ from circle.web3 import utils
108
+ from steward_sdk import Steward
109
+
110
+ client = utils.init_developer_controlled_wallets_client(api_key=os.environ["CIRCLE_API_KEY"], entity_secret=os.environ["CIRCLE_ENTITY_SECRET"])
111
+ s = Steward(allowance_manager="0x3AAfC635a1D1391c9FD8b5B9d8A518Fe980cb7E6", audit_log="0x89264D27AFbCb2Ac90b8a3802340C26Ea1326866",
112
+ circle_client=client, circle_wallet_id=os.environ["AGENT_WALLET_ID"])
113
+ r = s.decide(allowance_id=0, amount=150_000_000, memo="logo v2", inputs={"milestone": "logo v2"})
114
+ ```
115
+
116
+ Tested live on Arc Testnet with both SDKs (a `HOLD` recorded through a Circle wallet:
117
+ [TypeScript](https://explorer.testnet.arc.io/tx/0x1618d269880dae697842db31470db89e383e48b42450e829b0b8458f94e8a73f),
118
+ [Python](https://explorer.testnet.arc.io/tx/0xbb36c711456a84a2de37992f10b81409807db2436abdaecddb4d257bf9c1fda8)).
119
+
53
120
  ## Owner side
54
121
 
55
122
  The owner (a Circle Developer-Controlled wallet in the reference app) calls `create(agent, payee, capPerPeriod, perTxCap, period, expiry)`,
package/dist/index.cjs CHANGED
@@ -77,13 +77,19 @@ function applyRules(a, p) {
77
77
  if (allowed < p.amount) return { action: "PARTIAL", rule: "R4_partial", pay: allowed, remainder: p.amount - allowed };
78
78
  return { action: "PAY", rule: "R5_pay", pay: p.amount, remainder: 0n };
79
79
  }
80
+ var SIGNATURES = {
81
+ record: "record(uint256,bytes32,uint8,uint128)",
82
+ pay: "pay(uint256,uint128,bytes32,string)",
83
+ escalate: "escalate(uint256,uint128,bytes32,string)"
84
+ };
80
85
  var Steward = class {
81
86
  constructor(cfg) {
82
87
  this.cfg = cfg;
83
88
  const chain = cfg.chain ?? import_chains.arcTestnet;
84
89
  const transport = cfg.transport ?? (0, import_viem.http)(cfg.rpc ?? "https://rpc.testnet.arc.io");
85
90
  this.pub = (0, import_viem.createPublicClient)({ chain, transport });
86
- this.wallet = (0, import_viem.createWalletClient)({ chain, transport, account: cfg.account });
91
+ this.wallet = cfg.account ? (0, import_viem.createWalletClient)({ chain, transport, account: cfg.account }) : void 0;
92
+ if (!cfg.account && !cfg.circle) throw new Error("Steward needs either `account` (a viem account) or `circle` (a Circle wallet)");
87
93
  }
88
94
  cfg;
89
95
  pub;
@@ -98,7 +104,31 @@ var Steward = class {
98
104
  const maxFeePerGas = base + base / 4n > MIN_FEE_PER_GAS ? base + base / 4n : MIN_FEE_PER_GAS;
99
105
  return { maxFeePerGas, maxPriorityFeePerGas: 1000000000n };
100
106
  }
107
+ /** Submit through Circle and wait for the on-chain hash. The idempotency key is fixed first, so a retry can't double-submit. */
108
+ async writeCircle(address, functionName, args) {
109
+ const c = this.cfg.circle;
110
+ const idempotencyKey = globalThis.crypto.randomUUID();
111
+ const res = await c.client.createContractExecutionTransaction({
112
+ walletId: c.walletId,
113
+ contractAddress: address,
114
+ abiFunctionSignature: SIGNATURES[functionName],
115
+ abiParameters: args.map((a) => typeof a === "bigint" || typeof a === "number" ? a.toString() : a),
116
+ fee: { type: "level", config: { feeLevel: c.feeLevel ?? "MEDIUM" } },
117
+ idempotencyKey
118
+ });
119
+ const id = res.data?.id;
120
+ if (!id) throw new Error("Circle did not return a transaction id");
121
+ const deadline = Date.now() + (c.timeoutMs ?? 12e4);
122
+ while (Date.now() < deadline) {
123
+ await new Promise((r) => setTimeout(r, c.pollMs ?? 2e3));
124
+ const t = (await c.client.getTransaction({ id })).data?.transaction;
125
+ if ((t?.state === "COMPLETE" || t?.state === "CONFIRMED") && t.txHash) return t.txHash;
126
+ if (t?.state === "FAILED" || t?.state === "DENIED" || t?.state === "CANCELLED") throw new Error(`Circle tx ${id} ${t.state}: ${t.errorReason ?? ""}`);
127
+ }
128
+ throw new Error(`Circle tx ${id} not confirmed in time`);
129
+ }
101
130
  async write(address, abi, functionName, args) {
131
+ if (!this.wallet) return this.writeCircle(address, functionName, args);
102
132
  const fees = await this.fees();
103
133
  const hash = await this.wallet.writeContract({ address, abi, functionName, args, ...fees });
104
134
  const rcpt = await this.pub.waitForTransactionReceipt({ hash });
package/dist/index.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { Hex, Chain, Account, Transport } from 'viem';
1
+ import { Hex, Chain, Transport, Account } from 'viem';
2
2
  export { arcTestnet } from 'viem/chains';
3
3
 
4
4
  declare const AM_ABI: readonly [{
@@ -190,23 +190,75 @@ declare function applyRules(a: Allowance, p: DecideParams): {
190
190
  pay: bigint;
191
191
  remainder: bigint;
192
192
  };
193
+ /**
194
+ * The part of a Circle Developer-Controlled Wallets client STEWARD uses. Pass the client you already have:
195
+ * `initiateDeveloperControlledWalletsClient({ apiKey, entitySecret })` from `@circle-fin/developer-controlled-wallets`.
196
+ */
197
+ type CircleClientLike = {
198
+ createContractExecutionTransaction(req: {
199
+ walletId?: string;
200
+ walletAddress?: string;
201
+ blockchain?: string;
202
+ contractAddress: string;
203
+ abiFunctionSignature: string;
204
+ abiParameters: unknown[];
205
+ fee: {
206
+ type: "level";
207
+ config: {
208
+ feeLevel: "LOW" | "MEDIUM" | "HIGH";
209
+ };
210
+ };
211
+ idempotencyKey?: string;
212
+ }): Promise<{
213
+ data?: {
214
+ id?: string;
215
+ };
216
+ }>;
217
+ getTransaction(req: {
218
+ id: string;
219
+ }): Promise<{
220
+ data?: {
221
+ transaction?: {
222
+ state?: string;
223
+ txHash?: string;
224
+ errorReason?: string;
225
+ };
226
+ };
227
+ }>;
228
+ };
229
+ /** Sign with a Circle developer-controlled wallet instead of a raw key. The allowance's `agent` must be this wallet's address. */
230
+ type CircleSigner = {
231
+ client: CircleClientLike;
232
+ walletId: string;
233
+ feeLevel?: "LOW" | "MEDIUM" | "HIGH";
234
+ pollMs?: number;
235
+ timeoutMs?: number;
236
+ };
237
+ type StewardConfig = {
238
+ rpc?: string;
239
+ chain?: Chain;
240
+ allowanceManager: Hex;
241
+ auditLog: Hex;
242
+ transport?: Transport;
243
+ } & ({
244
+ account: Account;
245
+ circle?: undefined;
246
+ } | {
247
+ circle: CircleSigner;
248
+ account?: undefined;
249
+ });
193
250
  declare class Steward {
194
251
  private cfg;
195
252
  private pub;
196
253
  private wallet;
197
- constructor(cfg: {
198
- rpc?: string;
199
- chain?: Chain;
200
- allowanceManager: Hex;
201
- auditLog: Hex;
202
- account: Account;
203
- transport?: Transport;
204
- });
254
+ constructor(cfg: StewardConfig);
205
255
  allowance(id: bigint): Promise<Allowance>;
206
256
  private fees;
257
+ /** Submit through Circle and wait for the on-chain hash. The idempotency key is fixed first, so a retry can't double-submit. */
258
+ private writeCircle;
207
259
  private write;
208
260
  /** rules → canonical hash → AuditLog.record() → pay() | escalate(). Every call is recorded, including HOLD. */
209
261
  decide(p: DecideParams): Promise<DecideResult>;
210
262
  }
211
263
 
212
- export { ACTION_CODE, AM_ABI, type Action, type Allowance, type DecideParams, type DecideResult, type DecisionRecord, LOG_ABI, MIN_FEE_PER_GAS, REMAINDER_TAG, Steward, applyRules, canonical, decisionHash, remainderHash };
264
+ export { ACTION_CODE, AM_ABI, type Action, type Allowance, type CircleClientLike, type CircleSigner, type DecideParams, type DecideResult, type DecisionRecord, LOG_ABI, MIN_FEE_PER_GAS, REMAINDER_TAG, Steward, applyRules, canonical, decisionHash, remainderHash };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Hex, Chain, Account, Transport } from 'viem';
1
+ import { Hex, Chain, Transport, Account } from 'viem';
2
2
  export { arcTestnet } from 'viem/chains';
3
3
 
4
4
  declare const AM_ABI: readonly [{
@@ -190,23 +190,75 @@ declare function applyRules(a: Allowance, p: DecideParams): {
190
190
  pay: bigint;
191
191
  remainder: bigint;
192
192
  };
193
+ /**
194
+ * The part of a Circle Developer-Controlled Wallets client STEWARD uses. Pass the client you already have:
195
+ * `initiateDeveloperControlledWalletsClient({ apiKey, entitySecret })` from `@circle-fin/developer-controlled-wallets`.
196
+ */
197
+ type CircleClientLike = {
198
+ createContractExecutionTransaction(req: {
199
+ walletId?: string;
200
+ walletAddress?: string;
201
+ blockchain?: string;
202
+ contractAddress: string;
203
+ abiFunctionSignature: string;
204
+ abiParameters: unknown[];
205
+ fee: {
206
+ type: "level";
207
+ config: {
208
+ feeLevel: "LOW" | "MEDIUM" | "HIGH";
209
+ };
210
+ };
211
+ idempotencyKey?: string;
212
+ }): Promise<{
213
+ data?: {
214
+ id?: string;
215
+ };
216
+ }>;
217
+ getTransaction(req: {
218
+ id: string;
219
+ }): Promise<{
220
+ data?: {
221
+ transaction?: {
222
+ state?: string;
223
+ txHash?: string;
224
+ errorReason?: string;
225
+ };
226
+ };
227
+ }>;
228
+ };
229
+ /** Sign with a Circle developer-controlled wallet instead of a raw key. The allowance's `agent` must be this wallet's address. */
230
+ type CircleSigner = {
231
+ client: CircleClientLike;
232
+ walletId: string;
233
+ feeLevel?: "LOW" | "MEDIUM" | "HIGH";
234
+ pollMs?: number;
235
+ timeoutMs?: number;
236
+ };
237
+ type StewardConfig = {
238
+ rpc?: string;
239
+ chain?: Chain;
240
+ allowanceManager: Hex;
241
+ auditLog: Hex;
242
+ transport?: Transport;
243
+ } & ({
244
+ account: Account;
245
+ circle?: undefined;
246
+ } | {
247
+ circle: CircleSigner;
248
+ account?: undefined;
249
+ });
193
250
  declare class Steward {
194
251
  private cfg;
195
252
  private pub;
196
253
  private wallet;
197
- constructor(cfg: {
198
- rpc?: string;
199
- chain?: Chain;
200
- allowanceManager: Hex;
201
- auditLog: Hex;
202
- account: Account;
203
- transport?: Transport;
204
- });
254
+ constructor(cfg: StewardConfig);
205
255
  allowance(id: bigint): Promise<Allowance>;
206
256
  private fees;
257
+ /** Submit through Circle and wait for the on-chain hash. The idempotency key is fixed first, so a retry can't double-submit. */
258
+ private writeCircle;
207
259
  private write;
208
260
  /** rules → canonical hash → AuditLog.record() → pay() | escalate(). Every call is recorded, including HOLD. */
209
261
  decide(p: DecideParams): Promise<DecideResult>;
210
262
  }
211
263
 
212
- export { ACTION_CODE, AM_ABI, type Action, type Allowance, type DecideParams, type DecideResult, type DecisionRecord, LOG_ABI, MIN_FEE_PER_GAS, REMAINDER_TAG, Steward, applyRules, canonical, decisionHash, remainderHash };
264
+ export { ACTION_CODE, AM_ABI, type Action, type Allowance, type CircleClientLike, type CircleSigner, type DecideParams, type DecideResult, type DecisionRecord, LOG_ABI, MIN_FEE_PER_GAS, REMAINDER_TAG, Steward, applyRules, canonical, decisionHash, remainderHash };
package/dist/index.js CHANGED
@@ -49,13 +49,19 @@ function applyRules(a, p) {
49
49
  if (allowed < p.amount) return { action: "PARTIAL", rule: "R4_partial", pay: allowed, remainder: p.amount - allowed };
50
50
  return { action: "PAY", rule: "R5_pay", pay: p.amount, remainder: 0n };
51
51
  }
52
+ var SIGNATURES = {
53
+ record: "record(uint256,bytes32,uint8,uint128)",
54
+ pay: "pay(uint256,uint128,bytes32,string)",
55
+ escalate: "escalate(uint256,uint128,bytes32,string)"
56
+ };
52
57
  var Steward = class {
53
58
  constructor(cfg) {
54
59
  this.cfg = cfg;
55
60
  const chain = cfg.chain ?? arcTestnet;
56
61
  const transport = cfg.transport ?? http(cfg.rpc ?? "https://rpc.testnet.arc.io");
57
62
  this.pub = createPublicClient({ chain, transport });
58
- this.wallet = createWalletClient({ chain, transport, account: cfg.account });
63
+ this.wallet = cfg.account ? createWalletClient({ chain, transport, account: cfg.account }) : void 0;
64
+ if (!cfg.account && !cfg.circle) throw new Error("Steward needs either `account` (a viem account) or `circle` (a Circle wallet)");
59
65
  }
60
66
  cfg;
61
67
  pub;
@@ -70,7 +76,31 @@ var Steward = class {
70
76
  const maxFeePerGas = base + base / 4n > MIN_FEE_PER_GAS ? base + base / 4n : MIN_FEE_PER_GAS;
71
77
  return { maxFeePerGas, maxPriorityFeePerGas: 1000000000n };
72
78
  }
79
+ /** Submit through Circle and wait for the on-chain hash. The idempotency key is fixed first, so a retry can't double-submit. */
80
+ async writeCircle(address, functionName, args) {
81
+ const c = this.cfg.circle;
82
+ const idempotencyKey = globalThis.crypto.randomUUID();
83
+ const res = await c.client.createContractExecutionTransaction({
84
+ walletId: c.walletId,
85
+ contractAddress: address,
86
+ abiFunctionSignature: SIGNATURES[functionName],
87
+ abiParameters: args.map((a) => typeof a === "bigint" || typeof a === "number" ? a.toString() : a),
88
+ fee: { type: "level", config: { feeLevel: c.feeLevel ?? "MEDIUM" } },
89
+ idempotencyKey
90
+ });
91
+ const id = res.data?.id;
92
+ if (!id) throw new Error("Circle did not return a transaction id");
93
+ const deadline = Date.now() + (c.timeoutMs ?? 12e4);
94
+ while (Date.now() < deadline) {
95
+ await new Promise((r) => setTimeout(r, c.pollMs ?? 2e3));
96
+ const t = (await c.client.getTransaction({ id })).data?.transaction;
97
+ if ((t?.state === "COMPLETE" || t?.state === "CONFIRMED") && t.txHash) return t.txHash;
98
+ if (t?.state === "FAILED" || t?.state === "DENIED" || t?.state === "CANCELLED") throw new Error(`Circle tx ${id} ${t.state}: ${t.errorReason ?? ""}`);
99
+ }
100
+ throw new Error(`Circle tx ${id} not confirmed in time`);
101
+ }
73
102
  async write(address, abi, functionName, args) {
103
+ if (!this.wallet) return this.writeCircle(address, functionName, args);
74
104
  const fees = await this.fees();
75
105
  const hash = await this.wallet.writeContract({ address, abi, functionName, args, ...fees });
76
106
  const rcpt = await this.pub.waitForTransactionReceipt({ hash });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "steward-arc-sdk",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Per-payee on-chain allowances, a replayable decision log and human escalation for AI agents that pay people in USDC on Arc.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -31,7 +31,8 @@
31
31
  },
32
32
  "files": [
33
33
  "dist",
34
- "README.md"
34
+ "README.md",
35
+ "LICENSE"
35
36
  ],
36
37
  "scripts": {
37
38
  "build": "tsup src/index.ts --format esm,cjs --dts --clean",