@402flow/sdk 0.1.0-alpha.13 → 0.1.0-alpha.14

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # @402flow/sdk
2
2
 
3
- Node.js SDK for making paid requests through the 402flow control plane.
3
+ Node.js SDK for making governed paid HTTP requests through the 402flow control plane.
4
4
 
5
5
  ## Install
6
6
 
@@ -8,86 +8,285 @@ Node.js SDK for making paid requests through the 402flow control plane.
8
8
  npm install @402flow/sdk
9
9
  ```
10
10
 
11
- ## Usage
11
+ ## Overview
12
12
 
13
- ### Bootstrap Key
13
+ `@402flow/sdk` is built for AI agents and other callers that need to check, prepare, and execute paid HTTP requests without embedding control-plane or protocol-specific logic in the host.
14
+
15
+ The SDK has one core client, `AgentPayClient`, and three main calls:
16
+
17
+ | API | Use when | What it does |
18
+ | --- | --- | --- |
19
+ | `fetchPaid(...)` | You already know the request shape | Probes if needed, resolves payment through the control plane, and returns passthrough or success |
20
+ | `preparePaidRequest(...)` | You want to inspect before paying | Returns normalized payment terms, request hints, validation issues, and `nextAction` |
21
+ | `executePreparedRequest(...)` | You already prepared the request | Executes the exact prepared request without re-probing first |
22
+
23
+ Use `fetchPaid(...)` for the simplest direct path.
24
+ Use `preparePaidRequest(...)` plus `executePreparedRequest(...)` when the caller needs an explicit inspect, revise, then execute loop.
25
+
26
+ The package also includes `AgentHarness`, an optional preparedId-based wrapper for tool hosts. It is a convenience layer on top of `AgentPayClient`, not part of the core client API.
27
+
28
+ ## Create A Client
29
+
30
+ Create one `AgentPayClient` per agent identity. The client binds the organization and agent selectors up front, and each request only carries request-specific context.
31
+
32
+ ### Bootstrap key
33
+
34
+ For most SDK integrations, bootstrap-key auth is the recommended mode. The SDK exchanges it for a short-lived runtime token, caches that token, and refreshes it automatically before expiry.
14
35
 
15
36
  ```ts
16
37
  import { AgentPayClient } from '@402flow/sdk';
17
38
 
18
39
  const client = new AgentPayClient({
19
- controlPlaneBaseUrl: 'https://402flow.ai',
20
- organization: 'acme-labs',
21
- agent: 'reporting-worker',
22
- auth: {
23
- type: 'bootstrapKey',
24
- bootstrapKey: process.env.AGENT_PAY_BOOTSTRAP_KEY ?? '',
25
- },
40
+ controlPlaneBaseUrl: 'https://402flow.ai',
41
+ organization: 'acme-labs',
42
+ agent: 'reporting-worker',
43
+ auth: {
44
+ type: 'bootstrapKey',
45
+ bootstrapKey: process.env.X402FLOW_BOOTSTRAP_KEY ?? '',
46
+ },
26
47
  });
27
48
  ```
28
49
 
50
+ ### Runtime token
29
51
 
30
- Create one `AgentPayClient` per agent identity. The client binds the organization and agent selectors up front, and `fetchPaid()` only carries request-specific context.
31
- For most SDK integrations, `bootstrapKey` is the recommended auth mode. The SDK exchanges it for a short-lived runtime token, caches that token, and refreshes it automatically before expiry.
52
+ ```ts
53
+ import { AgentPayClient } from '@402flow/sdk';
54
+
55
+ const client = new AgentPayClient({
56
+ controlPlaneBaseUrl: 'https://402flow.ai',
57
+ organization: 'acme-labs',
58
+ agent: 'reporting-worker',
59
+ auth: {
60
+ type: 'runtimeToken',
61
+ runtimeToken: process.env.X402FLOW_RUNTIME_TOKEN ?? '',
62
+ },
63
+ });
64
+ ```
32
65
 
33
- ### fetchPaid()
66
+ ## Fast Path: `fetchPaid()`
34
67
 
35
- Call `fetchPaid()` with the merchant URL, the outgoing request, and request-specific control-plane context.
68
+ Call `fetchPaid()` when you already know the merchant URL, method, headers, and body.
36
69
 
37
70
  ```ts
