qorbitpay-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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Qorbitpay
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 ADDED
@@ -0,0 +1,78 @@
1
+ # qorbitpay-sdk
2
+
3
+ Payments SDK for autonomous AI agents on [Qorbitpay](https://github.com/), running on Arc
4
+ (Circle's L1 testnet, chainId `5042002`, native gas token USDC).
5
+
6
+ ```bash
7
+ npm install qorbitpay-sdk
8
+ ```
9
+
10
+ ## Quickstart
11
+
12
+ ```js
13
+ import { Qorbitpay } from 'qorbitpay-sdk';
14
+
15
+ const q = new Qorbitpay({ privateKey: process.env.AGENT_KEY });
16
+
17
+ // send a payment (self-sovereign - signed and broadcast with your own wallet)
18
+ await q.pay({ to: '0xAgent', amount: 0.01, memo: 'service' });
19
+
20
+ // listen for incoming payments
21
+ const unsubscribe = q.receive({
22
+ onPayment: (tx) => fulfillService(tx),
23
+ });
24
+ ```
25
+
26
+ Fund your agent's wallet at the [Circle faucet](https://faucet.circle.com) (select Arc
27
+ Testnet) before sending payments.
28
+
29
+ ## Why no custodial relayer here
30
+
31
+ Qorbitpay's hosted backend (`POST /api/pay`) is a *custodial* relayer that adds QRNG-anchored
32
+ nonces, VQC fraud screening, QAOA-based provider routing, and Dilithium2 (post-quantum)
33
+ dual-signing with on-chain attestation for higher-value payments. This SDK instead talks
34
+ directly to the on-chain contracts with your agent's own key - lighter weight, no
35
+ custodial trust required, and all you need for the base protocol (sending/receiving
36
+ native-token payments, registering in the agent directory).
37
+
38
+ `checkFraud()` and `findRoute()` below call the *same* quantum services the hosted backend
39
+ uses, over HTTP, for agents that want those signals without running Qiskit themselves -
40
+ pass `apiUrl` pointing at a Qorbitpay backend deployment to use them.
41
+
42
+ ## API
43
+
44
+ ### `new Qorbitpay(options)`
45
+
46
+ | option | required | default | description |
47
+ |---|---|---|---|
48
+ | `privateKey` | yes | - | your agent's wallet private key |
49
+ | `rpcUrl` | no | Arc testnet RPC | |
50
+ | `chainId` | no | `5042002` | |
51
+ | `routerAddress` | no | deployed QorbitpayRouter | |
52
+ | `registryAddress` | no | deployed QorbitpayRegistry | |
53
+ | `apiUrl` | no | `null` | base URL of a Qorbitpay backend, required for `checkFraud`/`findRoute` |
54
+
55
+ ### `await q.pay({ to, amount, memo })`
56
+ Sends `amount` (native token units, string or number) to `to`. `memo` is hashed
57
+ (keccak256) on-chain. Returns `{ txHash, status, blockNumber, explorerUrl, ... }`.
58
+
59
+ ### `q.receive({ onPayment })`
60
+ Subscribes to incoming `PaymentSent` events addressed to your agent. Returns an
61
+ `unsubscribe()` function.
62
+
63
+ ### `await q.register({ serviceEndpoint })`
64
+ Registers your agent (self-sovereign: owner == agent == your wallet) in the on-chain
65
+ agent directory.
66
+
67
+ ### `await q.getAgent(address?)` / `await q.getReputation(address?)`
68
+ Reads from the agent directory. Defaults to your own address.
69
+
70
+ ### `await q.getBalance()`
71
+ Native token balance of your agent's wallet, formatted as a string.
72
+
73
+ ### `await q.checkFraud({ amount_zscore, agent_age_days, tx_frequency, dispute_rate })`
74
+ VQC (variational quantum circuit) fraud score via a Qorbitpay backend. Requires `apiUrl`.
75
+
76
+ ### `await q.findRoute(providers)`
77
+ QAOA-optimal pick among `providers: [{ id, price, latency, reputation }, ...]` via a
78
+ Qorbitpay backend. Requires `apiUrl`.
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "qorbitpay-sdk",
3
+ "version": "0.1.0",
4
+ "description": "SDK for autonomous AI agents to send and receive payments on Qorbitpay (Arc testnet)",
5
+ "type": "module",
6
+ "main": "src/index.js",
7
+ "exports": {
8
+ ".": "./src/index.js"
9
+ },
10
+ "files": [
11
+ "src",
12
+ "README.md",
13
+ "LICENSE"
14
+ ],
15
+ "engines": {
16
+ "node": ">=18"
17
+ },
18
+ "keywords": [
19
+ "qorbitpay",
20
+ "arc",
21
+ "circle",
22
+ "usdc",
23
+ "ai-agents",
24
+ "x402",
25
+ "payments",
26
+ "web3"
27
+ ],
28
+ "license": "MIT",
29
+ "author": "Qorbitpay",
30
+ "homepage": "https://qorbitpay.xyz",
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "https://github.com/davieslennox0/Qorbit.git",
34
+ "directory": "sdk"
35
+ },
36
+ "bugs": {
37
+ "url": "https://github.com/davieslennox0/Qorbit/issues"
38
+ },
39
+ "peerDependencies": {
40
+ "ethers": "^6.13.0"
41
+ },
42
+ "dependencies": {
43
+ "ethers": "^6.13.0"
44
+ }
45
+ }
package/src/abi.js ADDED
@@ -0,0 +1,55 @@
1
+ export const ROUTER_ABI = [
2
+ 'function pay(address to, bytes32 memoHash) payable',
3
+ 'function splitPay(address[] to, uint256[] amounts, bytes32 memoHash) payable',
4
+ 'function createEscrow(address payee, uint64 timeoutSeconds) payable returns (uint256)',
5
+ 'function releaseEscrow(uint256 id)',
6
+ 'function refundEscrow(uint256 id)',
7
+ 'function getEscrow(uint256 id) view returns (tuple(address payer,address payee,uint256 amount,uint64 createdAt,uint64 timeoutSeconds,bool released,bool refunded))',
8
+ 'event PaymentSent(address indexed from, address indexed to, uint256 amount, bytes32 memoHash)',
9
+ ];
10
+
11
+ const _IDENTITY = 'tuple(address owner,string name,string version,string serviceEndpoint,bytes32 capabilityHash,uint256 registeredAt,bool active)';
12
+ const _RECORD = 'tuple(address owner,string name,string version,string serviceEndpoint,bytes32 capabilityHash,uint256 registeredAt,bool active,uint32 reputation,bytes32 dilithiumPubKeyHash,uint32 trustScore,uint8 quantumLayerMask)';
13
+
14
+ export const REGISTRY_ABI = [
15
+ // ERC-8004 required
16
+ `function registerAgent(${_IDENTITY} identity) returns (bytes32 agentId)`,
17
+ `function getAgent(bytes32 agentId) view returns (${_IDENTITY})`,
18
+ 'function validateAgent(bytes32 agentId) view returns (bool)',
19
+ 'function deactivateAgent(bytes32 agentId)',
20
+ 'event AgentRegistered(bytes32 indexed agentId, address indexed owner, string name)',
21
+ 'event AgentValidated(bytes32 indexed agentId)',
22
+ 'event AgentDeactivated(bytes32 indexed agentId)',
23
+ // Qorbitpay extensions
24
+ `function getAgentRecord(bytes32 agentId) view returns (${_RECORD})`,
25
+ `function getAgentByOwner(address owner) view returns (bytes32 agentId, ${_RECORD} record)`,
26
+ 'function updateEndpoint(bytes32 agentId, string serviceEndpoint)',
27
+ 'function setDilithiumPubKey(bytes32 agentId, bytes32 pubKeyHash)',
28
+ 'function isRegistered(address agentOwner) view returns (bool)',
29
+ 'function ownerToAgentId(address) view returns (bytes32)',
30
+ 'function agentCount() view returns (uint256)',
31
+ `function listAgents(uint256 offset, uint256 limit) view returns (${_RECORD}[] page, bytes32[] ids)`,
32
+ ];
33
+
34
+ export const BILLING_ABI = [
35
+ 'function createSubscription(address agent, uint256 amountPerPeriod, uint256 periodSeconds) payable returns (bytes32)',
36
+ 'function cancelSubscription(bytes32 subId)',
37
+ 'function processPayment(bytes32 subId)',
38
+ 'function topUp(bytes32 subId) payable',
39
+ 'function getSubscription(bytes32 subId) view returns (tuple(address subscriber,address agent,uint256 amountPerPeriod,uint256 periodSeconds,uint256 lastPaidAt,uint256 balance,bool active))',
40
+ 'function nextPaymentDue(bytes32 subId) view returns (uint256)',
41
+ 'function isDue(bytes32 subId) view returns (bool)',
42
+ 'event SubscriptionCreated(bytes32 indexed subId, address indexed subscriber, address indexed agent, uint256 amountPerPeriod, uint256 periodSeconds)',
43
+ 'event PaymentProcessed(bytes32 indexed subId, address indexed agent, uint256 amount)',
44
+ ];
45
+
46
+ export const TREASURY_ABI = [
47
+ 'function deposit() payable',
48
+ 'function allocateBudget(address workerAgent, uint256 dailyCap, uint256 categoryMask, uint256 expiresAt)',
49
+ 'function spend(address treasury, uint256 amount, uint256 category)',
50
+ 'function revokeBudget(address workerAgent)',
51
+ 'function getBudget(address treasury, address workerAgent) view returns (uint256 remaining, uint256 dailyCap, uint256 expiresAt, uint256 categoryMask, bool active)',
52
+ 'function treasuryBalances(address) view returns (uint256)',
53
+ 'event BudgetAllocated(address indexed treasury, address indexed worker, uint256 dailyCap, uint256 categoryMask, uint256 expiresAt)',
54
+ 'event SpendExecuted(address indexed treasury, address indexed worker, uint256 amount, uint256 category)',
55
+ ];
package/src/config.js ADDED
@@ -0,0 +1,18 @@
1
+ // Defaults for Arc testnet - see contracts/deployments/arc-testnet.json in the Qorbitpay repo.
2
+ export const DEFAULT_NETWORK = {
3
+ rpcUrl: 'https://rpc.testnet.arc.network',
4
+ chainId: 5042002,
5
+ explorer: 'https://testnet.arcscan.app',
6
+ routerAddress: '0xCEe1f311261Ffe460ef5060F94183320e74fD703',
7
+ registryAddress: '0x3fbCdaD38f40d932A5574562BB72B9115c265093',
8
+ verifierAddress: '0xca8FbCFb990A77B130939Ec5E0B98e4324fE0c79',
9
+ billingAddress: '0xdF87E0c0cfcA0AEa1e899073c36F29Fd865B5e97',
10
+ treasuryAddress: '0x7168493186654AD84Ef2dEfb78C773eeCB1BaFC8',
11
+ };
12
+
13
+ export const CATEGORY = {
14
+ data: 0x01,
15
+ compute: 0x02,
16
+ storage: 0x04,
17
+ all: 0x07,
18
+ };
package/src/index.js ADDED
@@ -0,0 +1,314 @@
1
+ import { ethers } from 'ethers';
2
+ import { ROUTER_ABI, REGISTRY_ABI, BILLING_ABI, TREASURY_ABI } from './abi.js';
3
+ import { DEFAULT_NETWORK, CATEGORY } from './config.js';
4
+
5
+ /// Qorbitpay SDK - a thin client for AI agents to send/receive payments directly on Arc,
6
+ /// using the agent's own funded wallet (self-sovereign), rather than going through
7
+ /// Qorbitpay's custodial backend relayer. The relayer's extra protections (QRNG-anchored
8
+ /// nonces, VQC fraud screening, QAOA routing, Dilithium dual-signing/attestation) are
9
+ /// features of the hosted POST /api/pay endpoint, not requirements of the on-chain
10
+ /// protocol itself - calling the contracts directly here is a fully valid, lighter-weight
11
+ /// way to use Qorbitpay. checkFraud()/findRoute() below call the hosted quantum services
12
+ /// over HTTP for agents that want those signals without running Qiskit themselves.
13
+ class Qorbitpay {
14
+ constructor({
15
+ privateKey,
16
+ rpcUrl = DEFAULT_NETWORK.rpcUrl,
17
+ chainId = DEFAULT_NETWORK.chainId,
18
+ routerAddress = DEFAULT_NETWORK.routerAddress,
19
+ registryAddress = DEFAULT_NETWORK.registryAddress,
20
+ billingAddress = DEFAULT_NETWORK.billingAddress,
21
+ treasuryAddress = DEFAULT_NETWORK.treasuryAddress,
22
+ apiUrl = null,
23
+ } = {}) {
24
+ if (!privateKey) throw new Error('Qorbitpay SDK requires a privateKey');
25
+
26
+ this.provider = new ethers.JsonRpcProvider(rpcUrl, { chainId, name: 'arc-testnet' });
27
+ this.wallet = new ethers.Wallet(privateKey, this.provider);
28
+ this.address = this.wallet.address;
29
+ this.apiUrl = apiUrl;
30
+ this.explorer = DEFAULT_NETWORK.explorer;
31
+
32
+ this.router = new ethers.Contract(routerAddress, ROUTER_ABI, this.wallet);
33
+ this.registry = new ethers.Contract(registryAddress, REGISTRY_ABI, this.wallet);
34
+ this.billing = new ethers.Contract(billingAddress, BILLING_ABI, this.wallet);
35
+ this.treasury = new ethers.Contract(treasuryAddress, TREASURY_ABI, this.wallet);
36
+
37
+ // ERC-8004 agentId, populated after register() or set manually.
38
+ this.agentId = null;
39
+
40
+ // Guards against nonce races if an agent fires multiple calls without awaiting each one.
41
+ this._sendQueue = Promise.resolve();
42
+ }
43
+
44
+ _withNonceLock(fn) {
45
+ const run = this._sendQueue.then(async () => {
46
+ const nonce = await this.provider.getTransactionCount(this.address, 'pending');
47
+ return fn(nonce);
48
+ });
49
+ this._sendQueue = run.then(() => {}, () => {});
50
+ return run;
51
+ }
52
+
53
+ /// Sends a native-token (USDC) payment to `to` via QorbitpayRouter.pay(). `memo` is
54
+ /// hashed (keccak256) on-chain as an opaque reference - pass the same memo string
55
+ /// elsewhere if you need to look the payment back up.
56
+ async pay({ to, amount, memo } = {}) {
57
+ if (!to || !ethers.isAddress(to)) throw new Error('pay() requires a valid `to` address');
58
+ if (!amount || Number(amount) <= 0) throw new Error('pay() requires a positive `amount`');
59
+
60
+ const memoHash = memo ? ethers.keccak256(ethers.toUtf8Bytes(memo)) : ethers.ZeroHash;
61
+ const value = ethers.parseEther(String(amount));
62
+
63
+ const tx = await this._withNonceLock((nonce) => this.router.pay(to, memoHash, { value, nonce }));
64
+ const receipt = await tx.wait();
65
+
66
+ return {
67
+ txHash: tx.hash,
68
+ from: this.address,
69
+ to,
70
+ amount: String(amount),
71
+ memo: memo || null,
72
+ status: receipt.status === 1 ? 'confirmed' : 'failed',
73
+ blockNumber: receipt.blockNumber,
74
+ explorerUrl: `${this.explorer}/tx/${tx.hash}`,
75
+ };
76
+ }
77
+
78
+ /// Subscribes to incoming payments (PaymentSent events where `to` is this agent's
79
+ /// address). Returns an unsubscribe function.
80
+ receive({ onPayment } = {}) {
81
+ if (typeof onPayment !== 'function') throw new Error('receive() requires an onPayment callback');
82
+
83
+ const filter = this.router.filters.PaymentSent(null, this.address);
84
+ // Subscribing with a TopicFilter object (rather than the bare event name string)
85
+ // means ethers does NOT expand decoded args into separate listener parameters - it
86
+ // passes a single ContractEventPayload. Assuming the (from, to, amount, memoHash,
87
+ // event) signature here silently threw inside the listener (event was undefined) and
88
+ // ethers swallowed it, which looked exactly like a polling/timing bug until logging
89
+ // the raw listener arguments showed only one object was ever passed.
90
+ const listener = (payload) => {
91
+ const [from, to, amount, memoHash] = payload.args;
92
+ onPayment({
93
+ from,
94
+ to,
95
+ amount: ethers.formatEther(amount),
96
+ memoHash,
97
+ txHash: payload.log.transactionHash,
98
+ blockNumber: payload.log.blockNumber,
99
+ explorerUrl: `${this.explorer}/tx/${payload.log.transactionHash}`,
100
+ });
101
+ };
102
+
103
+ this.router.on(filter, listener);
104
+ return () => this.router.off(filter, listener);
105
+ }
106
+
107
+ /// Registers this agent in QorbitpayRegistry using an ERC-8004 AgentIdentity struct
108
+ /// (self-sovereign — owner is this wallet). Returns the bytes32 agentId and stores it
109
+ /// on this instance for automatic inclusion as X-Agent-Id in all subsequent API calls.
110
+ /// For custodial registration on an agent's behalf, see the backend's POST /api/receive.
111
+ async register({ name, serviceEndpoint, version = '1.0.0', capabilityHash } = {}) {
112
+ if (!name) throw new Error('register() requires a name');
113
+ if (!serviceEndpoint) throw new Error('register() requires a serviceEndpoint');
114
+
115
+ const identity = {
116
+ owner: this.address,
117
+ name,
118
+ version,
119
+ serviceEndpoint,
120
+ capabilityHash: capabilityHash || ethers.ZeroHash,
121
+ registeredAt: 0n,
122
+ active: true,
123
+ };
124
+
125
+ const tx = await this._withNonceLock((nonce) =>
126
+ this.registry.registerAgent(identity, { nonce })
127
+ );
128
+ const receipt = await tx.wait();
129
+
130
+ let agentId = null;
131
+ for (const log of receipt.logs) {
132
+ try {
133
+ const parsed = this.registry.interface.parseLog({ topics: log.topics, data: log.data });
134
+ if (parsed?.name === 'AgentRegistered') { agentId = parsed.args.agentId; break; }
135
+ } catch { /* ignore */ }
136
+ }
137
+
138
+ this.agentId = agentId;
139
+ return { txHash: tx.hash, agentId, status: receipt.status === 1 ? 'confirmed' : 'failed' };
140
+ }
141
+
142
+ _formatAgentRecord(agentId, record) {
143
+ return {
144
+ agentId,
145
+ erc8004: true,
146
+ owner: record.owner,
147
+ name: record.name,
148
+ version: record.version,
149
+ serviceEndpoint: record.serviceEndpoint,
150
+ capabilityHash: record.capabilityHash,
151
+ registeredAt: Number(record.registeredAt),
152
+ active: record.active,
153
+ reputation: Number(record.reputation),
154
+ dilithiumPubKeyHash: record.dilithiumPubKeyHash,
155
+ trustScore: Number(record.trustScore),
156
+ quantumLayerMask: Number(record.quantumLayerMask),
157
+ };
158
+ }
159
+
160
+ async getAgent(agentIdOrAddress = this.agentId || this.address) {
161
+ // Accept either a bytes32 agentId (66-char hex) or an owner address (42-char).
162
+ if (agentIdOrAddress && agentIdOrAddress.length > 42) {
163
+ const record = await this.registry.getAgentRecord(agentIdOrAddress);
164
+ return this._formatAgentRecord(agentIdOrAddress, record);
165
+ }
166
+ const [agentId, record] = await this.registry.getAgentByOwner(agentIdOrAddress);
167
+ return this._formatAgentRecord(agentId, record);
168
+ }
169
+
170
+ async getReputation(agentIdOrAddress = this.agentId || this.address) {
171
+ return (await this.getAgent(agentIdOrAddress)).reputation;
172
+ }
173
+
174
+ async getBalance() {
175
+ const balance = await this.provider.getBalance(this.address);
176
+ return ethers.formatEther(balance);
177
+ }
178
+
179
+ async _apiPost(path, body) {
180
+ if (!this.apiUrl) {
181
+ throw new Error(`${path} requires apiUrl to be set in the Qorbitpay constructor`);
182
+ }
183
+ const headers = { 'Content-Type': 'application/json' };
184
+ if (this.agentId) headers['X-Agent-Id'] = this.agentId;
185
+ const res = await fetch(`${this.apiUrl}${path}`, {
186
+ method: 'POST',
187
+ headers,
188
+ body: JSON.stringify(body),
189
+ });
190
+ const data = await res.json();
191
+ if (!res.ok) throw new Error(data.error || `${path} failed with status ${res.status}`);
192
+ return data;
193
+ }
194
+
195
+ /// VQC fraud score via the hosted quantum services (POST /api/fraud).
196
+ checkFraud({ amount_zscore, agent_age_days, tx_frequency, dispute_rate } = {}) {
197
+ return this._apiPost('/api/fraud', { amount_zscore, agent_age_days, tx_frequency, dispute_rate });
198
+ }
199
+
200
+ /// QAOA-optimal provider selection via the hosted quantum services (POST /api/route).
201
+ findRoute(providers) {
202
+ return this._apiPost('/api/route', { providers });
203
+ }
204
+
205
+ /// Create a recurring subscription to pay `to` agent `amount` USDC every `period`.
206
+ /// `period` can be seconds (number) or a shorthand string: '7d', '24h', '1w', etc.
207
+ /// `initialBalance` (optional) pre-funds the subscription; defaults to one period's amount.
208
+ async subscribe({ to, amount, period, initialBalance } = {}) {
209
+ if (!to || !ethers.isAddress(to)) throw new Error('subscribe() requires a valid `to` address');
210
+ if (!amount || Number(amount) <= 0) throw new Error('subscribe() requires a positive `amount`');
211
+ if (!period) throw new Error('subscribe() requires a `period` (e.g. "7d", 604800)');
212
+
213
+ const periodSeconds = BigInt(_parsePeriod(period));
214
+ const amountWei = ethers.parseEther(String(amount));
215
+ const balanceWei = initialBalance ? ethers.parseEther(String(initialBalance)) : amountWei;
216
+
217
+ const tx = await this._withNonceLock((nonce) =>
218
+ this.billing.createSubscription(to, amountWei, periodSeconds, { value: balanceWei, nonce })
219
+ );
220
+ const receipt = await tx.wait();
221
+
222
+ let subId = null;
223
+ for (const log of receipt.logs) {
224
+ try {
225
+ const parsed = this.billing.interface.parseLog({ topics: log.topics, data: log.data });
226
+ if (parsed?.name === 'SubscriptionCreated') {
227
+ subId = parsed.args.subId;
228
+ break;
229
+ }
230
+ } catch { /* ignore */ }
231
+ }
232
+
233
+ return {
234
+ subId,
235
+ to,
236
+ amount: String(amount),
237
+ periodSeconds: Number(periodSeconds),
238
+ initialBalance: ethers.formatEther(balanceWei),
239
+ txHash: tx.hash,
240
+ status: receipt.status === 1 ? 'confirmed' : 'failed',
241
+ explorerUrl: `${this.explorer}/tx/${tx.hash}`,
242
+ };
243
+ }
244
+
245
+ /// Cancel a subscription (subscriber only). Refunds remaining balance.
246
+ async cancelSubscription(subId) {
247
+ if (!subId) throw new Error('cancelSubscription() requires a subId');
248
+ const tx = await this._withNonceLock((nonce) => this.billing.cancelSubscription(subId, { nonce }));
249
+ const receipt = await tx.wait();
250
+ return { txHash: tx.hash, status: receipt.status === 1 ? 'confirmed' : 'failed' };
251
+ }
252
+
253
+ /// Spend from a treasury budget. `from` is the treasury address, `category` is
254
+ /// 'data' | 'compute' | 'storage' or a numeric bitmask.
255
+ async spend({ from, amount, category = 'data' } = {}) {
256
+ if (!from || !ethers.isAddress(from)) throw new Error('spend() requires a valid `from` (treasury) address');
257
+ if (!amount || Number(amount) <= 0) throw new Error('spend() requires a positive `amount`');
258
+
259
+ const cat = typeof category === 'string'
260
+ ? (CATEGORY[category.toLowerCase()] ?? (() => { throw new Error(`unknown category: ${category}`); })())
261
+ : Number(category);
262
+ const amountWei = ethers.parseEther(String(amount));
263
+
264
+ const tx = await this._withNonceLock((nonce) =>
265
+ this.treasury.spend(from, amountWei, BigInt(cat), { nonce })
266
+ );
267
+ const receipt = await tx.wait();
268
+
269
+ return {
270
+ from,
271
+ to: this.address,
272
+ amount: String(amount),
273
+ category,
274
+ txHash: tx.hash,
275
+ status: receipt.status === 1 ? 'confirmed' : 'failed',
276
+ explorerUrl: `${this.explorer}/tx/${tx.hash}`,
277
+ };
278
+ }
279
+
280
+ /// Deposit funds into the treasury under this agent's address.
281
+ async depositToTreasury(amount) {
282
+ if (!amount || Number(amount) <= 0) throw new Error('depositToTreasury() requires a positive amount');
283
+ const value = ethers.parseEther(String(amount));
284
+ const tx = await this._withNonceLock((nonce) => this.treasury.deposit({ value, nonce }));
285
+ const receipt = await tx.wait();
286
+ return { txHash: tx.hash, status: receipt.status === 1 ? 'confirmed' : 'failed' };
287
+ }
288
+
289
+ /// Allocate a spending budget to a worker agent.
290
+ async allocateBudget({ worker, dailyCap, categoryMask = CATEGORY.all, expiresAt } = {}) {
291
+ if (!worker || !ethers.isAddress(worker)) throw new Error('allocateBudget() requires a valid `worker` address');
292
+ if (!dailyCap || Number(dailyCap) <= 0) throw new Error('allocateBudget() requires a positive `dailyCap`');
293
+ if (!expiresAt) throw new Error('allocateBudget() requires an `expiresAt` unix timestamp');
294
+
295
+ const tx = await this._withNonceLock((nonce) =>
296
+ this.treasury.allocateBudget(worker, ethers.parseEther(String(dailyCap)), BigInt(categoryMask), BigInt(expiresAt), { nonce })
297
+ );
298
+ const receipt = await tx.wait();
299
+ return { txHash: tx.hash, status: receipt.status === 1 ? 'confirmed' : 'failed' };
300
+ }
301
+ }
302
+
303
+ function _parsePeriod(period) {
304
+ if (typeof period === 'number') return period;
305
+ const str = String(period);
306
+ if (/^\d+$/.test(str)) return Number(str);
307
+ const match = str.match(/^(\d+(?:\.\d+)?)(s|m|h|d|w)$/);
308
+ if (!match) throw new Error(`invalid period: ${period} (use e.g. 7d, 24h, 3600)`);
309
+ const n = Number(match[1]);
310
+ const units = { s: 1, m: 60, h: 3600, d: 86400, w: 604800 };
311
+ return Math.round(n * units[match[2]]);
312
+ }
313
+
314
+ export { Qorbitpay, CATEGORY };