@rubicon-caliga/agent-sdk 0.1.2 → 0.1.4

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.
@@ -2,10 +2,8 @@ import type { StartSessionResponse, StreamPaymentRequest } from "@rubicon-caliga
2
2
  import { x402Client } from "@x402/core/client";
3
3
  import { registerBatchScheme } from "@circle-fin/x402-batching/client";
4
4
  import { ExactEvmScheme } from "@x402/evm/exact/client";
5
- import {
6
- initiateDeveloperControlledWalletsClient,
7
- type CircleDeveloperControlledWalletsClient,
8
- } from "@circle-fin/developer-controlled-wallets";
5
+ import * as CircleWallets from "@circle-fin/developer-controlled-wallets";
6
+ import type { CircleDeveloperControlledWalletsClient } from "@circle-fin/developer-controlled-wallets";
9
7
  import type { AgentPaymentEngine } from "./payment-engine.js";
10
8
 
11
9
  export interface CircleAgentWalletEngineOptions {
@@ -13,7 +11,7 @@ export interface CircleAgentWalletEngineOptions {
13
11
  apiKey: string;
14
12
  /** Entity secret registered for the Circle developer account. */
15
13
  entitySecret: string;
16
- /** The Agent Wallet that holds USDC and signs each one-word payment. */
14
+ /** The Agent Wallet that holds USDC and signs Circle / Arc authorizations. */
17
15
  walletId: string;
18
16
  /**
19
17
  * The wallet's on-chain address. Optional — when omitted it is resolved once
@@ -80,7 +78,7 @@ class CircleAgentWalletSigner {
80
78
  const res = await this.client.signTypedData({
81
79
  walletId: this.walletId,
82
80
  data: serializeTypedData(toEip712Payload(typed)),
83
- memo: "Rubicon one-word payment",
81
+ memo: "Rubicon reading authorization",
84
82
  });
85
83
  const signature = res.data?.signature;
86
84
  if (!signature) {
@@ -91,10 +89,10 @@ class CircleAgentWalletSigner {
91
89
  }
92
90
 
93
91
  /**
94
- * Circle Agent Wallet engine. Signs the gateway's one-word x402 terms with a
95
- * custodial Circle Agent Wallet the recommended buyer setup so the agent
96
- * never handles a local signing key. Settlement may be batched by Circle, but
97
- * each signed payload still corresponds to exactly one word.
92
+ * Circle Agent Wallet engine. Signs gateway authorization terms with a
93
+ * custodial Circle Agent Wallet so the buyer agent never handles a local
94
+ * signing key. Current gateways may still call the legacy one-word method as a
95
+ * chunk-compatibility path.
98
96
  */
99
97
  export class CircleAgentWalletEngine implements AgentPaymentEngine {
100
98
  private readonly x402 = new x402Client();
@@ -103,7 +101,7 @@ export class CircleAgentWalletEngine implements AgentPaymentEngine {
103
101
  constructor(options: CircleAgentWalletEngineOptions) {
104
102
  const client =
105
103
  options.client ??
106
- initiateDeveloperControlledWalletsClient({
104
+ CircleWallets.initiateDeveloperControlledWalletsClient({
107
105
  apiKey: options.apiKey,
108
106
  entitySecret: options.entitySecret,
109
107
  ...(options.baseUrl ? { baseUrl: options.baseUrl } : {}),
@@ -119,7 +117,7 @@ export class CircleAgentWalletEngine implements AgentPaymentEngine {
119
117
 
120
118
  async createWordPayment(session: StartSessionResponse): Promise<StreamPaymentRequest> {
121
119
  if (!session.paymentRequired) {
122
- throw new Error("Session did not include an x402 one-word payment requirement");
120
+ throw new Error("Session did not include a legacy x402 payment requirement");
123
121
  }
124
122
  // Resolve the wallet address up front so the synchronous `address` read
125
123
  // inside createPaymentPayload sees a real value.
@@ -128,6 +126,25 @@ export class CircleAgentWalletEngine implements AgentPaymentEngine {
128
126
  paymentPayload: await this.x402.createPaymentPayload(session.paymentRequired as never),
129
127
  };
130
128
  }
129
+
130
+ async createChunkPayment(session: StartSessionResponse, input: { nextSequence: number; maxWords: number }): Promise<StreamPaymentRequest> {
131
+ if (!session.paymentRequired) {
132
+ throw new Error("Session did not include an x402 payment requirement");
133
+ }
134
+ await this.signer.ensureAddress();
135
+ const amountAtomic = BigInt(session.wordPaymentAtomic) * BigInt(input.maxWords);
136
+ return {
137
+ paymentPayload: await this.x402.createPaymentPayload(
138
+ withChunkRequirement(session.paymentRequired, {
139
+ amountAtomic: `${amountAtomic}`,
140
+ maxWords: input.maxWords,
141
+ nextSequence: input.nextSequence,
142
+ sessionId: session.sessionId,
143
+ }) as never,
144
+ ),
145
+ maxWords: input.maxWords,
146
+ };
147
+ }
131
148
  }
132
149
 
133
150
  /**
@@ -208,3 +225,29 @@ function isEip712Field(field: unknown): field is { name: string; type: string }
208
225
  typeof (field as { type?: unknown }).type === "string"
209
226
  );
210
227
  }
228
+
229
+ function withChunkRequirement(
230
+ paymentRequired: unknown,
231
+ chunk: { amountAtomic: `${bigint}`; maxWords: number; nextSequence: number; sessionId: string },
232
+ ): unknown {
233
+ const source = paymentRequired as { accepts?: Array<Record<string, unknown>> };
234
+ if (!Array.isArray(source.accepts)) {
235
+ return paymentRequired;
236
+ }
237
+ return {
238
+ ...(paymentRequired as Record<string, unknown>),
239
+ accepts: source.accepts.map((accept) => ({
240
+ ...accept,
241
+ amount: chunk.amountAtomic,
242
+ extra: {
243
+ ...((accept.extra as Record<string, unknown> | undefined) ?? {}),
244
+ amountAtomic: chunk.amountAtomic,
245
+ maxWords: chunk.maxWords,
246
+ sequence: chunk.nextSequence,
247
+ authorizationMode: "chunk",
248
+ nonce: `${chunk.sessionId}:${chunk.nextSequence}:${chunk.maxWords}`,
249
+ idempotencyKey: `${chunk.sessionId}:${chunk.nextSequence}:${chunk.maxWords}`,
250
+ },
251
+ })),
252
+ };
253
+ }
@@ -49,10 +49,10 @@ interface TypedDataRequest {
49
49
  }
50
50
 
51
51
  /**
52
- * Circle CLI / Agent Wallet payment engine. It creates the one-word x402
53
- * payment payload for Rubicon's session-first flow and delegates EIP-712
54
- * signing to `circle wallet sign typed-data`, so agents never need raw private
55
- * 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.
56
56
  */
57
57
  export class CircleCliGatewayPaymentEngine implements AgentPaymentEngine {
58
58
  private readonly x402 = new x402Client();
@@ -74,13 +74,32 @@ export class CircleCliGatewayPaymentEngine implements AgentPaymentEngine {
74
74
 
75
75
  async createWordPayment(session: StartSessionResponse): Promise<StreamPaymentRequest> {
76
76
  if (!session.paymentRequired) {
77
- 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");
78
78
  }
79
79
  await this.signer.ensureAddress();
80
80
  return {
81
81
  paymentPayload: await this.x402.createPaymentPayload(session.paymentRequired as never),
82
82
  };
83
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
+ }
84
103
  }
85
104
 
86
105
  export class CircleCliGatewaySigner {
@@ -263,6 +282,32 @@ function findString(value: unknown, keys: string[]): string | undefined {
263
282
  return undefined;
264
283
  }
265
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
+
266
311
  function parseJson(value: string): unknown {
267
312
  try {
268
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: {