38
71
  try {
39
- const result = await client.fetchPaid(
40
- 'https://merchant.example.com/reports/daily',
41
- {
42
- method: 'POST',
43
- headers: {
44
- 'content-type': 'application/json',
45
- },
46
- body: JSON.stringify({
47
- date: '2026-03-25',
48
- }),
49
- },
50
- {
51
- description: 'sync daily paid report',
52
- idempotencyKey: 'daily-report-2026-03-25',
53
- },
54
- );
55
- const paidContent = await result.response.json();
56
- console.log('paid content:', paidContent);
72
+ const result = await client.fetchPaid(
73
+ 'https://merchant.example.com/reports/daily',
74
+ {
75
+ method: 'POST',
76
+ headers: {
77
+ 'content-type': 'application/json',
78
+ },
79
+ body: JSON.stringify({
80
+ date: '2026-03-25',
81
+ }),
82
+ },
83
+ {
84
+ description: 'sync daily paid report',
85
+ idempotencyKey: 'daily-report-2026-03-25',
86
+ },
87
+ );
88
+
89
+ const paidContent = await result.response.json();
90
+ console.log('paid content:', paidContent);
57
91
  } catch (error) {
58
- console.error('paid request failed', error);
59
- throw error;
92
+ console.error('paid request failed', error);
93
+ throw error;
94
+ }
95
+ ```
96
+
97
+ If the merchant does not require payment for that exact request, the SDK returns a passthrough response. If the merchant returns a payable challenge, the SDK resolves payment through the control plane and returns a durable paid outcome.
98
+
99
+ ## Preparation Flow
100
+
101
+ Use `preparePaidRequest()` when the caller needs a first-class pre-execution result before paying.
102
+
103
+ ```ts
104
+ const prepared = await client.preparePaidRequest(
105
+ 'https://merchant.example.com/images/generate',
106
+ {
107
+ method: 'POST',
108
+ headers: {
109
+ 'content-type': 'application/json',
110
+ },
111
+ body: JSON.stringify({
112
+ prompt: 'foggy coastline',
113
+ }),
114
+ },
115
+ );
116
+
117
+ if (prepared.kind === 'passthrough') {
118
+ console.log('merchant did not require payment', prepared.probe?.responseStatus);
119
+ } else {
120
+ console.log('protocol:', prepared.protocol);
121
+ console.log('payment requirement:', prepared.paymentRequirement);
122
+ console.log('request hints:', prepared.hints);
60
123
  }
