steward-arc-sdk 0.1.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/README.md ADDED
@@ -0,0 +1,57 @@
1
+ # steward-sdk
2
+
3
+ If your agent pays anyone in USDC on Arc, put STEWARD in front of it: one contract call gives you per-payee caps + period limits +
4
+ expiry the agent can't exceed, an on-chain decision log, and an escalation path. Ten lines.
5
+
6
+ Contracts + docs: https://github.com/big14way/steward (Tameion Agents Hackathon, Canteen × Circle × Arc).
7
+
8
+ ## TypeScript
9
+
10
+ ```bash
11
+ npm i steward-arc-sdk viem
12
+ ```
13
+
14
+ ```ts
15
+ import { Steward } from "steward-arc-sdk";
16
+ import { privateKeyToAccount } from "viem/accounts";
17
+
18
+ const s = new Steward({
19
+ allowanceManager: "0x…", // the owner created an allowance for your agent address + the payee
20
+ auditLog: "0x…",
21
+ account: privateKeyToAccount(process.env.AGENT_PK as `0x${string}`),
22
+ });
23
+
24
+ // rules → canonical hash → AuditLog.record() → pay() | escalate()
25
+ const r = await s.decide({
26
+ allowanceId: 0n, amount: 150_000_000n, memo: "logo v2",
27
+ inputs: { milestone: "logo v2", evidence: "ipfs://…" }, evidence: true, screenOk: true,
28
+ reason: "Milestone delivered with evidence; within per-tx and period caps.", // your LLM's text, never an amount
29
+ });
30
+ console.log(r.action, r.hash, r.payTx ?? r.escalateTx);
31
+ ```
32
+
33
+ What `decide` does: `SCREEN_FAIL` if `screenOk === false` · `HOLD` if `evidence === false` · `ESCALATE` if over the per-tx cap or no room ·
34
+ `PARTIAL` if only part fits (pays that part, escalates the rest under `keccak256("STEWARD/remainder" ‖ hash)`) · else `PAY`.
35
+ Every outcome is `record()`ed on `AuditLog` with the keccak256 of a canonical JSON record you get back (`r.canonical`), so an auditor can replay it.
36
+ A decision hash pays once, even if you retry. Arc's 20 gwei floor is handled.
37
+
38
+ ## Python
39
+
40
+ ```bash
41
+ pip install steward-sdk
42
+ ```
43
+
44
+ ```python
45
+ from steward_sdk import Steward
46
+
47
+ s = Steward(allowance_manager="0x…", audit_log="0x…", agent_private_key=os.environ["AGENT_PK"])
48
+ r = s.decide(allowance_id=0, amount=150_000_000, memo="logo v2",
49
+ inputs={"milestone": "logo v2", "evidence": "ipfs://…"}, evidence=True, screen_ok=True)
50
+ print(r.action, r.hash, r.pay_tx or r.escalate_tx)
51
+ ```
52
+
53
+ ## Owner side
54
+
55
+ The owner (a Circle Developer-Controlled wallet in the reference app) calls `create(agent, payee, capPerPeriod, perTxCap, period, expiry)`,
56
+ `fund(id, amount)`, `approveAndPay(id, amount, hash)` for escalations, and `revoke(id)` to pull unspent funds and lock the agent out.
57
+ The reference FastAPI service + Telegram bot in the main repo does this with one tap.
package/dist/index.cjs ADDED
@@ -0,0 +1,149 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ ACTION_CODE: () => ACTION_CODE,
24
+ AM_ABI: () => AM_ABI,
25
+ LOG_ABI: () => LOG_ABI,
26
+ MIN_FEE_PER_GAS: () => MIN_FEE_PER_GAS,
27
+ REMAINDER_TAG: () => REMAINDER_TAG,
28
+ Steward: () => Steward,
29
+ applyRules: () => applyRules,
30
+ arcTestnet: () => import_chains.arcTestnet,
31
+ canonical: () => canonical,
32
+ decisionHash: () => decisionHash,
33
+ remainderHash: () => remainderHash
34
+ });
35
+ module.exports = __toCommonJS(index_exports);
36
+ var import_viem = require("viem");
37
+ var import_chains = require("viem/chains");
38
+ var AM_ABI = [
39
+ { type: "function", name: "pay", stateMutability: "nonpayable", inputs: [{ name: "id", type: "uint256" }, { name: "amount", type: "uint128" }, { name: "decisionHash", type: "bytes32" }, { name: "memo", type: "string" }], outputs: [] },
40
+ { type: "function", name: "escalate", stateMutability: "nonpayable", inputs: [{ name: "id", type: "uint256" }, { name: "amount", type: "uint128" }, { name: "decisionHash", type: "bytes32" }, { name: "reason", type: "string" }], outputs: [] },
41
+ { type: "function", name: "allowances", stateMutability: "view", inputs: [{ name: "id", type: "uint256" }], outputs: [
42
+ { name: "owner", type: "address" },
43
+ { name: "agent", type: "address" },
44
+ { name: "payee", type: "address" },
45
+ { name: "capPerPeriod", type: "uint128" },
46
+ { name: "perTxCap", type: "uint128" },
47
+ { name: "period", type: "uint64" },
48
+ { name: "periodStart", type: "uint64" },
49
+ { name: "expiry", type: "uint64" },
50
+ { name: "spentThisPeriod", type: "uint128" },
51
+ { name: "funded", type: "uint128" },
52
+ { name: "revoked", type: "bool" }
53
+ ] },
54
+ { type: "function", name: "usedDecision", stateMutability: "view", inputs: [{ name: "h", type: "bytes32" }], outputs: [{ type: "bool" }] },
55
+ { type: "function", name: "nextId", stateMutability: "view", inputs: [], outputs: [{ type: "uint256" }] }
56
+ ];
57
+ var LOG_ABI = [
58
+ { type: "function", name: "record", stateMutability: "nonpayable", inputs: [{ name: "allowanceId", type: "uint256" }, { name: "decisionHash", type: "bytes32" }, { name: "action", type: "uint8" }, { name: "amount", type: "uint128" }], outputs: [] }
59
+ ];
60
+ var ACTION_CODE = { HOLD: 0, PAY: 1, PARTIAL: 2, ESCALATE: 3, SCREEN_FAIL: 6 };
61
+ var MIN_FEE_PER_GAS = 20000000000n;
62
+ var REMAINDER_TAG = "STEWARD/remainder";
63
+ function canonical(obj) {
64
+ const sort = (v) => Array.isArray(v) ? v.map(sort) : v && typeof v === "object" ? Object.fromEntries(Object.keys(v).sort().map((k) => [k, sort(v[k])])) : typeof v === "bigint" ? v.toString() : v;
65
+ return JSON.stringify(sort(obj));
66
+ }
67
+ var decisionHash = (record) => (0, import_viem.keccak256)((0, import_viem.stringToHex)(canonical(record)));
68
+ var remainderHash = (hash) => (0, import_viem.keccak256)(`0x${(0, import_viem.stringToHex)(REMAINDER_TAG).slice(2)}${hash.slice(2)}`);
69
+ function applyRules(a, p) {
70
+ if (p.screenOk === false) return { action: "SCREEN_FAIL", rule: "R1_screen", pay: 0n, remainder: p.amount };
71
+ if (p.evidence === false) return { action: "HOLD", rule: "R2_no_evidence", pay: 0n, remainder: 0n };
72
+ if (p.amount > a.perTxCap) return { action: "ESCALATE", rule: "R3_over_per_tx", pay: 0n, remainder: p.amount };
73
+ const room = a.capPerPeriod - a.spentThisPeriod;
74
+ const liquid = a.funded - (p.reserveFloor ?? 0n) - (p.obligations ?? 0n);
75
+ const allowed = [p.amount, room, liquid].reduce((m, x) => x < m ? x : m);
76
+ if (allowed <= 0n) return { action: "ESCALATE", rule: "R4_no_room", pay: 0n, remainder: p.amount };
77
+ if (allowed < p.amount) return { action: "PARTIAL", rule: "R4_partial", pay: allowed, remainder: p.amount - allowed };
78
+ return { action: "PAY", rule: "R5_pay", pay: p.amount, remainder: 0n };
79
+ }
80
+ var Steward = class {
81
+ constructor(cfg) {
82
+ this.cfg = cfg;
83
+ const chain = cfg.chain ?? import_chains.arcTestnet;
84
+ const transport = cfg.transport ?? (0, import_viem.http)(cfg.rpc ?? "https://rpc.testnet.arc.io");
85
+ this.pub = (0, import_viem.createPublicClient)({ chain, transport });
86
+ this.wallet = (0, import_viem.createWalletClient)({ chain, transport, account: cfg.account });
87
+ }
88
+ cfg;
89
+ pub;
90
+ wallet;
91
+ async allowance(id) {
92
+ const [owner, agent, payee, capPerPeriod, perTxCap, period, periodStart, expiry, spentThisPeriod, funded, revoked] = await this.pub.readContract({ address: this.cfg.allowanceManager, abi: AM_ABI, functionName: "allowances", args: [id] });
93
+ return { owner, agent, payee, capPerPeriod, perTxCap, period, periodStart, expiry, spentThisPeriod, funded, revoked };
94
+ }
95
+ async fees() {
96
+ const block = await this.pub.getBlock();
97
+ const base = block.baseFeePerGas ?? 0n;
98
+ const maxFeePerGas = base + base / 4n > MIN_FEE_PER_GAS ? base + base / 4n : MIN_FEE_PER_GAS;
99
+ return { maxFeePerGas, maxPriorityFeePerGas: 1000000000n };
100
+ }
101
+ async write(address, abi, functionName, args) {
102
+ const fees = await this.fees();
103
+ const hash = await this.wallet.writeContract({ address, abi, functionName, args, ...fees });
104
+ const rcpt = await this.pub.waitForTransactionReceipt({ hash });
105
+ if (rcpt.status !== "success") throw new Error(`tx reverted ${hash}`);
106
+ return hash;
107
+ }
108
+ /** rules → canonical hash → AuditLog.record() → pay() | escalate(). Every call is recorded, including HOLD. */
109
+ async decide(p) {
110
+ const a = await this.allowance(p.allowanceId);
111
+ const r = applyRules(a, p);
112
+ const blockNumber = await this.pub.getBlockNumber();
113
+ const record = {
114
+ allowanceId: p.allowanceId.toString(),
115
+ requested: p.amount.toString(),
116
+ amount: r.pay.toString(),
117
+ remainder: r.remainder.toString(),
118
+ action: r.action,
119
+ rule: r.rule,
120
+ memo: p.memo,
121
+ reason: p.reason ?? `${r.rule}: ${r.action} by policy`,
122
+ inputs: p.inputs,
123
+ blockNumber: blockNumber.toString()
124
+ };
125
+ const hash = decisionHash(record);
126
+ const recordTx = await this.write(this.cfg.auditLog, LOG_ABI, "record", [p.allowanceId, hash, ACTION_CODE[r.action], r.pay]);
127
+ let payTx, escalateTx, rHash;
128
+ if (r.action === "PAY" || r.action === "PARTIAL") payTx = await this.write(this.cfg.allowanceManager, AM_ABI, "pay", [p.allowanceId, r.pay, hash, p.memo]);
129
+ if (r.remainder > 0n) {
130
+ rHash = r.action === "PARTIAL" ? remainderHash(hash) : hash;
131
+ escalateTx = await this.write(this.cfg.allowanceManager, AM_ABI, "escalate", [p.allowanceId, r.remainder, rHash, record.reason.slice(0, 200)]);
132
+ }
133
+ return { action: r.action, rule: r.rule, pay: r.pay, remainder: r.remainder, hash, remainderHash: rHash, recordTx, payTx, escalateTx, canonical: canonical(record), record };
134
+ }
135
+ };
136
+ // Annotate the CommonJS export names for ESM import in node:
137
+ 0 && (module.exports = {
138
+ ACTION_CODE,
139
+ AM_ABI,
140
+ LOG_ABI,
141
+ MIN_FEE_PER_GAS,
142
+ REMAINDER_TAG,
143
+ Steward,
144
+ applyRules,
145
+ arcTestnet,
146
+ canonical,
147
+ decisionHash,
148
+ remainderHash
149
+ });
@@ -0,0 +1,212 @@
1
+ import { Hex, Chain, Account, Transport } from 'viem';
2
+ export { arcTestnet } from 'viem/chains';
3
+
4
+ declare const AM_ABI: readonly [{
5
+ readonly type: "function";
6
+ readonly name: "pay";
7
+ readonly stateMutability: "nonpayable";
8
+ readonly inputs: readonly [{
9
+ readonly name: "id";
10
+ readonly type: "uint256";
11
+ }, {
12
+ readonly name: "amount";
13
+ readonly type: "uint128";
14
+ }, {
15
+ readonly name: "decisionHash";
16
+ readonly type: "bytes32";
17
+ }, {
18
+ readonly name: "memo";
19
+ readonly type: "string";
20
+ }];
21
+ readonly outputs: readonly [];
22
+ }, {
23
+ readonly type: "function";
24
+ readonly name: "escalate";
25
+ readonly stateMutability: "nonpayable";
26
+ readonly inputs: readonly [{
27
+ readonly name: "id";
28
+ readonly type: "uint256";
29
+ }, {
30
+ readonly name: "amount";
31
+ readonly type: "uint128";
32
+ }, {
33
+ readonly name: "decisionHash";
34
+ readonly type: "bytes32";
35
+ }, {
36
+ readonly name: "reason";
37
+ readonly type: "string";
38
+ }];
39
+ readonly outputs: readonly [];
40
+ }, {
41
+ readonly type: "function";
42
+ readonly name: "allowances";
43
+ readonly stateMutability: "view";
44
+ readonly inputs: readonly [{
45
+ readonly name: "id";
46
+ readonly type: "uint256";
47
+ }];
48
+ readonly outputs: readonly [{
49
+ readonly name: "owner";
50
+ readonly type: "address";
51
+ }, {
52
+ readonly name: "agent";
53
+ readonly type: "address";
54
+ }, {
55
+ readonly name: "payee";
56
+ readonly type: "address";
57
+ }, {
58
+ readonly name: "capPerPeriod";
59
+ readonly type: "uint128";
60
+ }, {
61
+ readonly name: "perTxCap";
62
+ readonly type: "uint128";
63
+ }, {
64
+ readonly name: "period";
65
+ readonly type: "uint64";
66
+ }, {
67
+ readonly name: "periodStart";
68
+ readonly type: "uint64";
69
+ }, {
70
+ readonly name: "expiry";
71
+ readonly type: "uint64";
72
+ }, {
73
+ readonly name: "spentThisPeriod";
74
+ readonly type: "uint128";
75
+ }, {
76
+ readonly name: "funded";
77
+ readonly type: "uint128";
78
+ }, {
79
+ readonly name: "revoked";
80
+ readonly type: "bool";
81
+ }];
82
+ }, {
83
+ readonly type: "function";
84
+ readonly name: "usedDecision";
85
+ readonly stateMutability: "view";
86
+ readonly inputs: readonly [{
87
+ readonly name: "h";
88
+ readonly type: "bytes32";
89
+ }];
90
+ readonly outputs: readonly [{
91
+ readonly type: "bool";
92
+ }];
93
+ }, {
94
+ readonly type: "function";
95
+ readonly name: "nextId";
96
+ readonly stateMutability: "view";
97
+ readonly inputs: readonly [];
98
+ readonly outputs: readonly [{
99
+ readonly type: "uint256";
100
+ }];
101
+ }];
102
+ declare const LOG_ABI: readonly [{
103
+ readonly type: "function";
104
+ readonly name: "record";
105
+ readonly stateMutability: "nonpayable";
106
+ readonly inputs: readonly [{
107
+ readonly name: "allowanceId";
108
+ readonly type: "uint256";
109
+ }, {
110
+ readonly name: "decisionHash";
111
+ readonly type: "bytes32";
112
+ }, {
113
+ readonly name: "action";
114
+ readonly type: "uint8";
115
+ }, {
116
+ readonly name: "amount";
117
+ readonly type: "uint128";
118
+ }];
119
+ readonly outputs: readonly [];
120
+ }];
121
+ type Action = "HOLD" | "PAY" | "PARTIAL" | "ESCALATE" | "SCREEN_FAIL";
122
+ declare const ACTION_CODE: Record<Action, number>;
123
+ /** Arc's minimum base fee. Anything lower is dropped silently with no receipt. */
124
+ declare const MIN_FEE_PER_GAS = 20000000000n;
125
+ /** Remainder key for a PARTIAL: pay() consumed the decision hash, approveAndPay() consumes this one. Same tag as the Python agent. */
126
+ declare const REMAINDER_TAG = "STEWARD/remainder";
127
+ type Allowance = {
128
+ owner: Hex;
129
+ agent: Hex;
130
+ payee: Hex;
131
+ capPerPeriod: bigint;
132
+ perTxCap: bigint;
133
+ period: bigint;
134
+ periodStart: bigint;
135
+ expiry: bigint;
136
+ spentThisPeriod: bigint;
137
+ funded: bigint;
138
+ revoked: boolean;
139
+ };
140
+ type DecideParams = {
141
+ allowanceId: bigint;
142
+ /** requested amount, 6-dp USDC */
143
+ amount: bigint;
144
+ /** on-chain memo (≤ 60 chars recommended) */
145
+ memo: string;
146
+ /** anything you want hashed into the decision record (milestone id, evidence, model inputs…) */
147
+ inputs: Record<string, unknown>;
148
+ /** false → SCREEN_FAIL. Default true. */
149
+ screenOk?: boolean;
150
+ /** false → HOLD. Default true. */
151
+ evidence?: boolean;
152
+ reserveFloor?: bigint;
153
+ obligations?: bigint;
154
+ /** a human-readable reason (your LLM's, or leave empty for a rules-only reason). Never sets amounts. */
155
+ reason?: string;
156
+ };
157
+ type DecisionRecord = {
158
+ allowanceId: string;
159
+ requested: string;
160
+ amount: string;
161
+ remainder: string;
162
+ action: Action;
163
+ rule: string;
164
+ memo: string;
165
+ reason: string;
166
+ inputs: Record<string, unknown>;
167
+ blockNumber: string;
168
+ };
169
+ type DecideResult = {
170
+ action: Action;
171
+ rule: string;
172
+ pay: bigint;
173
+ remainder: bigint;
174
+ hash: Hex;
175
+ remainderHash?: Hex;
176
+ recordTx: Hex;
177
+ payTx?: Hex;
178
+ escalateTx?: Hex;
179
+ canonical: string;
180
+ record: DecisionRecord;
181
+ };
182
+ /** Sorted-key, whitespace-free JSON (arrays keep order). Same idea as the Python agent's canonical_json. */
183
+ declare function canonical(obj: unknown): string;
184
+ declare const decisionHash: (record: unknown) => Hex;
185
+ declare const remainderHash: (hash: Hex) => Hex;
186
+ /** Deterministic rules. The LLM (if any) only supplies `reason`. */
187
+ declare function applyRules(a: Allowance, p: DecideParams): {
188
+ action: Action;
189
+ rule: string;
190
+ pay: bigint;
191
+ remainder: bigint;
192
+ };
193
+ declare class Steward {
194
+ private cfg;
195
+ private pub;
196
+ private wallet;
197
+ constructor(cfg: {
198
+ rpc?: string;
199
+ chain?: Chain;
200
+ allowanceManager: Hex;
201
+ auditLog: Hex;
202
+ account: Account;
203
+ transport?: Transport;
204
+ });
205
+ allowance(id: bigint): Promise<Allowance>;
206
+ private fees;
207
+ private write;
208
+ /** rules → canonical hash → AuditLog.record() → pay() | escalate(). Every call is recorded, including HOLD. */
209
+ decide(p: DecideParams): Promise<DecideResult>;
210
+ }
211
+
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 };
@@ -0,0 +1,212 @@
1
+ import { Hex, Chain, Account, Transport } from 'viem';
2
+ export { arcTestnet } from 'viem/chains';
3
+
4
+ declare const AM_ABI: readonly [{
5
+ readonly type: "function";
6
+ readonly name: "pay";
7
+ readonly stateMutability: "nonpayable";
8
+ readonly inputs: readonly [{
9
+ readonly name: "id";
10
+ readonly type: "uint256";
11
+ }, {
12
+ readonly name: "amount";
13
+ readonly type: "uint128";
14
+ }, {
15
+ readonly name: "decisionHash";
16
+ readonly type: "bytes32";
17
+ }, {
18
+ readonly name: "memo";
19
+ readonly type: "string";
20
+ }];
21
+ readonly outputs: readonly [];
22
+ }, {
23
+ readonly type: "function";
24
+ readonly name: "escalate";
25
+ readonly stateMutability: "nonpayable";
26
+ readonly inputs: readonly [{
27
+ readonly name: "id";
28
+ readonly type: "uint256";
29
+ }, {
30
+ readonly name: "amount";
31
+ readonly type: "uint128";
32
+ }, {
33
+ readonly name: "decisionHash";
34
+ readonly type: "bytes32";
35
+ }, {
36
+ readonly name: "reason";
37
+ readonly type: "string";
38
+ }];
39
+ readonly outputs: readonly [];
40
+ }, {
41
+ readonly type: "function";
42
+ readonly name: "allowances";
43
+ readonly stateMutability: "view";
44
+ readonly inputs: readonly [{
45
+ readonly name: "id";
46
+ readonly type: "uint256";
47
+ }];
48
+ readonly outputs: readonly [{
49
+ readonly name: "owner";
50
+ readonly type: "address";
51
+ }, {
52
+ readonly name: "agent";
53
+ readonly type: "address";
54
+ }, {
55
+ readonly name: "payee";
56
+ readonly type: "address";
57
+ }, {
58
+ readonly name: "capPerPeriod";
59
+ readonly type: "uint128";
60
+ }, {
61
+ readonly name: "perTxCap";
62
+ readonly type: "uint128";
63
+ }, {
64
+ readonly name: "period";
65
+ readonly type: "uint64";
66
+ }, {
67
+ readonly name: "periodStart";
68
+ readonly type: "uint64";
69
+ }, {
70
+ readonly name: "expiry";
71
+ readonly type: "uint64";
72
+ }, {
73
+ readonly name: "spentThisPeriod";
74
+ readonly type: "uint128";
75
+ }, {
76
+ readonly name: "funded";
77
+ readonly type: "uint128";
78
+ }, {
79
+ readonly name: "revoked";
80
+ readonly type: "bool";
81
+ }];
82
+ }, {
83
+ readonly type: "function";
84
+ readonly name: "usedDecision";
85
+ readonly stateMutability: "view";
86
+ readonly inputs: readonly [{
87
+ readonly name: "h";
88
+ readonly type: "bytes32";
89
+ }];
90
+ readonly outputs: readonly [{
91
+ readonly type: "bool";
92
+ }];
93
+ }, {
94
+ readonly type: "function";
95
+ readonly name: "nextId";
96
+ readonly stateMutability: "view";
97
+ readonly inputs: readonly [];
98
+ readonly outputs: readonly [{
99
+ readonly type: "uint256";
100
+ }];
101
+ }];
102
+ declare const LOG_ABI: readonly [{
103
+ readonly type: "function";
104
+ readonly name: "record";
105
+ readonly stateMutability: "nonpayable";
106
+ readonly inputs: readonly [{
107
+ readonly name: "allowanceId";
108
+ readonly type: "uint256";
109
+ }, {
110
+ readonly name: "decisionHash";
111
+ readonly type: "bytes32";
112
+ }, {
113
+ readonly name: "action";
114
+ readonly type: "uint8";
115
+ }, {
116
+ readonly name: "amount";
117
+ readonly type: "uint128";
118
+ }];
119
+ readonly outputs: readonly [];
120
+ }];
121
+ type Action = "HOLD" | "PAY" | "PARTIAL" | "ESCALATE" | "SCREEN_FAIL";
122
+ declare const ACTION_CODE: Record<Action, number>;
123
+ /** Arc's minimum base fee. Anything lower is dropped silently with no receipt. */
124
+ declare const MIN_FEE_PER_GAS = 20000000000n;
125
+ /** Remainder key for a PARTIAL: pay() consumed the decision hash, approveAndPay() consumes this one. Same tag as the Python agent. */
126
+ declare const REMAINDER_TAG = "STEWARD/remainder";
127
+ type Allowance = {
128
+ owner: Hex;
129
+ agent: Hex;
130
+ payee: Hex;
131
+ capPerPeriod: bigint;
132
+ perTxCap: bigint;
133
+ period: bigint;
134
+ periodStart: bigint;
135
+ expiry: bigint;
136
+ spentThisPeriod: bigint;
137
+ funded: bigint;
138
+ revoked: boolean;
139
+ };
140
+ type DecideParams = {
141
+ allowanceId: bigint;
142
+ /** requested amount, 6-dp USDC */
143
+ amount: bigint;
144
+ /** on-chain memo (≤ 60 chars recommended) */
145
+ memo: string;
146
+ /** anything you want hashed into the decision record (milestone id, evidence, model inputs…) */
147
+ inputs: Record<string, unknown>;
148
+ /** false → SCREEN_FAIL. Default true. */
149
+ screenOk?: boolean;
150
+ /** false → HOLD. Default true. */
151
+ evidence?: boolean;
152
+ reserveFloor?: bigint;
153
+ obligations?: bigint;
154
+ /** a human-readable reason (your LLM's, or leave empty for a rules-only reason). Never sets amounts. */
155
+ reason?: string;
156
+ };
157
+ type DecisionRecord = {
158
+ allowanceId: string;
159
+ requested: string;
160
+ amount: string;
161
+ remainder: string;
162
+ action: Action;
163
+ rule: string;
164
+ memo: string;
165
+ reason: string;
166
+ inputs: Record<string, unknown>;
167
+ blockNumber: string;
168
+ };
169
+ type DecideResult = {
170
+ action: Action;
171
+ rule: string;
172
+ pay: bigint;
173
+ remainder: bigint;
174
+ hash: Hex;
175
+ remainderHash?: Hex;
176
+ recordTx: Hex;
177
+ payTx?: Hex;
178
+ escalateTx?: Hex;
179
+ canonical: string;
180
+ record: DecisionRecord;
181
+ };
182
+ /** Sorted-key, whitespace-free JSON (arrays keep order). Same idea as the Python agent's canonical_json. */
183
+ declare function canonical(obj: unknown): string;
184
+ declare const decisionHash: (record: unknown) => Hex;
185
+ declare const remainderHash: (hash: Hex) => Hex;
186
+ /** Deterministic rules. The LLM (if any) only supplies `reason`. */
187
+ declare function applyRules(a: Allowance, p: DecideParams): {
188
+ action: Action;
189
+ rule: string;
190
+ pay: bigint;
191
+ remainder: bigint;
192
+ };
193
+ declare class Steward {
194
+ private cfg;
195
+ private pub;
196
+ private wallet;
197
+ constructor(cfg: {
198
+ rpc?: string;
199
+ chain?: Chain;
200
+ allowanceManager: Hex;
201
+ auditLog: Hex;
202
+ account: Account;
203
+ transport?: Transport;
204
+ });
205
+ allowance(id: bigint): Promise<Allowance>;
206
+ private fees;
207
+ private write;
208
+ /** rules → canonical hash → AuditLog.record() → pay() | escalate(). Every call is recorded, including HOLD. */
209
+ decide(p: DecideParams): Promise<DecideResult>;
210
+ }
211
+
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 };
package/dist/index.js ADDED
@@ -0,0 +1,120 @@
1
+ // src/index.ts
2
+ import {
3
+ createPublicClient,
4
+ createWalletClient,
5
+ http,
6
+ keccak256,
7
+ stringToHex
8
+ } from "viem";
9
+ import { arcTestnet } from "viem/chains";
10
+ var AM_ABI = [
11
+ { type: "function", name: "pay", stateMutability: "nonpayable", inputs: [{ name: "id", type: "uint256" }, { name: "amount", type: "uint128" }, { name: "decisionHash", type: "bytes32" }, { name: "memo", type: "string" }], outputs: [] },
12
+ { type: "function", name: "escalate", stateMutability: "nonpayable", inputs: [{ name: "id", type: "uint256" }, { name: "amount", type: "uint128" }, { name: "decisionHash", type: "bytes32" }, { name: "reason", type: "string" }], outputs: [] },
13
+ { type: "function", name: "allowances", stateMutability: "view", inputs: [{ name: "id", type: "uint256" }], outputs: [
14
+ { name: "owner", type: "address" },
15
+ { name: "agent", type: "address" },
16
+ { name: "payee", type: "address" },
17
+ { name: "capPerPeriod", type: "uint128" },
18
+ { name: "perTxCap", type: "uint128" },
19
+ { name: "period", type: "uint64" },
20
+ { name: "periodStart", type: "uint64" },
21
+ { name: "expiry", type: "uint64" },
22
+ { name: "spentThisPeriod", type: "uint128" },
23
+ { name: "funded", type: "uint128" },
24
+ { name: "revoked", type: "bool" }
25
+ ] },
26
+ { type: "function", name: "usedDecision", stateMutability: "view", inputs: [{ name: "h", type: "bytes32" }], outputs: [{ type: "bool" }] },
27
+ { type: "function", name: "nextId", stateMutability: "view", inputs: [], outputs: [{ type: "uint256" }] }
28
+ ];
29
+ var LOG_ABI = [
30
+ { type: "function", name: "record", stateMutability: "nonpayable", inputs: [{ name: "allowanceId", type: "uint256" }, { name: "decisionHash", type: "bytes32" }, { name: "action", type: "uint8" }, { name: "amount", type: "uint128" }], outputs: [] }
31
+ ];
32
+ var ACTION_CODE = { HOLD: 0, PAY: 1, PARTIAL: 2, ESCALATE: 3, SCREEN_FAIL: 6 };
33
+ var MIN_FEE_PER_GAS = 20000000000n;
34
+ var REMAINDER_TAG = "STEWARD/remainder";
35
+ function canonical(obj) {
36
+ const sort = (v) => Array.isArray(v) ? v.map(sort) : v && typeof v === "object" ? Object.fromEntries(Object.keys(v).sort().map((k) => [k, sort(v[k])])) : typeof v === "bigint" ? v.toString() : v;
37
+ return JSON.stringify(sort(obj));
38
+ }
39
+ var decisionHash = (record) => keccak256(stringToHex(canonical(record)));
40
+ var remainderHash = (hash) => keccak256(`0x${stringToHex(REMAINDER_TAG).slice(2)}${hash.slice(2)}`);
41
+ function applyRules(a, p) {
42
+ if (p.screenOk === false) return { action: "SCREEN_FAIL", rule: "R1_screen", pay: 0n, remainder: p.amount };
43
+ if (p.evidence === false) return { action: "HOLD", rule: "R2_no_evidence", pay: 0n, remainder: 0n };
44
+ if (p.amount > a.perTxCap) return { action: "ESCALATE", rule: "R3_over_per_tx", pay: 0n, remainder: p.amount };
45
+ const room = a.capPerPeriod - a.spentThisPeriod;
46
+ const liquid = a.funded - (p.reserveFloor ?? 0n) - (p.obligations ?? 0n);
47
+ const allowed = [p.amount, room, liquid].reduce((m, x) => x < m ? x : m);
48
+ if (allowed <= 0n) return { action: "ESCALATE", rule: "R4_no_room", pay: 0n, remainder: p.amount };
49
+ if (allowed < p.amount) return { action: "PARTIAL", rule: "R4_partial", pay: allowed, remainder: p.amount - allowed };
50
+ return { action: "PAY", rule: "R5_pay", pay: p.amount, remainder: 0n };
51
+ }
52
+ var Steward = class {
53
+ constructor(cfg) {
54
+ this.cfg = cfg;
55
+ const chain = cfg.chain ?? arcTestnet;
56
+ const transport = cfg.transport ?? http(cfg.rpc ?? "https://rpc.testnet.arc.io");
57
+ this.pub = createPublicClient({ chain, transport });
58
+ this.wallet = createWalletClient({ chain, transport, account: cfg.account });
59
+ }
60
+ cfg;
61
+ pub;
62
+ wallet;
63
+ async allowance(id) {
64
+ const [owner, agent, payee, capPerPeriod, perTxCap, period, periodStart, expiry, spentThisPeriod, funded, revoked] = await this.pub.readContract({ address: this.cfg.allowanceManager, abi: AM_ABI, functionName: "allowances", args: [id] });
65
+ return { owner, agent, payee, capPerPeriod, perTxCap, period, periodStart, expiry, spentThisPeriod, funded, revoked };
66
+ }
67
+ async fees() {
68
+ const block = await this.pub.getBlock();
69
+ const base = block.baseFeePerGas ?? 0n;
70
+ const maxFeePerGas = base + base / 4n > MIN_FEE_PER_GAS ? base + base / 4n : MIN_FEE_PER_GAS;
71
+ return { maxFeePerGas, maxPriorityFeePerGas: 1000000000n };
72
+ }
73
+ async write(address, abi, functionName, args) {
74
+ const fees = await this.fees();
75
+ const hash = await this.wallet.writeContract({ address, abi, functionName, args, ...fees });
76
+ const rcpt = await this.pub.waitForTransactionReceipt({ hash });
77
+ if (rcpt.status !== "success") throw new Error(`tx reverted ${hash}`);
78
+ return hash;
79
+ }
80
+ /** rules → canonical hash → AuditLog.record() → pay() | escalate(). Every call is recorded, including HOLD. */
81
+ async decide(p) {
82
+ const a = await this.allowance(p.allowanceId);
83
+ const r = applyRules(a, p);
84
+ const blockNumber = await this.pub.getBlockNumber();
85
+ const record = {
86
+ allowanceId: p.allowanceId.toString(),
87
+ requested: p.amount.toString(),
88
+ amount: r.pay.toString(),
89
+ remainder: r.remainder.toString(),
90
+ action: r.action,
91
+ rule: r.rule,
92
+ memo: p.memo,
93
+ reason: p.reason ?? `${r.rule}: ${r.action} by policy`,
94
+ inputs: p.inputs,
95
+ blockNumber: blockNumber.toString()
96
+ };
97
+ const hash = decisionHash(record);
98
+ const recordTx = await this.write(this.cfg.auditLog, LOG_ABI, "record", [p.allowanceId, hash, ACTION_CODE[r.action], r.pay]);
99
+ let payTx, escalateTx, rHash;
100
+ if (r.action === "PAY" || r.action === "PARTIAL") payTx = await this.write(this.cfg.allowanceManager, AM_ABI, "pay", [p.allowanceId, r.pay, hash, p.memo]);
101
+ if (r.remainder > 0n) {
102
+ rHash = r.action === "PARTIAL" ? remainderHash(hash) : hash;
103
+ escalateTx = await this.write(this.cfg.allowanceManager, AM_ABI, "escalate", [p.allowanceId, r.remainder, rHash, record.reason.slice(0, 200)]);
104
+ }
105
+ return { action: r.action, rule: r.rule, pay: r.pay, remainder: r.remainder, hash, remainderHash: rHash, recordTx, payTx, escalateTx, canonical: canonical(record), record };
106
+ }
107
+ };
108
+ export {
109
+ ACTION_CODE,
110
+ AM_ABI,
111
+ LOG_ABI,
112
+ MIN_FEE_PER_GAS,
113
+ REMAINDER_TAG,
114
+ Steward,
115
+ applyRules,
116
+ arcTestnet,
117
+ canonical,
118
+ decisionHash,
119
+ remainderHash
120
+ };
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "steward-arc-sdk",
3
+ "version": "0.1.0",
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
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/big14way/steward.git",
9
+ "directory": "packages/steward-sdk"
10
+ },
11
+ "homepage": "https://github.com/big14way/steward/tree/main/packages/steward-sdk",
12
+ "keywords": [
13
+ "arc",
14
+ "circle",
15
+ "usdc",
16
+ "agent",
17
+ "allowance",
18
+ "tameion",
19
+ "viem"
20
+ ],
21
+ "type": "module",
22
+ "main": "./dist/index.cjs",
23
+ "module": "./dist/index.js",
24
+ "types": "./dist/index.d.ts",
25
+ "exports": {
26
+ ".": {
27
+ "types": "./dist/index.d.ts",
28
+ "import": "./dist/index.js",
29
+ "require": "./dist/index.cjs"
30
+ }
31
+ },
32
+ "files": [
33
+ "dist",
34
+ "README.md"
35
+ ],
36
+ "scripts": {
37
+ "build": "tsup src/index.ts --format esm,cjs --dts --clean",
38
+ "test": "node --test test/*.test.mjs",
39
+ "prepublishOnly": "npm run build"
40
+ },
41
+ "peerDependencies": {
42
+ "viem": ">=2"
43
+ },
44
+ "devDependencies": {
45
+ "tsup": "^8",
46
+ "typescript": "^5",
47
+ "viem": "^2"
48
+ },
49
+ "publishConfig": {
50
+ "access": "public"
51
+ }
52
+ }