@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.
- package/README.md +28 -24
- package/dist/agent-client.d.ts +41 -5
- package/dist/agent-client.d.ts.map +1 -1
- package/dist/agent-client.js +120 -4
- package/dist/agent-client.js.map +1 -1
- package/dist/agent-client.test.js +229 -3
- package/dist/agent-client.test.js.map +1 -1
- package/dist/circle-agent-wallet.d.ts +10 -6
- package/dist/circle-agent-wallet.d.ts.map +1 -1
- package/dist/circle-agent-wallet.js +46 -8
- package/dist/circle-agent-wallet.js.map +1 -1
- package/dist/circle-cli-gateway-payment.d.ts +8 -4
- package/dist/circle-cli-gateway-payment.d.ts.map +1 -1
- package/dist/circle-cli-gateway-payment.js +43 -5
- package/dist/circle-cli-gateway-payment.js.map +1 -1
- package/dist/payment-engine.d.ts +17 -5
- package/dist/payment-engine.d.ts.map +1 -1
- package/dist/payment-engine.js +30 -2
- package/dist/payment-engine.js.map +1 -1
- package/package.json +2 -2
- package/src/agent-client.test.ts +241 -3
- package/src/agent-client.ts +148 -4
- package/src/circle-agent-wallet.ts +55 -12
- package/src/circle-cli-gateway-payment.ts +50 -5
- package/src/payment-engine.ts +39 -5
package/dist/payment-engine.d.ts
CHANGED
|
@@ -1,18 +1,30 @@
|
|
|
1
|
-
import type { StartSessionResponse, StreamPaymentRequest } from "@rubicon-caliga/core";
|
|
1
|
+
import type { StartSessionResponse, StreamAuthorizationRequest, StreamPaymentRequest } from "@rubicon-caliga/core";
|
|
2
2
|
/**
|
|
3
|
-
* Produces
|
|
4
|
-
*
|
|
3
|
+
* Produces Circle / Arc authorization payloads for Rubicon reads. Preferred
|
|
4
|
+
* engines authorize a whole session once; fallback engines authorize chunks.
|
|
5
|
+
* Legacy one-word payloads remain supported for older gateways and tests.
|
|
5
6
|
*/
|
|
6
7
|
export interface AgentPaymentEngine {
|
|
8
|
+
createSessionAuthorization?(session: StartSessionResponse): Promise<StreamAuthorizationRequest>;
|
|
9
|
+
createChunkPayment?(session: StartSessionResponse, input: {
|
|
10
|
+
nextSequence: number;
|
|
11
|
+
maxWords: number;
|
|
12
|
+
}): Promise<StreamPaymentRequest>;
|
|
13
|
+
/** @deprecated Compatibility path for one-word x402 gateways. */
|
|
7
14
|
createWordPayment(session: StartSessionResponse): Promise<StreamPaymentRequest>;
|
|
8
15
|
}
|
|
9
16
|
/**
|
|
10
|
-
* Development engine. Declares the
|
|
11
|
-
* for use against a dev-mode gateway. NOT for production.
|
|
17
|
+
* Development engine. Declares the authorized amount without settling real
|
|
18
|
+
* funds, for use against a dev-mode gateway. NOT for production.
|
|
12
19
|
*/
|
|
13
20
|
export declare class StaticPaymentEngine implements AgentPaymentEngine {
|
|
14
21
|
private readonly network;
|
|
15
22
|
constructor(network?: string);
|
|
23
|
+
createSessionAuthorization(session: StartSessionResponse): Promise<StreamAuthorizationRequest>;
|
|
24
|
+
createChunkPayment(session: StartSessionResponse, input: {
|
|
25
|
+
nextSequence: number;
|
|
26
|
+
maxWords: number;
|
|
27
|
+
}): Promise<StreamPaymentRequest>;
|
|
16
28
|
createWordPayment(session: StartSessionResponse): Promise<StreamPaymentRequest>;
|
|
17
29
|
}
|
|
18
30
|
//# sourceMappingURL=payment-engine.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"payment-engine.d.ts","sourceRoot":"","sources":["../src/payment-engine.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"payment-engine.d.ts","sourceRoot":"","sources":["../src/payment-engine.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,0BAA0B,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAEnH;;;;GAIG;AACH,MAAM,WAAW,kBAAkB;IACjC,0BAA0B,CAAC,CAAC,OAAO,EAAE,oBAAoB,GAAG,OAAO,CAAC,0BAA0B,CAAC,CAAC;IAChG,kBAAkB,CAAC,CAAC,OAAO,EAAE,oBAAoB,EAAE,KAAK,EAAE;QAAE,YAAY,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAAC;IACrI,iEAAiE;IACjE,iBAAiB,CAAC,OAAO,EAAE,oBAAoB,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAAC;CACjF;AAED;;;GAGG;AACH,qBAAa,mBAAoB,YAAW,kBAAkB;IAChD,OAAO,CAAC,QAAQ,CAAC,OAAO;gBAAP,OAAO,SAAmB;IAEjD,0BAA0B,CAAC,OAAO,EAAE,oBAAoB,GAAG,OAAO,CAAC,0BAA0B,CAAC;IAa9F,kBAAkB,CAAC,OAAO,EAAE,oBAAoB,EAAE,KAAK,EAAE;QAAE,YAAY,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,oBAAoB,CAAC;IAiBnI,iBAAiB,CAAC,OAAO,EAAE,oBAAoB,GAAG,OAAO,CAAC,oBAAoB,CAAC;CAWtF"}
|
package/dist/payment-engine.js
CHANGED
|
@@ -1,12 +1,40 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Development engine. Declares the
|
|
3
|
-
* for use against a dev-mode gateway. NOT for production.
|
|
2
|
+
* Development engine. Declares the authorized amount without settling real
|
|
3
|
+
* funds, for use against a dev-mode gateway. NOT for production.
|
|
4
4
|
*/
|
|
5
5
|
export class StaticPaymentEngine {
|
|
6
6
|
network;
|
|
7
7
|
constructor(network = "eip155:5042002") {
|
|
8
8
|
this.network = network;
|
|
9
9
|
}
|
|
10
|
+
async createSessionAuthorization(session) {
|
|
11
|
+
return {
|
|
12
|
+
authorizationPayload: {
|
|
13
|
+
scheme: "development-static",
|
|
14
|
+
network: this.network,
|
|
15
|
+
sessionId: session.sessionId,
|
|
16
|
+
amountAtomic: session.authorizationRequired?.maxAuthorizedAtomic ?? session.maxArticlePriceAtomic,
|
|
17
|
+
meteringUnit: "word",
|
|
18
|
+
authorizationMode: "session",
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
async createChunkPayment(session, input) {
|
|
23
|
+
const amountAtomic = BigInt(session.wordPaymentAtomic) * BigInt(input.maxWords);
|
|
24
|
+
return {
|
|
25
|
+
paymentPayload: {
|
|
26
|
+
scheme: "development-static",
|
|
27
|
+
network: this.network,
|
|
28
|
+
sessionId: session.sessionId,
|
|
29
|
+
amountAtomic: `${amountAtomic}`,
|
|
30
|
+
meteringUnit: "word",
|
|
31
|
+
authorizationMode: "chunk",
|
|
32
|
+
maxWords: input.maxWords,
|
|
33
|
+
nextSequence: input.nextSequence,
|
|
34
|
+
},
|
|
35
|
+
maxWords: input.maxWords,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
10
38
|
async createWordPayment(session) {
|
|
11
39
|
return {
|
|
12
40
|
paymentPayload: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"payment-engine.js","sourceRoot":"","sources":["../src/payment-engine.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"payment-engine.js","sourceRoot":"","sources":["../src/payment-engine.ts"],"names":[],"mappings":"AAcA;;;GAGG;AACH,MAAM,OAAO,mBAAmB;IACD;IAA7B,YAA6B,UAAU,gBAAgB;QAA1B,YAAO,GAAP,OAAO,CAAmB;IAAG,CAAC;IAE3D,KAAK,CAAC,0BAA0B,CAAC,OAA6B;QAC5D,OAAO;YACL,oBAAoB,EAAE;gBACpB,MAAM,EAAE,oBAAoB;gBAC5B,OAAO,EAAE,IAAI,CAAC,OAAO;gBACrB,SAAS,EAAE,OAAO,CAAC,SAAS;gBAC5B,YAAY,EAAE,OAAO,CAAC,qBAAqB,EAAE,mBAAmB,IAAI,OAAO,CAAC,qBAAqB;gBACjG,YAAY,EAAE,MAAM;gBACpB,iBAAiB,EAAE,SAAS;aAC7B;SACF,CAAC;IACJ,CAAC;IAED,KAAK,CAAC,kBAAkB,CAAC,OAA6B,EAAE,KAAiD;QACvG,MAAM,YAAY,GAAG,MAAM,CAAC,OAAO,CAAC,iBAAiB,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;QAChF,OAAO;YACL,cAAc,EAAE;gBACd,MAAM,EAAE,oBAAoB;gBAC5B,OAAO,EAAE,IAAI,CAAC,OAAO;gBACrB,SAAS,EAAE,OAAO,CAAC,SAAS;gBAC5B,YAAY,EAAE,GAAG,YAAY,EAAE;gBAC/B,YAAY,EAAE,MAAM;gBACpB,iBAAiB,EAAE,OAAO;gBAC1B,QAAQ,EAAE,KAAK,CAAC,QAAQ;gBACxB,YAAY,EAAE,KAAK,CAAC,YAAY;aACjC;YACD,QAAQ,EAAE,KAAK,CAAC,QAAQ;SACzB,CAAC;IACJ,CAAC;IAED,KAAK,CAAC,iBAAiB,CAAC,OAA6B;QACnD,OAAO;YACL,cAAc,EAAE;gBACd,MAAM,EAAE,oBAAoB;gBAC5B,OAAO,EAAE,IAAI,CAAC,OAAO;gBACrB,SAAS,EAAE,OAAO,CAAC,SAAS;gBAC5B,YAAY,EAAE,OAAO,CAAC,iBAAiB;gBACvC,YAAY,EAAE,MAAM;aACrB;SACF,CAAC;IACJ,CAAC;CACF"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rubicon-caliga/agent-sdk",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Client SDK for autonomous agents consuming per-word article streams via Rubicon x402.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"@x402/evm": "^2.15.0",
|
|
32
32
|
"eventsource": "^4.0.0",
|
|
33
33
|
"viem": "^2.52.2",
|
|
34
|
-
"@rubicon-caliga/core": "0.1.
|
|
34
|
+
"@rubicon-caliga/core": "0.1.2"
|
|
35
35
|
},
|
|
36
36
|
"scripts": {
|
|
37
37
|
"build": "tsc -p tsconfig.json",
|
package/src/agent-client.test.ts
CHANGED
|
@@ -9,6 +9,18 @@ const paymentEngine: AgentPaymentEngine = {
|
|
|
9
9
|
},
|
|
10
10
|
};
|
|
11
11
|
|
|
12
|
+
const chunkPaymentEngine: AgentPaymentEngine = {
|
|
13
|
+
async createWordPayment() {
|
|
14
|
+
throw new Error("word fallback should not be used");
|
|
15
|
+
},
|
|
16
|
+
async createChunkPayment(_session, input) {
|
|
17
|
+
return {
|
|
18
|
+
paymentPayload: { amountAtomic: `${input.maxWords}` },
|
|
19
|
+
maxWords: input.maxWords,
|
|
20
|
+
};
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
|
|
12
24
|
test("run receipt preserves Gateway settlement receipt fields", async () => {
|
|
13
25
|
const fetcher = (async (input: Parameters<typeof fetch>[0]) => {
|
|
14
26
|
const url = String(input);
|
|
@@ -80,6 +92,218 @@ test("run receipt preserves Gateway settlement receipt fields", async () => {
|
|
|
80
92
|
assert.equal(receipt.network, "eip155:5042002");
|
|
81
93
|
});
|
|
82
94
|
|
|
95
|
+
test("run can consume chunk stream while preserving per-word receipt fields", async () => {
|
|
96
|
+
const fetcher = (async (input: Parameters<typeof fetch>[0], init?: Parameters<typeof fetch>[1]) => {
|
|
97
|
+
const url = String(input);
|
|
98
|
+
if (url.endsWith("/v1/sessions")) {
|
|
99
|
+
return jsonResponse({
|
|
100
|
+
sessionId: "session_1",
|
|
101
|
+
state: "active",
|
|
102
|
+
article: article(3),
|
|
103
|
+
navigation: navigation(),
|
|
104
|
+
pricePerWordAtomic: "1",
|
|
105
|
+
maxArticlePriceAtomic: "10",
|
|
106
|
+
conversationId: "conversation_1",
|
|
107
|
+
wordPaymentAtomic: "1",
|
|
108
|
+
gatewayFeeBps: 0,
|
|
109
|
+
paymentRequired: { accepts: [{ amount: "1" }] },
|
|
110
|
+
expiresAt: "2026-06-18T12:00:00.000Z",
|
|
111
|
+
wordsPaid: 0,
|
|
112
|
+
wordsDelivered: 0,
|
|
113
|
+
paidAtomic: "0",
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
if (url.endsWith("/v1/sessions/session_1/stream")) {
|
|
117
|
+
const body = JSON.parse(String(init?.body)) as { maxWords: number };
|
|
118
|
+
assert.equal(body.maxWords, 3);
|
|
119
|
+
return jsonResponse({
|
|
120
|
+
accepted: true,
|
|
121
|
+
words: [
|
|
122
|
+
{ sequence: 0, word: "Rubicon", priceAtomic: "1" },
|
|
123
|
+
{ sequence: 1, word: "streams", priceAtomic: "1" },
|
|
124
|
+
{ sequence: 2, word: "chunks", priceAtomic: "1" },
|
|
125
|
+
],
|
|
126
|
+
text: "Rubicon streams chunks",
|
|
127
|
+
wordsPaid: 3,
|
|
128
|
+
wordsDelivered: 3,
|
|
129
|
+
paidAtomic: "3",
|
|
130
|
+
completed: true,
|
|
131
|
+
settlementIds: ["settlement_chunk"],
|
|
132
|
+
payment: {
|
|
133
|
+
...payment(0, "Rubicon streams chunks"),
|
|
134
|
+
amountAtomic: "3",
|
|
135
|
+
bundleSequence: 0,
|
|
136
|
+
wordsDelivered: 3,
|
|
137
|
+
pricePerWordAtomic: "1",
|
|
138
|
+
text: "Rubicon streams chunks",
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
throw new Error(`Unexpected fetch: ${url}`);
|
|
143
|
+
}) as typeof fetch;
|
|
144
|
+
|
|
145
|
+
const client = new RubiconClient({
|
|
146
|
+
baseUrl: "http://rubicon.test",
|
|
147
|
+
paymentEngine: chunkPaymentEngine,
|
|
148
|
+
fetch: fetcher,
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
const receipt = await client.run({
|
|
152
|
+
articleId: "article_1",
|
|
153
|
+
maxSpendAtomic: "10",
|
|
154
|
+
chunkWords: 3,
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
assert.equal(receipt.text, "Rubicon streams chunks");
|
|
158
|
+
assert.equal(receipt.wordsRead, 3);
|
|
159
|
+
assert.equal(receipt.amountPaidAtomic, "3");
|
|
160
|
+
assert.equal(receipt.payments.length, 1);
|
|
161
|
+
assert.equal(receipt.payments[0]?.amountAtomic, "3");
|
|
162
|
+
assert.equal(receipt.payments[0]?.wordsDelivered, 3);
|
|
163
|
+
assert.deepEqual(receipt.settlementIds, ["settlement_chunk"]);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("read defaults to bundled mode and clamps bundle size to remaining article words", async () => {
|
|
167
|
+
const fetcher = (async (input: Parameters<typeof fetch>[0], init?: Parameters<typeof fetch>[1]) => {
|
|
168
|
+
const url = String(input);
|
|
169
|
+
if (url.endsWith("/v1/sessions")) {
|
|
170
|
+
return jsonResponse({
|
|
171
|
+
sessionId: "session_1",
|
|
172
|
+
state: "active",
|
|
173
|
+
article: article(15),
|
|
174
|
+
navigation: navigation(),
|
|
175
|
+
pricePerWordAtomic: "1",
|
|
176
|
+
maxArticlePriceAtomic: "15",
|
|
177
|
+
conversationId: "conversation_1",
|
|
178
|
+
wordPaymentAtomic: "1",
|
|
179
|
+
gatewayFeeBps: 0,
|
|
180
|
+
paymentRequired: { accepts: [{ amount: "1" }] },
|
|
181
|
+
expiresAt: "2026-06-18T12:00:00.000Z",
|
|
182
|
+
wordsPaid: 0,
|
|
183
|
+
wordsDelivered: 0,
|
|
184
|
+
paidAtomic: "0",
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
if (url.endsWith("/v1/sessions/session_1/stream")) {
|
|
188
|
+
const body = JSON.parse(String(init?.body)) as { maxWords: number };
|
|
189
|
+
assert.equal(body.maxWords, 15);
|
|
190
|
+
return jsonResponse({
|
|
191
|
+
accepted: true,
|
|
192
|
+
words: Array.from({ length: 15 }, (_, sequence) => ({ sequence, word: `w${sequence + 1}`, priceAtomic: "1" })),
|
|
193
|
+
text: Array.from({ length: 15 }, (_, index) => `w${index + 1}`).join(" "),
|
|
194
|
+
wordsPaid: 15,
|
|
195
|
+
wordsDelivered: 15,
|
|
196
|
+
paidAtomic: "15",
|
|
197
|
+
completed: true,
|
|
198
|
+
payment: { ...payment(0, "bundle"), amountAtomic: "15", wordsDelivered: 15, pricePerWordAtomic: "1" },
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
throw new Error(`Unexpected fetch: ${url}`);
|
|
202
|
+
}) as typeof fetch;
|
|
203
|
+
|
|
204
|
+
const client = new RubiconClient({ baseUrl: "http://rubicon.test", paymentEngine: { ...chunkPaymentEngine, createWordPayment: paymentEngine.createWordPayment }, fetch: fetcher });
|
|
205
|
+
const events = [];
|
|
206
|
+
for await (const event of client.read({ articleId: "article_1", maxSpendAtomic: "100" })) {
|
|
207
|
+
events.push(event.type);
|
|
208
|
+
}
|
|
209
|
+
assert.deepEqual(events, ["session.started", "article.bundle", "article.usage", "article.completed"]);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test("bundled mode clamps bundle size to budget and max words", async () => {
|
|
213
|
+
const seenMaxWords: number[] = [];
|
|
214
|
+
const fetcher = (async (input: Parameters<typeof fetch>[0], init?: Parameters<typeof fetch>[1]) => {
|
|
215
|
+
const url = String(input);
|
|
216
|
+
if (url.endsWith("/v1/sessions")) {
|
|
217
|
+
return jsonResponse({
|
|
218
|
+
sessionId: "session_1",
|
|
219
|
+
state: "active",
|
|
220
|
+
article: article(100),
|
|
221
|
+
navigation: navigation(),
|
|
222
|
+
pricePerWordAtomic: "2",
|
|
223
|
+
maxArticlePriceAtomic: "200",
|
|
224
|
+
conversationId: "conversation_1",
|
|
225
|
+
wordPaymentAtomic: "2",
|
|
226
|
+
gatewayFeeBps: 0,
|
|
227
|
+
paymentRequired: { accepts: [{ amount: "2" }] },
|
|
228
|
+
expiresAt: "2026-06-18T12:00:00.000Z",
|
|
229
|
+
wordsPaid: 0,
|
|
230
|
+
wordsDelivered: 0,
|
|
231
|
+
paidAtomic: "0",
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
if (url.endsWith("/v1/sessions/session_1/stream")) {
|
|
235
|
+
const body = JSON.parse(String(init?.body)) as { maxWords: number };
|
|
236
|
+
seenMaxWords.push(body.maxWords);
|
|
237
|
+
return jsonResponse({
|
|
238
|
+
accepted: true,
|
|
239
|
+
words: Array.from({ length: body.maxWords }, (_, sequence) => ({ sequence, word: `w${sequence + 1}`, priceAtomic: "2" })),
|
|
240
|
+
text: "ok",
|
|
241
|
+
wordsPaid: body.maxWords,
|
|
242
|
+
wordsDelivered: body.maxWords,
|
|
243
|
+
paidAtomic: `${body.maxWords * 2}`,
|
|
244
|
+
completed: true,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
throw new Error(`Unexpected fetch: ${url}`);
|
|
248
|
+
}) as typeof fetch;
|
|
249
|
+
|
|
250
|
+
const client = new RubiconClient({
|
|
251
|
+
baseUrl: "http://rubicon.test",
|
|
252
|
+
paymentEngine: { ...chunkPaymentEngine, createWordPayment: paymentEngine.createWordPayment },
|
|
253
|
+
fetch: fetcher,
|
|
254
|
+
});
|
|
255
|
+
await client.run({ articleId: "article_1", maxSpendAtomic: "12", maxWords: 10 });
|
|
256
|
+
assert.deepEqual(seenMaxWords, [6]);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
test("explicit word stream mode keeps legacy word events", async () => {
|
|
260
|
+
const fetcher = (async (input: Parameters<typeof fetch>[0]) => {
|
|
261
|
+
const url = String(input);
|
|
262
|
+
if (url.endsWith("/v1/sessions")) {
|
|
263
|
+
return jsonResponse({
|
|
264
|
+
sessionId: "session_1",
|
|
265
|
+
state: "active",
|
|
266
|
+
article: article(1),
|
|
267
|
+
navigation: navigation(),
|
|
268
|
+
pricePerWordAtomic: "1",
|
|
269
|
+
maxArticlePriceAtomic: "1",
|
|
270
|
+
conversationId: "conversation_1",
|
|
271
|
+
wordPaymentAtomic: "1",
|
|
272
|
+
gatewayFeeBps: 0,
|
|
273
|
+
paymentRequired: { accepts: [{ amount: "1" }] },
|
|
274
|
+
expiresAt: "2026-06-18T12:00:00.000Z",
|
|
275
|
+
wordsPaid: 0,
|
|
276
|
+
wordsDelivered: 0,
|
|
277
|
+
paidAtomic: "0",
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
if (url.endsWith("/v1/sessions/session_1/payments")) {
|
|
281
|
+
return jsonResponse({
|
|
282
|
+
accepted: true,
|
|
283
|
+
sequence: 0,
|
|
284
|
+
word: "Rubicon",
|
|
285
|
+
priceAtomic: "1",
|
|
286
|
+
wordsPaid: 1,
|
|
287
|
+
wordsDelivered: 1,
|
|
288
|
+
paidAtomic: "1",
|
|
289
|
+
completed: true,
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
throw new Error(`Unexpected fetch: ${url}`);
|
|
293
|
+
}) as typeof fetch;
|
|
294
|
+
|
|
295
|
+
const client = new RubiconClient({
|
|
296
|
+
baseUrl: "http://rubicon.test",
|
|
297
|
+
paymentEngine: { ...chunkPaymentEngine, createWordPayment: paymentEngine.createWordPayment },
|
|
298
|
+
fetch: fetcher,
|
|
299
|
+
});
|
|
300
|
+
const events = [];
|
|
301
|
+
for await (const event of client.read({ articleId: "article_1", maxSpendAtomic: "10", streamMode: "word" })) {
|
|
302
|
+
events.push(event.type);
|
|
303
|
+
}
|
|
304
|
+
assert.deepEqual(events, ["session.started", "article.word", "article.usage", "article.completed"]);
|
|
305
|
+
});
|
|
306
|
+
|
|
83
307
|
function jsonResponse(body: unknown): Response {
|
|
84
308
|
return new Response(JSON.stringify(body), {
|
|
85
309
|
status: 200,
|
|
@@ -87,7 +311,7 @@ function jsonResponse(body: unknown): Response {
|
|
|
87
311
|
});
|
|
88
312
|
}
|
|
89
313
|
|
|
90
|
-
function article() {
|
|
314
|
+
function article(totalWords = 1) {
|
|
91
315
|
return {
|
|
92
316
|
articleId: "article_1",
|
|
93
317
|
creatorId: "creator_1",
|
|
@@ -95,9 +319,9 @@ function article() {
|
|
|
95
319
|
title: "Title",
|
|
96
320
|
author: "Author",
|
|
97
321
|
state: "published",
|
|
98
|
-
totalWords
|
|
322
|
+
totalWords,
|
|
99
323
|
pricePerWordAtomic: "1",
|
|
100
|
-
maxArticlePriceAtomic:
|
|
324
|
+
maxArticlePriceAtomic: `${totalWords}`,
|
|
101
325
|
sections: [],
|
|
102
326
|
};
|
|
103
327
|
}
|
|
@@ -116,3 +340,17 @@ function navigation() {
|
|
|
116
340
|
stopConditions: [],
|
|
117
341
|
};
|
|
118
342
|
}
|
|
343
|
+
|
|
344
|
+
function payment(sequence: number, word: string) {
|
|
345
|
+
return {
|
|
346
|
+
paymentId: `payment_${sequence}`,
|
|
347
|
+
sessionId: "session_1",
|
|
348
|
+
articleId: "article_1",
|
|
349
|
+
sequence,
|
|
350
|
+
meteringUnit: "word",
|
|
351
|
+
amountAtomic: "1",
|
|
352
|
+
currency: "USDC",
|
|
353
|
+
settledAt: "2026-06-18T12:00:00.000Z",
|
|
354
|
+
word,
|
|
355
|
+
};
|
|
356
|
+
}
|
package/src/agent-client.ts
CHANGED
|
@@ -7,6 +7,8 @@ import type {
|
|
|
7
7
|
StartConversationResponse,
|
|
8
8
|
StartSessionRequest,
|
|
9
9
|
StartSessionResponse,
|
|
10
|
+
StreamChunkResponse,
|
|
11
|
+
StreamMode,
|
|
10
12
|
StreamPaymentRequest,
|
|
11
13
|
StreamPaymentResponse,
|
|
12
14
|
WordPaymentReceipt,
|
|
@@ -63,6 +65,28 @@ export type RubiconReadEvent =
|
|
|
63
65
|
payment?: WordPaymentReceipt;
|
|
64
66
|
text: string;
|
|
65
67
|
}
|
|
68
|
+
| {
|
|
69
|
+
type: "article.bundle";
|
|
70
|
+
bundleSequence: number;
|
|
71
|
+
words: Array<{ sequence: number; word: string; priceAtomic: `${bigint}`; payment?: WordPaymentReceipt }>;
|
|
72
|
+
text: string;
|
|
73
|
+
bundleText: string;
|
|
74
|
+
wordCount: number;
|
|
75
|
+
amountAtomic: `${bigint}`;
|
|
76
|
+
payment?: WordPaymentReceipt;
|
|
77
|
+
wordsRead: number;
|
|
78
|
+
amountPaidAtomic: `${bigint}`;
|
|
79
|
+
completed: boolean;
|
|
80
|
+
}
|
|
81
|
+
/** @deprecated Use article.bundle. */
|
|
82
|
+
| {
|
|
83
|
+
type: "article.chunk";
|
|
84
|
+
words: Array<{ sequence: number; word: string; priceAtomic: `${bigint}`; payment?: WordPaymentReceipt }>;
|
|
85
|
+
text: string;
|
|
86
|
+
wordsRead: number;
|
|
87
|
+
amountPaidAtomic: `${bigint}`;
|
|
88
|
+
completed: boolean;
|
|
89
|
+
}
|
|
66
90
|
| { type: "article.usage"; wordsPaid: number; wordsDelivered: number; paidAtomic: `${bigint}` }
|
|
67
91
|
| { type: "article.completed"; receipt: ReadReceipt }
|
|
68
92
|
| { type: "article.error"; message: string };
|
|
@@ -76,6 +100,10 @@ export interface ReadOptions {
|
|
|
76
100
|
maxSpendAtomic?: `${bigint}`;
|
|
77
101
|
budget?: Budget;
|
|
78
102
|
maxWords?: number;
|
|
103
|
+
/** Number of words to authorize and deliver per gateway round trip when supported. */
|
|
104
|
+
chunkWords?: number;
|
|
105
|
+
/** Default is bundled. Use word for legacy one-word events/payments. */
|
|
106
|
+
streamMode?: StreamMode;
|
|
79
107
|
/** Return true to stop reading once enough information has been collected. */
|
|
80
108
|
stopWhen?: (state: {
|
|
81
109
|
text: string;
|
|
@@ -92,8 +120,8 @@ export interface RunOptions extends ReadOptions {
|
|
|
92
120
|
|
|
93
121
|
/**
|
|
94
122
|
* High-level buyer-agent client for Rubicon. `read()` runs the entire
|
|
95
|
-
*
|
|
96
|
-
*
|
|
123
|
+
* authorize -> word -> usage loop until a stop condition is met, so application
|
|
124
|
+
* developers never drive a payment flow for every word themselves.
|
|
97
125
|
*/
|
|
98
126
|
export class RubiconClient {
|
|
99
127
|
private readonly fetcher: typeof fetch;
|
|
@@ -170,6 +198,18 @@ export class RubiconClient {
|
|
|
170
198
|
return response.json() as Promise<StreamPaymentResponse>;
|
|
171
199
|
}
|
|
172
200
|
|
|
201
|
+
async streamChunk(sessionId: string, payment: StreamPaymentRequest): Promise<StreamChunkResponse> {
|
|
202
|
+
const response = await this.fetcher(`${this.baseUrl}/v1/sessions/${sessionId}/stream`, {
|
|
203
|
+
method: "POST",
|
|
204
|
+
headers: this.headers({ "content-type": "application/json" }),
|
|
205
|
+
body: JSON.stringify(payment),
|
|
206
|
+
});
|
|
207
|
+
if (!response.ok) {
|
|
208
|
+
throw new Error(`Chunk stream rejected: ${response.status} ${await response.text()}`);
|
|
209
|
+
}
|
|
210
|
+
return response.json() as Promise<StreamChunkResponse>;
|
|
211
|
+
}
|
|
212
|
+
|
|
173
213
|
async abort(sessionId: string, reason = "agent_cancelled"): Promise<void> {
|
|
174
214
|
await this.fetcher(`${this.baseUrl}/v1/sessions/${sessionId}/abort`, {
|
|
175
215
|
method: "POST",
|
|
@@ -210,6 +250,14 @@ export class RubiconClient {
|
|
|
210
250
|
wordsRead: event.wordsRead,
|
|
211
251
|
amountPaidAtomic: event.amountPaidAtomic,
|
|
212
252
|
});
|
|
253
|
+
} else if (event.type === "article.bundle") {
|
|
254
|
+
for (const entry of event.words) {
|
|
255
|
+
await options.onWord?.(entry.word, {
|
|
256
|
+
text: event.text,
|
|
257
|
+
wordsRead: event.wordsRead,
|
|
258
|
+
amountPaidAtomic: event.amountPaidAtomic,
|
|
259
|
+
});
|
|
260
|
+
}
|
|
213
261
|
}
|
|
214
262
|
if (event.type === "article.completed") {
|
|
215
263
|
receipt = event.receipt;
|
|
@@ -222,8 +270,8 @@ export class RubiconClient {
|
|
|
222
270
|
}
|
|
223
271
|
|
|
224
272
|
/**
|
|
225
|
-
* Read an article
|
|
226
|
-
* running usage, and a final completion event carrying the receipt.
|
|
273
|
+
* Read an article with word-level metering. Yields seller messages, paid
|
|
274
|
+
* words, running usage, and a final completion event carrying the receipt.
|
|
227
275
|
*/
|
|
228
276
|
async *read(options: ReadOptions): AsyncGenerator<RubiconReadEvent, ReadReceipt> {
|
|
229
277
|
const budget: Budget =
|
|
@@ -274,6 +322,11 @@ export class RubiconClient {
|
|
|
274
322
|
const transactionHashes: string[] = [];
|
|
275
323
|
const settlementIds: string[] = [];
|
|
276
324
|
const payments: WordPaymentReceipt[] = [];
|
|
325
|
+
const streamMode = options.streamMode ?? "bundled";
|
|
326
|
+
const bundleWords = normalizeBundleWords(options.chunkWords);
|
|
327
|
+
const useBundles = streamMode === "bundled" && typeof this.paymentEngine.createChunkPayment === "function";
|
|
328
|
+
const selectedWordLimit = selectedRangeWordLimit(session, sectionId);
|
|
329
|
+
let bundleSequence = 0;
|
|
277
330
|
let stopReason: ReadReceipt["stopReason"] = "article_completed";
|
|
278
331
|
let completed = false;
|
|
279
332
|
|
|
@@ -308,6 +361,84 @@ export class RubiconClient {
|
|
|
308
361
|
break;
|
|
309
362
|
}
|
|
310
363
|
|
|
364
|
+
if (useBundles) {
|
|
365
|
+
const remainingRequestedWords = options.maxWords === undefined ? Number.MAX_SAFE_INTEGER : Math.max(0, options.maxWords - wordsRead);
|
|
366
|
+
const affordableWords = Number((budgetAtomic - amountPaid) / wordPaymentAtomic);
|
|
367
|
+
const remainingArticleWords =
|
|
368
|
+
selectedWordLimit === undefined ? Number.MAX_SAFE_INTEGER : Math.max(0, selectedWordLimit - wordsRead);
|
|
369
|
+
const maxWords = Math.min(bundleWords, remainingRequestedWords, affordableWords, remainingArticleWords);
|
|
370
|
+
if (maxWords < 1) {
|
|
371
|
+
stopReason = selectedWordLimit !== undefined && wordsRead >= selectedWordLimit ? "article_completed" : "budget_reached";
|
|
372
|
+
break;
|
|
373
|
+
}
|
|
374
|
+
const payment = await this.paymentEngine.createChunkPayment!(session, {
|
|
375
|
+
nextSequence: wordsRead,
|
|
376
|
+
maxWords,
|
|
377
|
+
});
|
|
378
|
+
const idempotencyKey = `${session.sessionId}:${wordsRead}:${maxWords}`;
|
|
379
|
+
let result: StreamChunkResponse;
|
|
380
|
+
try {
|
|
381
|
+
result = await this.streamChunk(session.sessionId, { ...payment, idempotencyKey, maxWords });
|
|
382
|
+
} catch (error) {
|
|
383
|
+
yield { type: "article.error", message: error instanceof Error ? error.message : String(error) };
|
|
384
|
+
stopReason = "aborted";
|
|
385
|
+
break;
|
|
386
|
+
}
|
|
387
|
+
if (result.words.length === 0 && result.completed) {
|
|
388
|
+
completed = true;
|
|
389
|
+
stopReason = "article_completed";
|
|
390
|
+
const receipt = makeReceipt();
|
|
391
|
+
yield { type: "article.completed", receipt };
|
|
392
|
+
return receipt;
|
|
393
|
+
}
|
|
394
|
+
if (result.payment) {
|
|
395
|
+
payments.push(result.payment);
|
|
396
|
+
} else {
|
|
397
|
+
for (const entry of result.words) {
|
|
398
|
+
if (entry.payment) {
|
|
399
|
+
payments.push(entry.payment);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
const previousAmountPaid = amountPaid;
|
|
404
|
+
const bundleText = result.text || result.words.map((entry) => entry.word).join(" ");
|
|
405
|
+
for (const entry of result.words) {
|
|
406
|
+
text = text ? `${text} ${entry.word}` : entry.word;
|
|
407
|
+
}
|
|
408
|
+
wordsRead = result.wordsDelivered;
|
|
409
|
+
amountPaid = BigInt(result.paidAtomic);
|
|
410
|
+
transactionHashes.push(...(result.transactionHashes ?? (result.transactionHash ? [result.transactionHash] : [])));
|
|
411
|
+
settlementIds.push(...(result.settlementIds ?? (result.settlementId ? [result.settlementId] : [])));
|
|
412
|
+
yield {
|
|
413
|
+
type: "article.bundle",
|
|
414
|
+
bundleSequence,
|
|
415
|
+
words: result.words,
|
|
416
|
+
text,
|
|
417
|
+
bundleText,
|
|
418
|
+
wordCount: result.words.length,
|
|
419
|
+
amountAtomic: result.payment?.amountAtomic ?? `${amountPaid - previousAmountPaid}`,
|
|
420
|
+
payment: result.payment,
|
|
421
|
+
wordsRead,
|
|
422
|
+
amountPaidAtomic: `${amountPaid}`,
|
|
423
|
+
completed: result.completed,
|
|
424
|
+
};
|
|
425
|
+
bundleSequence += 1;
|
|
426
|
+
yield {
|
|
427
|
+
type: "article.usage",
|
|
428
|
+
wordsPaid: result.wordsPaid,
|
|
429
|
+
wordsDelivered: result.wordsDelivered,
|
|
430
|
+
paidAtomic: result.paidAtomic,
|
|
431
|
+
};
|
|
432
|
+
if (result.completed) {
|
|
433
|
+
completed = true;
|
|
434
|
+
stopReason = "article_completed";
|
|
435
|
+
const receipt = makeReceipt();
|
|
436
|
+
yield { type: "article.completed", receipt };
|
|
437
|
+
return receipt;
|
|
438
|
+
}
|
|
439
|
+
continue;
|
|
440
|
+
}
|
|
441
|
+
|
|
311
442
|
const payment = await this.paymentEngine.createWordPayment(session);
|
|
312
443
|
// Idempotency key ties this payment to the specific next word; safe retries.
|
|
313
444
|
const idempotencyKey = `${session.sessionId}:${wordsRead}`;
|
|
@@ -391,3 +522,16 @@ export class RubiconClient {
|
|
|
391
522
|
|
|
392
523
|
/** Backwards-compatible alias. */
|
|
393
524
|
export const AgentClient = RubiconClient;
|
|
525
|
+
|
|
526
|
+
function normalizeBundleWords(chunkWords: number | undefined): number {
|
|
527
|
+
if (chunkWords === undefined) return 32;
|
|
528
|
+
if (!Number.isInteger(chunkWords) || chunkWords < 1) return 1;
|
|
529
|
+
return Math.min(chunkWords, 256);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function selectedRangeWordLimit(session: StartSessionResponse, sectionId: string | undefined): number | undefined {
|
|
533
|
+
if (!sectionId || sectionId === "full-article") {
|
|
534
|
+
return session.article.totalWords;
|
|
535
|
+
}
|
|
536
|
+
return session.navigation.sections.find((section) => section.sectionId === sectionId)?.wordCount;
|
|
537
|
+
}
|