124
+
125
+ console.log('next action:', prepared.nextAction);
126
+ console.log('validation issues:', prepared.validationIssues);
61
127
  ```
62
128
 
63
- ### fetchPaid errors
129
+ This flow is useful when:
64
130
 
65
- `fetchPaid()` throws `FetchPaidError` for plocy denials and other failures:
131
+ 1. an agent needs request-shape hints before attempting execution
132
+ 2. the caller wants normalized payment terms before paying
133
+ 3. the caller wants to merge optional `discoveryMetadata` it already has from another system
66
134
 
67
- 1. `denied`: the control plane denied the paid request before execution becuase of a policy violation
68
- 2. `preflight_failed`: the request was incompatible with paid execution before payment started
69
- 3. `execution_pending`: a safe retry attached to an in-flight paid attempt that is still executing
70
- 4. `execution_failed`: payment failed, no receipt was produced, and no paid content was delivered
71
- 5. `paid_fulfillment_failed`: payment was accepted and a receipt exists, but the merchant did not deliver the paid content
72
- 6. `execution_inconclusive`: the system could not conclusively determine the payment outcome
135
+ The common loop is:
73
136
 
74
- ## Receipt Semantics
137
+ 1. prepare the request
138
+ 2. inspect `kind`, `paymentRequirement`, `hints`, `validationIssues`, and `nextAction`
139
+ 3. revise if needed
140
+ 4. execute only once the request is understood
75
141
 
76
- `receipt.status = 'confirmed'` means the control plane has chain-backed settlement attribution for the paid attempt. `receipt.status = 'provisional'` means the paid outcome was supportable by merchant provided evidence, but final settlement attribution is still pending on-chain reconciliation.
142
+ If your system already has endpoint metadata, you can pass it in as optional context:
77
143
 
78
- ## Notes
144
+ ```ts
145
+ const prepared = await client.preparePaidRequest(
146
+ 'https://merchant.example.com/images/generate',
147
+ {
148
+ method: 'POST',
149
+ headers: {
150
+ 'content-type': 'application/json',
151
+ },
152
+ body: JSON.stringify({
153
+ prompt: 'foggy coastline',
154
+ }),
155
+ },
156
+ {
157
+ discoveryMetadata: {
158
+ provider: {
159
+ requestBodyType: 'json',
160
+ requestBodyFields: [
161
+ {
162
+ name: 'prompt',
163
+ type: 'string',
164
+ required: true,
165
+ },
166
+ ],
167
+ },
168
+ },
169
+ },
170
+ );
171
+ ```
79
172
 
80
- 1. a `success` will always carry a receipt and the paid content
81
- 2. a `paid_fulfillment_failed` result will also carry a receipt when the merchant took payment but fulfillment failed
82
- 3. callers should treat provisional receipts as payment attempt evidence, not as proof of final settlement
83
- 4. later chain analysis in the control plane will advance a provisional receipt to confirmed, refunded, void, or expired
84
- 5. if you safely retry the same logical paid request with the same `idempotencyKey`, the SDK returns the same durable paid outcome and receipt instead of creating a second paid attempt
173
+ `discoveryMetadata` is optional caller context. It improves preparation when the caller already has structured endpoint knowledge, but it is not required for normal SDK use.
85
174
 
86
- ## Publish
175
+ ### What `ready` Means
176
+
177
+ `ready` means this exact request can proceed through governed paid execution as-is; it does not mean the SDK has inferred the best task parameters for you.
178
+
179
+ That distinction matters:
180
+
181
+ 1. `ready` is about protocol and payment executability
182
+ 2. `validationIssues` and `hints` are about request-shape guidance
183
+ 3. choosing semantically correct task parameters still belongs to the caller or agent
184
+
185
+ ### Execute A Prepared Request
186
+
187
+ If preparation returns `kind === 'ready'`, execute that exact prepared request with `executePreparedRequest(prepared, ...)`.
188
+
189
+ ```ts
190
+ const prepared = await client.preparePaidRequest(
191
+ 'https://merchant.example.com/images/generate',
192
+ {
193
+ method: 'POST',
194
+ headers: {
195
+ 'content-type': 'application/json',
196
+ },
197
+ body: JSON.stringify({
198
+ prompt: 'foggy coastline',
199
+ }),
200
+ },
201
+ );
87
202
 
