@rubicon-caliga/agent-sdk 0.1.1 → 0.1.3

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.
@@ -15,8 +15,24 @@ export interface CircleCliGatewayPaymentEngineOptions {
15
15
  /**
16
16
  * Agent Wallet address controlled by Circle CLI. When omitted, the engine
17
17
  * resolves the sole agent wallet returned by `circle wallet list`.
18
+ *
19
+ * @deprecated Use `agentWalletAddress`. This alias is kept for existing SDK
20
+ * callers and is treated as the Circle CLI signing wallet, not the x402
21
+ * authorization `from` address.
18
22
  */
19
23
  walletAddress?: `0x${string}`;
24
+ /**
25
+ * Agent Wallet address controlled by Circle CLI. This is passed to
26
+ * `circle wallet sign typed-data --address`.
27
+ */
28
+ agentWalletAddress?: `0x${string}`;
29
+ /**
30
+ * Gateway backing EOA used as the x402 authorization `from` address. When
31
+ * omitted, the engine resolves it from `circle gateway balance`.
32
+ */
33
+ buyerWalletAddress?: `0x${string}`;
34
+ /** Alias for `buyerWalletAddress`. */
35
+ backingEOA?: `0x${string}`;
20
36
  /** Circle CLI chain name. Rubicon real reads settle on Arc Testnet by default. */
21
37
  chain?: string;
22
38
  /** Circle CLI binary name or path. */
@@ -33,10 +49,10 @@ interface TypedDataRequest {
33
49
  }
34
50
 
35
51
  /**
36
- * Circle CLI / Agent Wallet payment engine. It creates the one-word x402
37
- * payment payload for Rubicon's session-first flow and delegates EIP-712
38
- * signing to `circle wallet sign typed-data`, so agents never need raw private
39
- * keys or hand-built x402 payloads.
52
+ * Circle CLI / Agent Wallet payment engine. It delegates EIP-712 signing to
53
+ * `circle wallet sign typed-data`, so agents never need raw private keys or
54
+ * hand-built Circle / Arc payloads. Current gateways may call the legacy
55
+ * one-word method as a chunk-compatibility path.
40
56
  */
41
57
  export class CircleCliGatewayPaymentEngine implements AgentPaymentEngine {
42
58
  private readonly x402 = new x402Client();
@@ -44,7 +60,8 @@ export class CircleCliGatewayPaymentEngine implements AgentPaymentEngine {
44
60
 
45
61
  constructor(options: CircleCliGatewayPaymentEngineOptions = {}) {
46
62
  this.signer = new CircleCliGatewaySigner({
47
- walletAddress: options.walletAddress,
63
+ agentWalletAddress: options.agentWalletAddress ?? options.walletAddress,
64
+ buyerWalletAddress: options.buyerWalletAddress ?? options.backingEOA,
48
65
  chain: options.chain ?? "ARC-TESTNET",
49
66
  command: options.command ?? "circle",
50
67
  runner: options.runner ?? runCircleCli,
@@ -57,30 +74,56 @@ export class CircleCliGatewayPaymentEngine implements AgentPaymentEngine {
57
74
 
58
75
  async createWordPayment(session: StartSessionResponse): Promise<StreamPaymentRequest> {
59
76
  if (!session.paymentRequired) {
60
- throw new Error("Session did not include an x402 one-word payment requirement");
77
+ throw new Error("Session did not include a legacy x402 payment requirement");
61
78
  }
62
79
  await this.signer.ensureAddress();
63
80
  return {
64
81
  paymentPayload: await this.x402.createPaymentPayload(session.paymentRequired as never),
65
82
  };
66
83
  }
84
+
85
+ async createChunkPayment(session: StartSessionResponse, input: { nextSequence: number; maxWords: number }): Promise<StreamPaymentRequest> {
86
+ if (!session.paymentRequired) {
87
+ throw new Error("Session did not include an x402 payment requirement");
88
+ }
89
+ await this.signer.ensureAddress();
90
+ const amountAtomic = BigInt(session.wordPaymentAtomic) * BigInt(input.maxWords);
91
+ return {
92
+ paymentPayload: await this.x402.createPaymentPayload(
93
+ withChunkRequirement(session.paymentRequired, {
94
+ amountAtomic: `${amountAtomic}`,
95
+ maxWords: input.maxWords,
96
+ nextSequence: input.nextSequence,
97
+ sessionId: session.sessionId,
98
+ }) as never,
99
+ ),
100
+ maxWords: input.maxWords,
101
+ };
102
+ }
67
103
  }
68
104
 
69
- class CircleCliGatewaySigner {
105
+ export class CircleCliGatewaySigner {
106
+ agentWalletAddress: `0x${string}` = "0x0000000000000000000000000000000000000000";
70
107
  address: `0x${string}` = "0x0000000000000000000000000000000000000000";
71
108
  private resolved = false;
72
109
  private resolving?: Promise<void>;
73
110
 
74
111
  constructor(
75
112
  private readonly options: {
76
- walletAddress?: `0x${string}`;
113
+ agentWalletAddress?: `0x${string}`;
114
+ buyerWalletAddress?: `0x${string}`;
77
115
  chain: string;
78
116
  command: string;
79
117
  runner: CircleCliRunner;
80
118
  },
81
119
  ) {
82
- if (options.walletAddress) {
83
- this.address = options.walletAddress;
120
+ if (options.agentWalletAddress) {
121
+ this.agentWalletAddress = options.agentWalletAddress;
122
+ }
123
+ if (options.buyerWalletAddress) {
124
+ this.address = options.buyerWalletAddress;
125
+ }
126
+ if (options.agentWalletAddress && options.buyerWalletAddress) {
84
127
  this.resolved = true;
85
128
  }
86
129
  }
@@ -101,7 +144,7 @@ class CircleCliGatewaySigner {
101
144
  "typed-data",
102
145
  serializeTypedData(toEip712Payload(typed)),
103
146
  "--address",
104
- this.address,
147
+ this.agentWalletAddress,
105
148
  "--chain",
106
149
  this.options.chain,
107
150
  "--quiet",
@@ -110,6 +153,15 @@ class CircleCliGatewaySigner {
110
153
  }
111
154
 
112
155
  private async resolveAddress(): Promise<void> {
156
+ const agentWalletAddress =
157
+ this.options.agentWalletAddress ?? (await this.resolveAgentWalletAddress());
158
+ this.agentWalletAddress = agentWalletAddress;
159
+ this.address =
160
+ this.options.buyerWalletAddress ?? (await this.resolveBackingEOA(agentWalletAddress));
161
+ this.resolved = true;
162
+ }
163
+
164
+ private async resolveAgentWalletAddress(): Promise<`0x${string}`> {
113
165
  const output = await this.options.runner(this.options.command, [
114
166
  "wallet",
115
167
  "list",
@@ -120,9 +172,21 @@ class CircleCliGatewaySigner {
120
172
  "--output",
121
173
  "json",
122
174
  ]);
123
- const address = parseCircleCliWalletAddress(output);
124
- this.address = address;
125
- this.resolved = true;
175
+ return parseCircleCliWalletAddress(output);
176
+ }
177
+
178
+ private async resolveBackingEOA(agentWalletAddress: `0x${string}`): Promise<`0x${string}`> {
179
+ const output = await this.options.runner(this.options.command, [
180
+ "gateway",
181
+ "balance",
182
+ "--address",
183
+ agentWalletAddress,
184
+ "--chain",
185
+ this.options.chain,
186
+ "--output",
187
+ "json",
188
+ ]);
189
+ return parseCircleGatewayBackingEOA(output);
126
190
  }
127
191
  }
128
192
 
@@ -173,7 +237,16 @@ export function parseCircleCliWalletAddress(output: string): `0x${string}` {
173
237
  if (unique.length === 0) {
174
238
  throw new Error("Circle CLI did not return an Agent Wallet address");
175
239
  }
176
- throw new Error("Multiple Circle Agent Wallets found; pass walletAddress explicitly");
240
+ throw new Error("Multiple Circle Agent Wallets found; pass agentWalletAddress explicitly");
241
+ }
242
+
243
+ export function parseCircleGatewayBackingEOA(output: string): `0x${string}` {
244
+ const parsed = parseJson(output);
245
+ const backingEOA = findString(parsed, ["data.backingEOA", "backingEOA"]);
246
+ if (backingEOA && isAddress(backingEOA)) {
247
+ return backingEOA;
248
+ }
249
+ throw new Error("Circle Gateway balance did not return a backingEOA address");
177
250
  }
178
251
 
179
252
  function collectWalletCandidates(value: unknown): Record<string, unknown>[] {
@@ -209,6 +282,32 @@ function findString(value: unknown, keys: string[]): string | undefined {
209
282
  return undefined;
210
283
  }
211
284
 
285
+ function withChunkRequirement(
286
+ paymentRequired: unknown,
287
+ chunk: { amountAtomic: `${bigint}`; maxWords: number; nextSequence: number; sessionId: string },
288
+ ): unknown {
289
+ const source = paymentRequired as { accepts?: Array<Record<string, unknown>> };
290
+ if (!Array.isArray(source.accepts)) {
291
+ return paymentRequired;
292
+ }
293
+ return {
294
+ ...(paymentRequired as Record<string, unknown>),
295
+ accepts: source.accepts.map((accept) => ({
296
+ ...accept,
297
+ amount: chunk.amountAtomic,
298
+ extra: {
299
+ ...((accept.extra as Record<string, unknown> | undefined) ?? {}),
300
+ amountAtomic: chunk.amountAtomic,
301
+ maxWords: chunk.maxWords,
302
+ sequence: chunk.nextSequence,
303
+ authorizationMode: "chunk",
304
+ nonce: `${chunk.sessionId}:${chunk.nextSequence}:${chunk.maxWords}`,
305
+ idempotencyKey: `${chunk.sessionId}:${chunk.nextSequence}:${chunk.maxWords}`,
306
+ },
307
+ })),
308
+ };
309
+ }
310
+
212
311
  function parseJson(value: string): unknown {
213
312
  try {
214
313
  return JSON.parse(value);
@@ -1,20 +1,54 @@
1
- import type { StartSessionResponse, StreamPaymentRequest } from "@rubicon-caliga/core";
1
+ import type { StartSessionResponse, StreamAuthorizationRequest, StreamPaymentRequest } from "@rubicon-caliga/core";
2
2
 
3
3
  /**
4
- * Produces the payment payload for exactly one word. Called once per word by the
5
- * SDK's read loop application developers never assemble payments themselves.
4
+ * Produces Circle / Arc authorization payloads for Rubicon reads. Preferred
5
+ * engines authorize a whole session once; fallback engines authorize chunks.
6
+ * Legacy one-word payloads remain supported for older gateways and tests.
6
7
  */
7
8
  export interface AgentPaymentEngine {
9
+ createSessionAuthorization?(session: StartSessionResponse): Promise<StreamAuthorizationRequest>;
10
+ createChunkPayment?(session: StartSessionResponse, input: { nextSequence: number; maxWords: number }): Promise<StreamPaymentRequest>;
11
+ /** @deprecated Compatibility path for one-word x402 gateways. */
8
12
  createWordPayment(session: StartSessionResponse): Promise<StreamPaymentRequest>;
9
13
  }
10
14
 
11
15
  /**
12
- * Development engine. Declares the one-word amount without settling real funds,
13
- * for use against a dev-mode gateway. NOT for production.
16
+ * Development engine. Declares the authorized amount without settling real
17
+ * funds, for use against a dev-mode gateway. NOT for production.
14
18
  */
15
19
  export class StaticPaymentEngine implements AgentPaymentEngine {
16
20
  constructor(private readonly network = "eip155:5042002") {}
17
21
 
22
+ async createSessionAuthorization(session: StartSessionResponse): Promise<StreamAuthorizationRequest> {
23
+ return {
24
+ authorizationPayload: {
25
+ scheme: "development-static",
26
+ network: this.network,
27
+ sessionId: session.sessionId,
28
+ amountAtomic: session.authorizationRequired?.maxAuthorizedAtomic ?? session.maxArticlePriceAtomic,
29
+ meteringUnit: "word",
30
+ authorizationMode: "session",
31
+ },
32
+ };
33
+ }
34
+
35
+ async createChunkPayment(session: StartSessionResponse, input: { nextSequence: number; maxWords: number }): Promise<StreamPaymentRequest> {
36
+ const amountAtomic = BigInt(session.wordPaymentAtomic) * BigInt(input.maxWords);
37
+ return {
38
+ paymentPayload: {
39
+ scheme: "development-static",
40
+ network: this.network,
41
+ sessionId: session.sessionId,
42
+ amountAtomic: `${amountAtomic}`,
43
+ meteringUnit: "word",
44
+ authorizationMode: "chunk",
45
+ maxWords: input.maxWords,
46
+ nextSequence: input.nextSequence,
47
+ },
48
+ maxWords: input.maxWords,
49
+ };
50
+ }
51
+
18
52
  async createWordPayment(session: StartSessionResponse): Promise<StreamPaymentRequest> {
19
53
  return {
20
54
  paymentPayload: {