agent-operator 0.1.0 → 0.2.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/cli/index.js CHANGED
File without changes
package/dist/cli/init.js CHANGED
@@ -106,29 +106,40 @@ async function promptWallet() {
106
106
  secret: wh.secret || undefined,
107
107
  };
108
108
  }
109
- // try to create/find OWS wallet
109
+ // try to find or create OWS wallet
110
110
  let walletId = "";
111
111
  try {
112
112
  const ows = await import("@open-wallet-standard/core");
113
- console.log(`\nChecking for OWS wallet "${name}"...`);
114
- try {
115
- const existing = ows.getWallet(name);
116
- walletId = existing.id;
117
- console.log(` → Found existing wallet: ${walletId}`);
113
+ const existing = ows.listWallets();
114
+ if (existing.length > 0) {
115
+ const { walletChoice } = await prompts({
116
+ type: "select",
117
+ name: "walletChoice",
118
+ message: "OWS wallet",
119
+ choices: [
120
+ ...existing.map((w) => ({
121
+ title: `${w.name} (${w.id.slice(0, 8)}...)`,
122
+ value: w.id,
123
+ })),
124
+ { title: "Create new wallet", value: "__new__" },
125
+ ],
126
+ });
127
+ if (walletChoice && walletChoice !== "__new__") {
128
+ walletId = walletChoice;
129
+ const picked = existing.find((w) => w.id === walletChoice);
130
+ console.log(` → Using wallet: ${picked?.name} (${walletId})`);
131
+ }
132
+ else {
133
+ const created = ows.createWallet(name);
134
+ walletId = created.id;
135
+ console.log(` ✅ Wallet created: ${name} (id: ${walletId})`);
136
+ }
118
137
  }
119
- catch {
120
- console.log(` → Not found. Creating wallet via OWS...`);
138
+ else {
139
+ console.log(`\nNo OWS wallets found. Creating one...`);
121
140
  const created = ows.createWallet(name);
122
141
  walletId = created.id;
123
142
  console.log(` ✅ Wallet created: ${name} (id: ${walletId})`);
124
- // generate API key
125
- try {
126
- const apiKey = ows.createApiKey(name, [walletId], [], "");
127
- console.log(` ✅ API key generated: ${apiKey.id}`);
128
- }
129
- catch {
130
- console.log(` ⚠️ Could not generate API key — set up manually`);
131
- }
132
143
  }
133
144
  }
134
145
  catch {
package/dist/config.d.ts CHANGED
@@ -1,8 +1,16 @@
1
1
  import type { PoliciesConfig } from "./policies/types.js";
2
2
  import type { AlertsConfig } from "./notifications/types.js";
3
+ export type Network = "mainnet" | "testnet";
4
+ export interface TokenConfig {
5
+ address: string;
6
+ symbol: string;
7
+ decimals?: number;
8
+ }
3
9
  export interface WalletConfig {
4
10
  walletId: string;
5
11
  chains: string[];
12
+ network?: Network;
13
+ tokens?: Record<string, TokenConfig[]>;
6
14
  policies: PoliciesConfig;
7
15
  notifications: NotificationConfig;
8
16
  }
@@ -1,6 +1,12 @@
1
+ import type { Network, TokenConfig } from "../config.js";
1
2
  import type { Connector, Session, SessionResult, Balance } from "./types.js";
2
3
  export declare class BaseConnector implements Connector {
3
4
  chain: string;
5
+ network: Network;
6
+ tokens: TokenConfig[];
7
+ constructor(network?: Network, tokens?: TokenConfig[]);
8
+ private get chainId();
9
+ private getViemChain;
4
10
  private sessions;
5
11
  openSession(params: {
6
12
  service: string;
@@ -8,5 +14,5 @@ export declare class BaseConnector implements Connector {
8
14
  walletId: string;
9
15
  }): Promise<Session>;
10
16
  closeSession(sessionId: string): Promise<SessionResult>;
11
- getBalance(walletId: string): Promise<Balance>;
17
+ getBalances(walletId: string): Promise<Balance[]>;
12
18
  }
@@ -1,19 +1,42 @@
1
1
  import { Mppx } from "mppx/client";
2
2
  import { Method, Credential } from "mppx";
3
3
  import { signTransaction, getWallet } from "@open-wallet-standard/core";
4
- // Base chain ID for EIP-155
5
- const BASE_CHAIN_ID = "eip155:8453";
4
+ const DEFAULT_TOKENS = {
5
+ mainnet: [
6
+ { address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", symbol: "USDC", decimals: 6 },
7
+ ],
8
+ testnet: [
9
+ { address: "0x036CbD53842c5426634e7929541eC2318f3dCF7e", symbol: "USDC", decimals: 6 },
10
+ ],
11
+ };
12
+ const CHAIN_IDS = {
13
+ mainnet: "eip155:8453",
14
+ testnet: "eip155:84532",
15
+ };
6
16
  export class BaseConnector {
7
17
  chain = "base";
18
+ network;
19
+ tokens;
20
+ constructor(network = "mainnet", tokens) {
21
+ this.network = network;
22
+ this.tokens = tokens ?? DEFAULT_TOKENS[network];
23
+ }
24
+ get chainId() {
25
+ return CHAIN_IDS[this.network];
26
+ }
27
+ async getViemChain() {
28
+ const chains = await import("viem/chains");
29
+ return this.network === "testnet" ? chains.baseSepolia : chains.base;
30
+ }
8
31
  sessions = new Map();
9
32
  async openSession(params) {
10
33
  const { service, maxBudget, walletId } = params;
34
+ const chainId = this.chainId;
11
35
  const wallet = getWallet(walletId);
12
36
  const baseAccount = wallet.accounts.find((a) => a.chainId.startsWith("eip155"));
13
37
  if (!baseAccount) {
14
38
  throw new Error(`Wallet '${walletId}' has no EVM account for Base`);
15
39
  }
16
- // define a custom charge method for Base
17
40
  const baseChargeMethod = Method.from({
18
41
  name: "base",
19
42
  intent: "charge",
@@ -32,26 +55,24 @@ export class BaseConnector {
32
55
  });
33
56
  const clientMethod = Method.toClient(baseChargeMethod, {
34
57
  async createCredential({ challenge }) {
35
- // sign the payment via OWS (policies enforced here)
36
58
  const txData = JSON.stringify({
37
59
  challengeId: challenge.id,
38
60
  from: baseAccount.address,
39
61
  amount: challenge.request?.amount,
40
62
  chain: "base",
41
63
  });
42
- const result = signTransaction(walletId, BASE_CHAIN_ID, txData);
64
+ const result = signTransaction(walletId, chainId, txData);
43
65
  return Credential.serialize({
44
66
  challenge,
45
67
  payload: { signature: result.signature },
46
- source: `did:pkh:eip155:8453:${baseAccount.address}`,
68
+ source: `did:pkh:${chainId}:${baseAccount.address}`,
47
69
  });
48
70
  },
49
71
  });
50
72
  const mppxClient = Mppx.create({
51
73
  methods: [clientMethod],
52
74
  });
53
- // initiate the 402 flow
54
- const response = await mppxClient.fetch(service, {
75
+ await mppxClient.fetch(service, {
55
76
  headers: { "X-Max-Budget": String(maxBudget) },
56
77
  });
57
78
  const sessionId = crypto.randomUUID();
@@ -83,37 +104,52 @@ export class BaseConnector {
83
104
  refund: session.maxBudget - session.spent,
84
105
  };
85
106
  }
86
- async getBalance(walletId) {
107
+ async getBalances(walletId) {
87
108
  const wallet = getWallet(walletId);
88
109
  const baseAccount = wallet.accounts.find((a) => a.chainId.startsWith("eip155"));
89
110
  if (!baseAccount) {
90
- return { chain: this.chain, amount: 0, currency: "USDC" };
111
+ return this.tokens.map((t) => ({
112
+ chain: this.chain,
113
+ amount: 0,
114
+ currency: t.symbol,
115
+ }));
91
116
  }
92
117
  try {
93
118
  const { createPublicClient, http, parseAbi, formatUnits } = await import("viem");
94
- const { base } = await import("viem/chains");
119
+ const chain = await this.getViemChain();
95
120
  const client = createPublicClient({
96
- chain: base,
121
+ chain,
97
122
  transport: http(),
98
123
  });
99
- // USDC on Base
100
- const USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
101
- const balance = await client.readContract({
102
- address: USDC_ADDRESS,
103
- abi: parseAbi([
104
- "function balanceOf(address) view returns (uint256)",
105
- ]),
106
- functionName: "balanceOf",
107
- args: [baseAccount.address],
108
- });
109
- return {
110
- chain: this.chain,
111
- amount: Number(formatUnits(balance, 6)),
112
- currency: "USDC",
113
- };
124
+ const balanceAbi = parseAbi([
125
+ "function balanceOf(address) view returns (uint256)",
126
+ ]);
127
+ const results = await Promise.all(this.tokens.map(async (token) => {
128
+ try {
129
+ const balance = await client.readContract({
130
+ address: token.address,
131
+ abi: balanceAbi,
132
+ functionName: "balanceOf",
133
+ args: [baseAccount.address],
134
+ });
135
+ return {
136
+ chain: this.chain,
137
+ amount: Number(formatUnits(balance, token.decimals ?? 6)),
138
+ currency: token.symbol,
139
+ };
140
+ }
141
+ catch {
142
+ return { chain: this.chain, amount: 0, currency: token.symbol };
143
+ }
144
+ }));
145
+ return results;
114
146
  }
115
147
  catch {
116
- return { chain: this.chain, amount: 0, currency: "USDC" };
148
+ return this.tokens.map((t) => ({
149
+ chain: this.chain,
150
+ amount: 0,
151
+ currency: t.symbol,
152
+ }));
117
153
  }
118
154
  }
119
155
  }
@@ -1,12 +1,18 @@
1
+ import type { Network, TokenConfig } from "../config.js";
1
2
  import type { Connector, Session, SessionResult, Balance } from "./types.js";
2
3
  export declare class SolanaConnector implements Connector {
3
4
  chain: string;
5
+ network: Network;
6
+ tokens: TokenConfig[];
4
7
  private sessions;
8
+ constructor(network?: Network, tokens?: TokenConfig[]);
9
+ private get rpcUrl();
10
+ private get solanaChainId();
5
11
  openSession(params: {
6
12
  service: string;
7
13
  maxBudget: number;
8
14
  walletId: string;
9
15
  }): Promise<Session>;
10
16
  closeSession(sessionId: string): Promise<SessionResult>;
11
- getBalance(walletId: string): Promise<Balance>;
17
+ getBalances(walletId: string): Promise<Balance[]>;
12
18
  }
@@ -1,9 +1,32 @@
1
1
  import { getWallet, signTransaction } from "@open-wallet-standard/core";
2
2
  import { solana } from "@solana/mpp/client";
3
3
  import { Mppx } from "mppx/client";
4
+ const RPC_URLS = {
5
+ mainnet: "https://api.mainnet-beta.solana.com",
6
+ testnet: "https://api.devnet.solana.com",
7
+ };
8
+ const SOLANA_CHAIN_IDS = {
9
+ mainnet: "solana:mainnet-beta",
10
+ testnet: "solana:devnet",
11
+ };
12
+ const DEFAULT_TOKENS = [
13
+ { address: "native", symbol: "SOL", decimals: 9 },
14
+ ];
4
15
  export class SolanaConnector {
5
16
  chain = "solana";
17
+ network;
18
+ tokens;
6
19
  sessions = new Map();
20
+ constructor(network = "mainnet", tokens) {
21
+ this.network = network;
22
+ this.tokens = tokens ?? DEFAULT_TOKENS;
23
+ }
24
+ get rpcUrl() {
25
+ return RPC_URLS[this.network];
26
+ }
27
+ get solanaChainId() {
28
+ return SOLANA_CHAIN_IDS[this.network];
29
+ }
7
30
  async openSession(params) {
8
31
  const { service, maxBudget, walletId } = params;
9
32
  const wallet = getWallet(walletId);
@@ -11,8 +34,8 @@ export class SolanaConnector {
11
34
  if (!solanaAccount) {
12
35
  throw new Error(`Wallet '${walletId}' has no Solana account`);
13
36
  }
14
- // create an OWS-backed TransactionSigner compatible with @solana/kit
15
37
  const { address } = await import("@solana/kit");
38
+ const solanaChainId = this.solanaChainId;
16
39
  const owsSigner = {
17
40
  address: address(solanaAccount.address),
18
41
  async signTransactions(transactions) {
@@ -21,20 +44,18 @@ export class SolanaConnector {
21
44
  ? tx.serialize()
22
45
  : new Uint8Array(tx);
23
46
  const txHex = Buffer.from(txBytes).toString("hex");
24
- const result = signTransaction(walletId, "solana:mainnet-beta", txHex);
25
- // return the signed transaction bytes
47
+ const result = signTransaction(walletId, solanaChainId, txHex);
26
48
  return Buffer.from(result.signature, "hex");
27
49
  }));
28
50
  },
29
51
  };
30
- // create @solana/mpp charge method with OWS signer
31
52
  const chargeMethod = solana.charge({
32
53
  signer: owsSigner,
54
+ rpcUrl: this.rpcUrl,
33
55
  });
34
56
  const mppxClient = Mppx.create({
35
57
  methods: [chargeMethod],
36
58
  });
37
- // initiate payment via the 402 challenge-response flow
38
59
  await mppxClient.fetch(service, {
39
60
  headers: { "X-Max-Budget": String(maxBudget) },
40
61
  });
@@ -65,27 +86,54 @@ export class SolanaConnector {
65
86
  totalSpent: session.maxBudget,
66
87
  };
67
88
  }
68
- async getBalance(walletId) {
89
+ async getBalances(walletId) {
69
90
  const wallet = getWallet(walletId);
70
91
  const solanaAccount = wallet.accounts.find((a) => a.chainId.startsWith("solana"));
71
92
  if (!solanaAccount) {
72
- return { chain: this.chain, amount: 0, currency: "USDC" };
93
+ return this.tokens.map((t) => ({
94
+ chain: this.chain,
95
+ amount: 0,
96
+ currency: t.symbol,
97
+ }));
73
98
  }
74
99
  try {
75
100
  const { createSolanaRpc } = await import("@solana/kit");
76
- const rpc = createSolanaRpc("https://api.mainnet-beta.solana.com");
77
- // get native SOL balance
78
- const { value } = await rpc
79
- .getBalance(solanaAccount.address)
80
- .send();
81
- return {
82
- chain: this.chain,
83
- amount: Number(value) / 1e9,
84
- currency: "SOL",
85
- };
101
+ const rpc = createSolanaRpc(this.rpcUrl);
102
+ const results = await Promise.all(this.tokens.map(async (token) => {
103
+ try {
104
+ if (token.address === "native") {
105
+ const { value } = await rpc
106
+ .getBalance(solanaAccount.address)
107
+ .send();
108
+ return {
109
+ chain: this.chain,
110
+ amount: Number(value) / 1e9,
111
+ currency: token.symbol,
112
+ };
113
+ }
114
+ // SPL token balance
115
+ const { value } = await rpc
116
+ .getTokenAccountsByOwner(solanaAccount.address, { mint: token.address }, { encoding: "jsonParsed" })
117
+ .send();
118
+ const amount = value?.[0]?.account?.data?.parsed?.info?.tokenAmount?.uiAmount ?? 0;
119
+ return {
120
+ chain: this.chain,
121
+ amount,
122
+ currency: token.symbol,
123
+ };
124
+ }
125
+ catch {
126
+ return { chain: this.chain, amount: 0, currency: token.symbol };
127
+ }
128
+ }));
129
+ return results;
86
130
  }
87
131
  catch {
88
- return { chain: this.chain, amount: 0, currency: "SOL" };
132
+ return this.tokens.map((t) => ({
133
+ chain: this.chain,
134
+ amount: 0,
135
+ currency: t.symbol,
136
+ }));
89
137
  }
90
138
  }
91
139
  }
@@ -1,12 +1,18 @@
1
+ import type { Network, TokenConfig } from "../config.js";
1
2
  import type { Connector, Session, SessionResult, Balance } from "./types.js";
2
3
  export declare class TempoConnector implements Connector {
3
4
  chain: string;
4
- private sessions;
5
+ network: Network;
6
+ tokens: TokenConfig[];
7
+ constructor(network?: Network, tokens?: TokenConfig[]);
8
+ private getChain;
9
+ private getChainId;
5
10
  openSession(params: {
6
11
  service: string;
7
12
  maxBudget: number;
8
13
  walletId: string;
9
14
  }): Promise<Session>;
15
+ private sessions;
10
16
  closeSession(sessionId: string): Promise<SessionResult>;
11
- getBalance(walletId: string): Promise<Balance>;
17
+ getBalances(walletId: string): Promise<Balance[]>;
12
18
  }
@@ -1,46 +1,58 @@
1
1
  import { tempo as tempoClient } from "mppx/client";
2
2
  import { signMessage, getWallet } from "@open-wallet-standard/core";
3
+ const DEFAULT_TOKENS = [
4
+ { address: "0x20c0000000000000000000000000000000000000", symbol: "pathUSD", decimals: 6 },
5
+ ];
3
6
  export class TempoConnector {
4
7
  chain = "tempo";
5
- sessions = new Map();
8
+ network;
9
+ tokens;
10
+ constructor(network = "mainnet", tokens) {
11
+ this.network = network;
12
+ this.tokens = tokens ?? DEFAULT_TOKENS;
13
+ }
14
+ async getChain() {
15
+ const chains = await import("viem/chains");
16
+ return this.network === "testnet" ? chains.tempoModerato : chains.tempo;
17
+ }
18
+ getChainId() {
19
+ // Tempo Mainnet: 4217, Testnet (Moderato): 42431
20
+ return this.network === "testnet" ? "eip155:42431" : "eip155:4217";
21
+ }
6
22
  async openSession(params) {
7
23
  const { service, maxBudget, walletId } = params;
24
+ const chainId = this.getChainId();
8
25
  const wallet = getWallet(walletId);
9
26
  const evmAccount = wallet.accounts.find((a) => a.chainId.startsWith("eip155"));
10
27
  if (!evmAccount) {
11
28
  throw new Error(`Wallet '${walletId}' has no EVM account for Tempo`);
12
29
  }
13
- // create OWS-backed viem custom account for Tempo
14
30
  const { toAccount } = await import("viem/accounts");
15
- const { createPublicClient, http } = await import("viem");
16
- const { tempo: tempoChain } = await import("viem/chains");
17
31
  const owsAccount = toAccount({
18
32
  address: evmAccount.address,
19
33
  async signMessage({ message }) {
20
34
  const msgStr = typeof message === "string"
21
35
  ? message
22
36
  : Buffer.from(message.raw).toString("hex");
23
- const result = signMessage(walletId, "eip155:1", msgStr);
37
+ const result = signMessage(walletId, chainId, msgStr);
24
38
  return result.signature;
25
39
  },
26
40
  async signTransaction(tx) {
27
41
  const { signTransaction: owsSignTx } = await import("@open-wallet-standard/core");
28
42
  const txHex = typeof tx === "string" ? tx : JSON.stringify(tx);
29
- const result = owsSignTx(walletId, "eip155:1", txHex);
43
+ const result = owsSignTx(walletId, chainId, txHex);
30
44
  return result.signature;
31
45
  },
32
46
  async signTypedData(typedData) {
33
47
  const { signTypedData: owsSignTyped } = await import("@open-wallet-standard/core");
34
- const result = owsSignTyped(walletId, "eip155:1", JSON.stringify(typedData));
48
+ const result = owsSignTyped(walletId, chainId, JSON.stringify(typedData));
35
49
  return result.signature;
36
50
  },
37
51
  });
38
- // create the session manager with Tempo's built-in method
39
52
  const manager = tempoClient.session({
40
53
  account: owsAccount,
41
54
  maxDeposit: String(maxBudget),
42
55
  });
43
- // open the session (creates on-chain channel)
44
56
  await manager.open({ deposit: BigInt(Math.floor(maxBudget * 1e6)) });
45
57
  const sessionId = manager.channelId ?? crypto.randomUUID();
46
58
  this.sessions.set(sessionId, {
@@ -58,12 +70,13 @@ export class TempoConnector {
58
70
  status: "active",
59
71
  };
60
72
  }
73
+ sessions = new Map();
61
74
  async closeSession(sessionId) {
62
75
  const session = this.sessions.get(sessionId);
63
76
  if (!session) {
64
77
  throw new Error(`Session '${sessionId}' not found`);
65
78
  }
66
- const receipt = await session.manager.close();
79
+ await session.manager.close();
67
80
  this.sessions.delete(sessionId);
68
81
  const totalSpent = Number(session.manager.cumulative) / 1e6;
69
82
  return {
@@ -72,31 +85,52 @@ export class TempoConnector {
72
85
  refund: session.maxBudget - totalSpent,
73
86
  };
74
87
  }
75
- async getBalance(walletId) {
88
+ async getBalances(walletId) {
76
89
  const wallet = getWallet(walletId);
77
90
  const evmAccount = wallet.accounts.find((a) => a.chainId.startsWith("eip155"));
78
91
  if (!evmAccount) {
79
- return { chain: this.chain, amount: 0, currency: "TEMPO" };
92
+ return this.tokens.map((t) => ({
93
+ chain: this.chain,
94
+ amount: 0,
95
+ currency: t.symbol,
96
+ }));
80
97
  }
81
98
  try {
82
- const { createPublicClient, http, parseAbi } = await import("viem");
83
- const { tempo: tempoChain } = await import("viem/chains");
99
+ const { createPublicClient, http, parseAbi, formatUnits } = await import("viem");
100
+ const chain = await this.getChain();
84
101
  const client = createPublicClient({
85
- chain: tempoChain,
102
+ chain,
86
103
  transport: http(),
87
104
  });
88
- // query native balance
89
- const balance = await client.getBalance({
90
- address: evmAccount.address,
91
- });
92
- return {
93
- chain: this.chain,
94
- amount: Number(balance) / 1e18,
95
- currency: "TEMPO",
96
- };
105
+ const balanceAbi = parseAbi([
106
+ "function balanceOf(address) view returns (uint256)",
107
+ ]);
108
+ const results = await Promise.all(this.tokens.map(async (token) => {
109
+ try {
110
+ const balance = await client.readContract({
111
+ address: token.address,
112
+ abi: balanceAbi,
113
+ functionName: "balanceOf",
114
+ args: [evmAccount.address],
115
+ });
116
+ return {
117
+ chain: this.chain,
118
+ amount: Number(formatUnits(balance, token.decimals ?? 6)),
119
+ currency: token.symbol,
120
+ };
121
+ }
122
+ catch {
123
+ return { chain: this.chain, amount: 0, currency: token.symbol };
124
+ }
125
+ }));
126
+ return results;
97
127
  }
98
128
  catch {
99
- return { chain: this.chain, amount: 0, currency: "TEMPO" };
129
+ return this.tokens.map((t) => ({
130
+ chain: this.chain,
131
+ amount: 0,
132
+ currency: t.symbol,
133
+ }));
100
134
  }
101
135
  }
102
136
  }
@@ -1,12 +1,15 @@
1
+ import type { Network, TokenConfig } from "../config.js";
1
2
  export interface Connector {
2
3
  chain: string;
4
+ network: Network;
5
+ tokens: TokenConfig[];
3
6
  openSession(params: {
4
7
  service: string;
5
8
  maxBudget: number;
6
9
  walletId: string;
7
10
  }): Promise<Session>;
8
11
  closeSession(sessionId: string): Promise<SessionResult>;
9
- getBalance(walletId: string): Promise<Balance>;
12
+ getBalances(walletId: string): Promise<Balance[]>;
10
13
  }
11
14
  export interface Session {
12
15
  sessionId: string;
package/dist/index.d.ts CHANGED
@@ -2,7 +2,7 @@ import { Operator } from "./operator.js";
2
2
  export { Operator } from "./operator.js";
3
3
  export type { PayParams } from "./operator.js";
4
4
  export { loadConfig } from "./config.js";
5
- export type { OperatorConfig, WalletConfig, NotificationConfig } from "./config.js";
5
+ export type { OperatorConfig, WalletConfig, NotificationConfig, TokenConfig, Network } from "./config.js";
6
6
  export type { Connector, Session, SessionResult, Balance } from "./connectors/types.js";
7
7
  export { TempoConnector } from "./connectors/tempo.js";
8
8
  export { BaseConnector } from "./connectors/base.js";
@@ -17,7 +17,7 @@ export declare class Operator {
17
17
  start(): Promise<void>;
18
18
  shutdown(): Promise<void>;
19
19
  pay(walletName: string, params: PayParams): Promise<SessionResult>;
20
- getBalance(walletName: string, chain: string): Promise<Balance>;
20
+ getBalances(walletName: string, chain: string): Promise<Balance[]>;
21
21
  closeSession(walletName: string, sessionId: string): Promise<SessionResult>;
22
22
  getWalletNames(): string[];
23
23
  private getWalletConfig;
package/dist/operator.js CHANGED
@@ -18,24 +18,30 @@ export class Operator {
18
18
  this.initNotifications();
19
19
  }
20
20
  initConnectors() {
21
- const connectorInstances = {
22
- tempo: new TempoConnector(),
23
- base: new BaseConnector(),
24
- solana: new SolanaConnector(),
25
- };
26
- // register only the connectors that wallets actually use
27
- const neededChains = new Set();
21
+ // connectors keyed by "chain:network" to support different networks per wallet
28
22
  for (const wallet of Object.values(this.config.wallets)) {
23
+ const network = wallet.network ?? "mainnet";
29
24
  for (const chain of wallet.chains) {
30
- neededChains.add(chain);
31
- }
32
- }
33
- for (const chain of neededChains) {
34
- const connector = connectorInstances[chain];
35
- if (!connector) {
36
- throw new Error(`No built-in connector for chain '${chain}'`);
25
+ const key = `${chain}:${network}`;
26
+ if (this.connectors.has(key))
27
+ continue;
28
+ const tokens = wallet.tokens?.[chain];
29
+ let connector;
30
+ switch (chain) {
31
+ case "tempo":
32
+ connector = new TempoConnector(network, tokens);
33
+ break;
34
+ case "base":
35
+ connector = new BaseConnector(network, tokens);
36
+ break;
37
+ case "solana":
38
+ connector = new SolanaConnector(network, tokens);
39
+ break;
40
+ default:
41
+ throw new Error(`No built-in connector for chain '${chain}'`);
42
+ }
43
+ this.connectors.set(key, connector);
37
44
  }
38
- this.connectors.set(chain, connector);
39
45
  }
40
46
  }
41
47
  initNotifications() {
@@ -126,17 +132,18 @@ export class Operator {
126
132
  recordSpend(walletName, params.service, params.chain, result.totalSpent);
127
133
  return result;
128
134
  }
129
- async getBalance(walletName, chain) {
135
+ async getBalances(walletName, chain) {
130
136
  const wallet = this.getWalletConfig(walletName);
131
137
  const connector = this.getConnector(walletName, chain);
132
- return connector.getBalance(wallet.walletId);
138
+ return connector.getBalances(wallet.walletId);
133
139
  }
134
140
  async closeSession(walletName, sessionId) {
135
141
  // find the connector that has this session
136
142
  // try all connectors for this wallet
137
143
  const wallet = this.getWalletConfig(walletName);
144
+ const network = wallet.network ?? "mainnet";
138
145
  for (const chain of wallet.chains) {
139
- const connector = this.connectors.get(chain);
146
+ const connector = this.connectors.get(`${chain}:${network}`);
140
147
  if (!connector)
141
148
  continue;
142
149
  try {
@@ -165,9 +172,11 @@ export class Operator {
165
172
  if (!wallet.chains.includes(chain)) {
166
173
  throw new Error(`Wallet '${walletName}' is not configured for chain '${chain}'. Available: ${wallet.chains.join(", ")}`);
167
174
  }
168
- const connector = this.connectors.get(chain);
175
+ const network = wallet.network ?? "mainnet";
176
+ const key = `${chain}:${network}`;
177
+ const connector = this.connectors.get(key);
169
178
  if (!connector) {
170
- throw new Error(`No connector found for chain '${chain}'`);
179
+ throw new Error(`No connector found for chain '${chain}' on ${network}`);
171
180
  }
172
181
  return connector;
173
182
  }
@@ -182,11 +191,14 @@ export class Operator {
182
191
  const balances = {};
183
192
  for (const chain of wallet.chains) {
184
193
  try {
185
- const balance = await this.getBalance(walletName, chain);
186
- balances[chain] = balance.amount;
194
+ const chainBalances = await this.getBalances(walletName, chain);
195
+ balances[chain] = {};
196
+ for (const b of chainBalances) {
197
+ balances[chain][b.currency] = b.amount;
198
+ }
187
199
  }
188
200
  catch {
189
- balances[chain] = -1;
201
+ balances[chain] = {};
190
202
  }
191
203
  }
192
204
  await this.notifications.notify(walletName, {
@@ -2,19 +2,21 @@ export async function checkLowBalance(walletName, walletId, chain, lowBalanceThr
2
2
  if (lowBalanceThreshold === undefined)
3
3
  return;
4
4
  try {
5
- const balance = await connector.getBalance(walletId);
6
- if (balance.amount < lowBalanceThreshold) {
7
- await provider.send({
8
- type: "low_balance",
9
- walletName,
10
- chain,
11
- data: {
12
- balance: balance.amount,
13
- currency: balance.currency,
14
- threshold: lowBalanceThreshold,
15
- },
16
- timestamp: new Date().toISOString(),
17
- }).catch(() => { });
5
+ const balances = await connector.getBalances(walletId);
6
+ for (const balance of balances) {
7
+ if (balance.amount < lowBalanceThreshold) {
8
+ await provider.send({
9
+ type: "low_balance",
10
+ walletName,
11
+ chain,
12
+ data: {
13
+ balance: balance.amount,
14
+ currency: balance.currency,
15
+ threshold: lowBalanceThreshold,
16
+ },
17
+ timestamp: new Date().toISOString(),
18
+ }).catch(() => { });
19
+ }
18
20
  }
19
21
  }
20
22
  catch {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-operator",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "Drop-in toolkit for governed, observable AI agent payments",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",