203
+ if (prepared.kind === 'ready') {
204
+ const result = await client.executePreparedRequest(prepared, {
205
+ description: 'generate image',
206
+ idempotencyKey: 'image-generate-foggy-coastline',
207
+ });
208
+
209
+ console.log('paid response status:', result.response.status);
210
+ }
211
+ ```
212
+
213
+ If preparation does not return `kind === 'ready'`, that is not necessarily an error. It means this exact request did not currently resolve to a payable executable path. The caller can accept that result, run a normal non-paid path, or revise and prepare again.
214
+
215
+ ## Prepared Result Semantics
216
+
217
+ `preparePaidRequest()` separates request checking from paid execution.
218
+
219
+ The preparation result distinguishes four important things:
220
+
221
+ 1. `paymentRequirement`: normalized payment terms derived from the merchant challenge when available
222
+ 2. `hints`: request-shape hints such as body fields, query params, path params, descriptions, examples, and notes
223
+ 3. `validationIssues`: structured remediation diagnostics derived from the current request and defensible preparation inputs
224
+ 4. `nextAction`: a narrow action summary such as `execute`, `revise_request`, or `treat_as_passthrough`
225
+
226
+ Each prepared hint carries `attribution` so callers can distinguish live merchant-authoritative data from advisory caller-supplied metadata.
227
+
228
+ ## Result And Error Semantics
229
+
230
+ `fetchPaid()` and `executePreparedRequest()` either:
231
+
232
+ 1. return a passthrough response when the request did not require payment
233
+ 2. return `success` with a receipt when the paid request completed successfully
234
+ 3. throw `FetchPaidError` for all non-success paid outcomes
235
+
236
+ `FetchPaidError` kinds are:
237
+
238
+ 1. `denied`
239
+ 2. `preflight_failed`
240
+ 3. `execution_pending`
241
+ 4. `execution_failed`
242
+ 5. `paid_fulfillment_failed`
243
+ 6. `execution_inconclusive`
244
+ 7. `request_failed`
245
+
246
+ Receipt notes:
247
+
248
+ 1. `receipt.status = 'confirmed'` means the control plane has chain-backed settlement attribution for the paid attempt
249
+ 2. `receipt.status = 'provisional'` means the paid outcome was supportable by merchant-provided evidence, but final settlement attribution is still pending reconciliation
250
+ 3. callers should treat provisional receipts as payment-attempt evidence, not as proof of final settlement
251
+ 4. if you safely retry the same logical paid request with the same `idempotencyKey`, the SDK returns the same durable paid outcome instead of creating a second paid attempt
252
+
253
+ ## Receipt Lookup
254
+
255
+ ```ts
256
+ const receipt = await client.lookupReceipt('receipt-id');
257
+
258
+ console.log(receipt.receipt.status);
88
259
  ```
260
+
261
+ ## Minimal Agent Integration Contract
262
+
263
+ Most agent frameworks only need a small orchestration policy:
264
+
265
+ ```text
266
+ When using @402flow/sdk:
267
+ - Always prepare a paid request before executing it.
268
+ - If preparation returns revise_request, inspect validationIssues and hints, revise the request, and prepare again.
269
+ - Do not invent discoveryMetadata unless the caller already has it from another system.
270
+ - Use merchant-challenge hints to understand request shape, but use the task and available business context to choose actual parameter values.
271
+ - Execute only after preparation shows the request is ready.
272
+ ```
273
+
274
+ That is the portable core SDK story. It should work across OpenAI, Claude, LangGraph, MCP, or custom workflows without requiring host-specific packaging in the SDK contract itself.
275
+
276
+ ## Optional `AgentHarness`
277
+
278
+ `AgentHarness` is an optional preparedId-based wrapper for tool hosts that do not want to manage in-flight prepared request objects themselves. It is a convenience layer on top of `AgentPayClient`, not a required abstraction.
279
+
280
+ For harness usage, presets, transcripts, and scenario packs, see:
281
+
282
+ 1. [docs/evaluation-harness.md](docs/evaluation-harness.md)
283
+ 2. [docs/harness-scenarios.md](docs/harness-scenarios.md)
284
+
285
+ ## Publish
286
+
287
+ ```bash
89
288
  npm install
90
289
  npm run check
91
290
  npm run pack:check
92
291
  npm publish --access public
93
- ```
292
+ ```
@@ -0,0 +1,161 @@
1
+ /**
2
+ * AgentHarness provides a small in-memory orchestration layer on top of
3
+ * AgentPayClient for hosts that prefer preparedId-based handoffs over holding
4
+ * full prepared request objects.
5
+ *
6
+ * The intended host-facing flow is:
7
+ *
8
+ * 1. prepare a candidate request and receive a preparedId plus the preparation summary
9
+ * 2. execute later by preparedId once nextAction is execute
10
+ * 3. read back the stored execution result by preparedId
11
+ *
12
+ * This is useful for tool-driven hosts such as OpenAI tools, MCP servers,
13
+ * Claude tools, LangGraph nodes, or custom orchestrators where passing a small
14
+ * opaque id between turns is easier than preserving the full prepared object.
15
+ *
16
+ * This file implements only a convenience layer. The core SDK contract remains
17
+ * AgentPayClient with preparePaidRequest() and executePreparedRequest().
18
+ *
19
+ * Important behavior:
20
+ * - State is kept only in memory inside this process.
21
+ * - Prepared requests expire after a TTL.
22
+ * - A newer active preparation for the same method + origin + pathname supersedes
23
+ * the older one.
24
+ * - Execution is rejected locally unless the stored preparation is still active,
25
+ * kind === 'ready', and nextAction === 'execute'.
26
+ */
27
+ import type { PaidRequestChallenge, SdkPreparationDiscoveryMetadata, SdkMerchantResponse, SdkPreparedNextAction, SdkPreparedPaidRequest, SdkPreparedPaymentRequirement, SdkPreparedRequestHints, SdkPreparedValidationIssue } from './contracts.js';
28
+ import type { AgentPayClient, ExecutePreparedRequest, FetchPaidFailureResponse, PaidResponse } from './index.js';
29
+ export type AgentHarnessPreparedState = 'active' | 'consumed' | 'expired' | 'superseded';
30
+ /** Local rejection reasons produced by the harness before the SDK is called. */
31
+ export type AgentHarnessRejectionCode = 'missing_prepared_id' | 'unknown_prepared_id' | 'expired_prepared_id' | 'prepared_request_superseded' | 'prepared_request_consumed' | 'prepared_request_not_ready' | 'prepared_request_not_executable';
32
+ /** Typed error used internally to convert local state failures into stable results. */
33
+ export declare class AgentHarnessError extends Error {
34
+ readonly code: AgentHarnessRejectionCode;
35
+ readonly preparedId: string | undefined;
36
+ constructor(code: AgentHarnessRejectionCode, message: string, preparedId?: string);
37
+ }
38
+ /** Host-facing input for the harness prepare step. */
39
+ export type AgentHarnessPrepareInput = {
40
+ url: string;
41
+ method?: string;
42
+ headers?: Record<string, string>;
43
+ body?: string;
44
+ discoveryMetadata?: SdkPreparationDiscoveryMetadata;
45
+ };
46
+ /** Host-facing input for the harness execute step. */
47
+ export type AgentHarnessExecuteInput = {
48
+ preparedId: string;
49
+ executionContext?: ExecutePreparedRequest;
50
+ };
51
+ /** Exact immutable execution payload stored behind a preparedId. */
52
+ export type AgentHarnessExecutionBinding = {
53
+ method: string;
54
+ url: string;
55
+ headers: Record<string, string>;
56
+ body?: string;
57
+ bodyHash?: string;
58
+ challenge?: {
59
+ protocol: PaidRequestChallenge['protocol'];
60
+ headers: Record<string, string>;
61
+ body?: unknown;
62
+ };
63
+ merchantOrigin: string;
64
+ };
65
+ /** Summary returned to the host after preparation succeeds. */
66
+ export type AgentHarnessPreparedSummary = {
67
+ preparedId: string;
68
+ state: 'active';
69
+ kind: SdkPreparedPaidRequest['kind'];
70
+ protocol: SdkPreparedPaidRequest['protocol'];
71
+ paymentRequirement?: SdkPreparedPaymentRequirement;
72
+ hints: SdkPreparedRequestHints;
73
+ probe?: SdkPreparedPaidRequest['probe'];
74
+ validationIssues: SdkPreparedValidationIssue[];
75
+ nextAction: SdkPreparedNextAction;
76
+ expiresAt: string;
77
+ };
78
+ /** Stored summary for an SDK-backed paid execution outcome. */
79
+ export type AgentHarnessExecutedResult = {
80
+ preparedId: string;
81
+ harnessDisposition: 'executed';
82
+ sdkOutcomeKind: PaidResponse['kind'] | FetchPaidFailureResponse['kind'];
83
+ status: number;
84
+ merchantResponse: SdkMerchantResponse;
85
+ receiptId?: string;
86
+ paidRequestId?: string;
87
+ paymentAttemptId?: string;
88
+ reason?: string;
89
+ policyReviewEventId?: string;
90
+ };
91
+ /** Stored summary for a harness-local rejection outcome. */
92
+ export type AgentHarnessRejectedResult = {
93
+ preparedId: string;
94
+ harnessDisposition: 'rejected';
95
+ rejectionCode: AgentHarnessRejectionCode;
96
+ message: string;
97
+ };
98
+ export type AgentHarnessExecutionResult = AgentHarnessExecutedResult | AgentHarnessRejectedResult;
99
+ /** Lookup shape returned when a host asks for the durable result of a preparedId. */
100
+ export type AgentHarnessExecutionLookup = {
101
+ preparedId: string;
102
+ state: AgentHarnessPreparedState;
103
+ supersededByPreparedId?: string;
104
+ executionResult?: AgentHarnessExecutionResult;
105
+ };
106
+ /** Full internal record shape exposed for debugging and test inspection. */
107
+ export type AgentHarnessPreparedRecord = {
108
+ preparedId: string;
109
+ state: AgentHarnessPreparedState;
110
+ createdAt: string;
111
+ expiresAt: string;
112
+ supersededByPreparedId?: string;
113
+ prepared: SdkPreparedPaidRequest;
114
+ executionBinding: AgentHarnessExecutionBinding;
115
+ executionResult?: AgentHarnessExecutionResult;
116
+ };
117
+ /** Minimal client surface AgentHarness needs from the core SDK. */
118
+ export type AgentHarnessClient = Pick<AgentPayClient, 'preparePaidRequest' | 'executePreparedRequest'>;
119
+ /** Configuration for the in-memory wrapper, including TTL and id generation hooks. */
120
+ export type AgentHarnessOptions = {
121
+ client: AgentHarnessClient;
122
+ preparedTtlMs?: number;
123
+ now?: () => Date;
124
+ createPreparedId?: () => string;
125
+ };
126
+ /**
127
+ * Optional in-memory preparedId wrapper over AgentPayClient.
128
+ */
129
+ export declare class AgentHarness {
130
+ private readonly client;
131
+ private readonly preparedTtlMs;
132
+ private readonly now;
133
+ private readonly createPreparedId;
134
+ private readonly preparedRecords;
135
+ constructor(options: AgentHarnessOptions);
136
+ /**
137
+ * Prepare a candidate request through the core SDK and store the immutable
138
+ * prepared result behind a generated preparedId for later execution.
139
+ */
140
+ preparePaidRequest(input: AgentHarnessPrepareInput): Promise<AgentHarnessPreparedSummary>;
141
+ /**
142
+ * Execute a previously stored ready preparation. Local state failures are
143
+ * converted into deterministic rejected results rather than thrown to the host.
144
+ */
145
+ executePreparedRequest(input: AgentHarnessExecuteInput): Promise<AgentHarnessExecutionResult>;
146
+ /**
147
+ * Return the durable stored outcome for a preparedId without re-running any
148
+ * merchant or control-plane call.
149
+ */
150
+ getExecutionResult(preparedId: string): AgentHarnessExecutionLookup;
151
+ /**
152
+ * Return the full stored record for debugging, tests, or host inspection.
153
+ */
154
+ getPreparedRecord(preparedId: string): AgentHarnessPreparedRecord;
155
+ private runExecution;
156
+ private handleExecutionRejection;
157
+ private getRecordForExecution;
158
+ private getKnownRecord;
159
+ private refreshRecordState;
160
+ private supersedeActiveRecords;
161
+ }