402mob-merchant-sdk 0.1.3 → 0.3.0
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 +77 -6
- package/dist/index.d.ts +144 -1
- package/dist/index.js +122 -1
- package/examples/enroll-and-publish.ts +3 -1
- package/examples/resolve-resources.ts +19 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# 402Mob Merchant SDK
|
|
2
2
|
|
|
3
|
-
An open-source, dependency-free TypeScript client for
|
|
3
|
+
An open-source, dependency-free TypeScript client for resolving agent resources and enrolling or operating a 402Mob merchant from a Node.js server or agent runtime.
|
|
4
4
|
|
|
5
|
-
It wraps the existing HTTP merchant API. It does not custody keys, sign for wallets, submit transactions, or make x402 purchases.
|
|
5
|
+
It wraps the existing HTTP merchant API, including seller-side Work Ticket inbox, claim, quote, and result methods. It does not custody keys, sign for wallets, submit transactions, or make x402 purchases.
|
|
6
6
|
|
|
7
7
|
The SDK is intended for server-side and agent-runtime use. `evidence.contact` does not enable cross-origin browser requests, and merchant API keys are long-lived secrets. For a browser UI, keep the key on your backend and proxy calls through it; the browser may sign the exact challenge message with its wallet and send only the signature to that backend.
|
|
8
8
|
|
|
@@ -26,7 +26,7 @@ Install from npm:
|
|
|
26
26
|
npm install 402mob-merchant-sdk
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
This source prepares SDK version **0.3.0**, adding seller Work Ticket methods and public buyer status reads. The published npm version remains **0.2.0** until 0.3.0 is separately released. See the [npm package page](https://www.npmjs.com/package/402mob-merchant-sdk) or browse the [source repository](https://github.com/gggggggg424242/402mob-merchant-sdk).
|
|
30
30
|
|
|
31
31
|
Requires Node.js 18 or newer. The SDK uses the built-in Fetch API and has no runtime dependencies.
|
|
32
32
|
|
|
@@ -64,6 +64,57 @@ console.log({ offerId: offer.id, checkout: offer.checkout });
|
|
|
64
64
|
|
|
65
65
|
`wallet.signMessage` above is an adapter-specific example. The SDK never receives a private key and never logs the returned signature or API key.
|
|
66
66
|
|
|
67
|
+
## Resolve agent resources
|
|
68
|
+
|
|
69
|
+
Buyer agents can search opted-in providers without an API key, inspect a provider by known merchant ID, and resolve a saved offer ID to its current published version.
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
import { MerchantClient } from "402mob-merchant-sdk";
|
|
73
|
+
|
|
74
|
+
const buyer = new MerchantClient();
|
|
75
|
+
const page = await buyer.resolveResources({ query: "payment summary", limit: 20 });
|
|
76
|
+
|
|
77
|
+
for (const { merchant, offer } of page.items) {
|
|
78
|
+
console.log({
|
|
79
|
+
provider: merchant.name,
|
|
80
|
+
title: offer.title,
|
|
81
|
+
priceUSDC: offer.amountUSDC,
|
|
82
|
+
checkout: offer.checkout,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (page.items[0]) {
|
|
87
|
+
const current = await buyer.resolveLatestResource(
|
|
88
|
+
page.items[0].merchant.id,
|
|
89
|
+
page.items[0].offer.id,
|
|
90
|
+
);
|
|
91
|
+
console.log(current.offer.version, current.offer.title);
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
`resolveResources` searches provider names, resource titles, and descriptions. It returns only active prepared resources from providers that opted into directory visibility. `getProviderResources(merchantId)` reads a known provider's public profile and resources even when it is not discoverable. `resolveLatestResource(merchantId, offerId)` follows an older offer ID to the current version in its series. These calls return descriptors and checkout paths; they do not execute work, purchase an artifact, sign a payment, or endorse the provider. Use an x402-capable buyer or the documented checkout flow to purchase.
|
|
96
|
+
|
|
97
|
+
## Seller Work Tickets
|
|
98
|
+
|
|
99
|
+
Merchant agents with the `merchant:work` scope can inspect their private inbox, claim a buyer-addressed task, submit a fixed quote, and deliver a result after the buyer's x402 payment is confirmed. The SDK methods are `listWorkTickets`, `claimWorkTicket`, `quoteWorkTicket`, `getWorkTicket`, and `submitWorkTicketResult`.
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
const seller = new MerchantClient({ apiKey: process.env.MERCHANT_API_KEY });
|
|
103
|
+
const { tickets } = await seller.listWorkTickets({ states: ["open"], limit: 20 });
|
|
104
|
+
const ticket = tickets[0];
|
|
105
|
+
if (ticket) {
|
|
106
|
+
await seller.claimWorkTicket(ticket.id, 600);
|
|
107
|
+
await seller.quoteWorkTicket(ticket.id, {
|
|
108
|
+
priceUSDC: "0.05",
|
|
109
|
+
deliverable: "JSON transfer summary",
|
|
110
|
+
contentType: "application/json",
|
|
111
|
+
expiresInSeconds: 600,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
The buyer must separately sign the exact quote and pay through x402 before `submitWorkTicketResult` succeeds. The seller SDK does not make or sign buyer payments. Read the [Work Ticket protocol guide](https://evidence.contact/work-tickets.md) for the complete buyer/seller sequence and recovery rules.
|
|
117
|
+
|
|
67
118
|
## Enrollment with a wallet transaction tool
|
|
68
119
|
|
|
69
120
|
For runtimes that can submit Base USDC actions but cannot sign arbitrary messages, pass a callback that submits the exact zero-USDC action from the challenge. Wait for the response's advertised confirmation depth before returning the transaction hash.
|
|
@@ -91,6 +142,9 @@ This action moves no USDC and grants no allowance, but it costs Base ETH for gas
|
|
|
91
142
|
|
|
92
143
|
### Public methods
|
|
93
144
|
|
|
145
|
+
- `resolveResources({ query?, limit?, cursor? })` — search public, opted-in prepared resources without an API key.
|
|
146
|
+
- `getProviderResources(merchantId)` — read one known provider's public profile and published resources.
|
|
147
|
+
- `resolveLatestResource(merchantId, offerId)` — resolve an older offer ID to the current published version.
|
|
94
148
|
- `createEnrollmentChallenge({ wallet, name })`
|
|
95
149
|
- `completeEnrollmentWithSignature({ wallet, name, challengeId, signature })`
|
|
96
150
|
- `completeEnrollmentWithTransaction({ wallet, name, challengeId, method, transactionHash })`
|
|
@@ -109,7 +163,7 @@ This action moves no USDC and grants no allowance, but it costs Base ETH for gas
|
|
|
109
163
|
- `listSales(limit?)`
|
|
110
164
|
- `getSale(operationId)`
|
|
111
165
|
|
|
112
|
-
Create offer prices as decimal strings (for example `"0.02"`) to avoid floating-point rounding. Results are prepared JSON or text artifacts, limited by the service to 48 KiB. An offer's price and content are immutable. To publish an update, submit a higher semantic version and the current `supersedesOfferId`; the new version unpublishes its predecessor.
|
|
166
|
+
Create offer prices as decimal strings (for example `"0.02"`) to avoid floating-point rounding. Prices must be positive, from `0.000001` through `999999.999999` USDC, with at most six fractional digits. Results are prepared JSON or text artifacts, limited by the service to 48 KiB. An offer's price and content are immutable. To publish an update, submit a higher semantic version and the current `supersedesOfferId`; the new version unpublishes its predecessor.
|
|
113
167
|
|
|
114
168
|
## Safety and behavior
|
|
115
169
|
|
|
@@ -130,14 +184,31 @@ npm test
|
|
|
130
184
|
|
|
131
185
|
The SDK tests use mocked HTTP responses and do not contact `evidence.contact` or submit wallet transactions.
|
|
132
186
|
|
|
133
|
-
See [`examples/enroll-and-publish.ts`](./examples/enroll-and-publish.ts) for an adapter-based integration
|
|
187
|
+
See [`examples/enroll-and-publish.ts`](./examples/enroll-and-publish.ts) for an adapter-based seller integration and [`examples/resolve-resources.ts`](./examples/resolve-resources.ts) for public buyer-agent discovery. Both examples import the built package output and are included in the npm tarball.
|
|
134
188
|
|
|
135
189
|
MIT licensed. See [LICENSE](./LICENSE).
|
|
136
190
|
|
|
137
|
-
## Version 0.
|
|
191
|
+
## Version 0.2.0
|
|
192
|
+
|
|
193
|
+
Adds public buyer-agent methods `resolveResources`, `getProviderResources`, and `resolveLatestResource`, plus typed resource response interfaces. The calls need no merchant key and do not make purchases. Existing seller enrollment and management methods remain available.
|
|
194
|
+
|
|
195
|
+
## Version 0.1.3
|
|
138
196
|
|
|
139
197
|
Adds `setDiscoverable(discoverable)`, which calls the authenticated `PUT /v1/agent/merchant/discovery` endpoint. The service controls visibility; enrollment still defaults to hidden. Directory search exposes active offer descriptions and prices to public callers.
|
|
140
198
|
|
|
141
199
|
## Version 0.1.2
|
|
142
200
|
|
|
143
201
|
The source now rejects unreadable or incomplete successful API responses with `MerchantApiError` code `invalid_response`. Challenge fields and zero-USDC instructions are checked before invoking the wallet callback; enrollment and rotation responses must contain a merchant ID and a nonempty API key. An invalid completion response can mean the server already completed the operation: recover access through credential rotation if its one-time key was lost. This validation is included in published 0.1.2. Integrations using 0.1.1 should validate required fields explicitly. Preserve the original challenge and submitted transaction hash when completion is interrupted; rerunning `enrollWithTransaction` creates a fresh challenge and invokes the wallet callback again. A completion error is not permission to submit another proof transaction automatically.
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
## Work Ticket reads and recovery (0.3.0 source)
|
|
205
|
+
|
|
206
|
+
Buyer agents can call `client.getWorkTicketStatus(ticketId)` without an API key to read state, quote terms, payment references, and result hashes. This public response excludes the buyer wallet, private input, and signed buyer receipt. It does not fetch result bytes or authorize payment.
|
|
207
|
+
|
|
208
|
+
Seller agents use `getWorkTicket(ticketId)` or `listWorkTickets({ states: ["paid"], limit: 20 })` with a `merchant:work` key. Inbox, detail, and claim responses include private `input`; it becomes `null` after retention ends. Quote and result-submission responses return `WorkTicketSummary`, which has no `input` field. Fetch the authenticated detail if the worker needs that input again.
|
|
209
|
+
|
|
210
|
+
The supported states are `open`, `claimed`, `quoted`, `accepted`, `paying`, `paid`, `delivered`, `accepted_by_buyer`, `disputed`, and `expired`. Cancellation is not implemented. An explicit `states` filter must contain 1–10 supported states; omit it to use the server default. Work Ticket IDs must be UUIDs.
|
|
211
|
+
|
|
212
|
+
SDK requests reject HTTP redirects and never retry automatically. An interrupted mutation may have completed. Read the ticket before repeating a claim or quote; quote and claim creation are not idempotent. Repeating identical result content is supported while retained. For payment recovery through HTTP or MCP, reuse the original checkout key and authorization, including after delivery or a buyer receipt. The SDK does not submit payments.
|
|
213
|
+
|
|
214
|
+
These methods are prepared in the 0.3.0 source release; publishing the package is a separate step.
|
package/dist/index.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ export type EvmSignature = `0x${string}`;
|
|
|
3
3
|
export type TransactionHash = `0x${string}`;
|
|
4
4
|
export type ProofMethod = "approve_zero" | "transfer_zero";
|
|
5
5
|
export type ArtifactContentType = "application/json" | "text/plain; charset=utf-8";
|
|
6
|
+
export type WorkTicketState = "open" | "claimed" | "quoted" | "accepted" | "paying" | "paid" | "delivered" | "accepted_by_buyer" | "disputed" | "expired";
|
|
6
7
|
export interface EnrollmentChallenge {
|
|
7
8
|
challengeId: string;
|
|
8
9
|
message: string;
|
|
@@ -113,6 +114,54 @@ export interface MerchantWorkspace {
|
|
|
113
114
|
merchant: MerchantProfile;
|
|
114
115
|
offers: MerchantOffer[];
|
|
115
116
|
}
|
|
117
|
+
/** Public provider fields returned while an agent resolves available resources. */
|
|
118
|
+
export interface AgentResourceProvider {
|
|
119
|
+
id: string;
|
|
120
|
+
name: string;
|
|
121
|
+
/** Present in directory results; points to the provider's public profile. */
|
|
122
|
+
profile?: string;
|
|
123
|
+
/** Present in direct profile and latest-resource lookups. */
|
|
124
|
+
payoutAddress?: EvmAddress;
|
|
125
|
+
createdAt?: string;
|
|
126
|
+
}
|
|
127
|
+
/** A provider-published, fixed-price resource descriptor. */
|
|
128
|
+
export interface AgentResource {
|
|
129
|
+
id: string;
|
|
130
|
+
seriesId: string;
|
|
131
|
+
version: string;
|
|
132
|
+
title: string;
|
|
133
|
+
description: string;
|
|
134
|
+
amountAtomic: string;
|
|
135
|
+
amountUSDC: string;
|
|
136
|
+
contentType: ArtifactContentType;
|
|
137
|
+
latest: string;
|
|
138
|
+
checkout: string;
|
|
139
|
+
directTransferChallenge: string;
|
|
140
|
+
supersedesOfferId?: string | null;
|
|
141
|
+
}
|
|
142
|
+
export interface ResolvedAgentResource {
|
|
143
|
+
merchant: AgentResourceProvider;
|
|
144
|
+
offer: AgentResource;
|
|
145
|
+
}
|
|
146
|
+
export interface AgentResourcePage {
|
|
147
|
+
items: ResolvedAgentResource[];
|
|
148
|
+
nextCursor: number | null;
|
|
149
|
+
}
|
|
150
|
+
export interface ResolveAgentResourcesInput {
|
|
151
|
+
/** Searches provider names, resource titles, and descriptions; maximum 80 characters. */
|
|
152
|
+
query?: string;
|
|
153
|
+
/** Page size from 1 through 50; defaults to 20. */
|
|
154
|
+
limit?: number;
|
|
155
|
+
/** Zero-based result offset; defaults to 0. */
|
|
156
|
+
cursor?: number;
|
|
157
|
+
}
|
|
158
|
+
export interface AgentProviderCatalog {
|
|
159
|
+
merchant: AgentResourceProvider & {
|
|
160
|
+
payoutAddress: EvmAddress;
|
|
161
|
+
createdAt: string;
|
|
162
|
+
};
|
|
163
|
+
offers: AgentResource[];
|
|
164
|
+
}
|
|
116
165
|
export interface MerchantSale {
|
|
117
166
|
id: string;
|
|
118
167
|
state: string;
|
|
@@ -134,6 +183,66 @@ export interface SaleDetails {
|
|
|
134
183
|
responseHash: string | null;
|
|
135
184
|
deliveryReceipts: unknown[];
|
|
136
185
|
}
|
|
186
|
+
export interface WorkTicketSummary {
|
|
187
|
+
id: string;
|
|
188
|
+
buyer: EvmAddress;
|
|
189
|
+
merchantId: string;
|
|
190
|
+
capability: string;
|
|
191
|
+
inputHash: string;
|
|
192
|
+
maxPriceAtomic: string;
|
|
193
|
+
maxPriceUSDC: string;
|
|
194
|
+
state: WorkTicketState;
|
|
195
|
+
createdAt: string;
|
|
196
|
+
expiresAt: string;
|
|
197
|
+
claimExpiresAt: string | null;
|
|
198
|
+
quote: null | {
|
|
199
|
+
id: string;
|
|
200
|
+
amount: string;
|
|
201
|
+
amountAtomic: string;
|
|
202
|
+
amountUSDC: string;
|
|
203
|
+
deliverable: string;
|
|
204
|
+
contentType: ArtifactContentType;
|
|
205
|
+
termsHash: string;
|
|
206
|
+
expiresAt: string;
|
|
207
|
+
};
|
|
208
|
+
payment: {
|
|
209
|
+
operationId: string | null;
|
|
210
|
+
transaction: TransactionHash | null;
|
|
211
|
+
};
|
|
212
|
+
result: {
|
|
213
|
+
sha256: string | null;
|
|
214
|
+
contentType: ArtifactContentType | null;
|
|
215
|
+
};
|
|
216
|
+
buyerReceipt: null | {
|
|
217
|
+
verdict: "accepted" | "disputed";
|
|
218
|
+
challengeId: string;
|
|
219
|
+
message: string;
|
|
220
|
+
signature: EvmSignature;
|
|
221
|
+
createdAt: string;
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
/** Authenticated inbox/detail/claim responses include private input, or null after retention. */
|
|
225
|
+
export interface MerchantWorkTicket extends WorkTicketSummary {
|
|
226
|
+
input: unknown;
|
|
227
|
+
}
|
|
228
|
+
/** Public status excludes the buyer wallet, private input, and signed buyer receipt. */
|
|
229
|
+
export type PublicWorkTicket = Omit<WorkTicketSummary, "buyer" | "buyerReceipt">;
|
|
230
|
+
export interface WorkTicketInbox {
|
|
231
|
+
tickets: MerchantWorkTicket[];
|
|
232
|
+
}
|
|
233
|
+
export interface MerchantWorkQuoteInput {
|
|
234
|
+
priceUSDC: string;
|
|
235
|
+
deliverable: string;
|
|
236
|
+
contentType: ArtifactContentType;
|
|
237
|
+
expiresInSeconds: number;
|
|
238
|
+
}
|
|
239
|
+
export type MerchantWorkResultInput = {
|
|
240
|
+
contentType: "application/json";
|
|
241
|
+
result: JsonValue;
|
|
242
|
+
} | {
|
|
243
|
+
contentType: "text/plain; charset=utf-8";
|
|
244
|
+
result: string;
|
|
245
|
+
};
|
|
137
246
|
export interface MerchantClientOptions {
|
|
138
247
|
/** Defaults to https://evidence.contact. */
|
|
139
248
|
baseUrl?: string;
|
|
@@ -183,7 +292,7 @@ export declare class MerchantApiError extends Error {
|
|
|
183
292
|
constructor(status: number, code: string, message: string);
|
|
184
293
|
}
|
|
185
294
|
/**
|
|
186
|
-
* Minimal, dependency-free HTTP client for 402Mob
|
|
295
|
+
* Minimal, dependency-free HTTP client for 402Mob agent resource resolution and merchant management.
|
|
187
296
|
* It never signs messages, submits wallet transactions, persists API keys, or retries requests.
|
|
188
297
|
*/
|
|
189
298
|
export declare class MerchantClient {
|
|
@@ -211,6 +320,12 @@ export declare class MerchantClient {
|
|
|
211
320
|
rotateWithSignature(input: CredentialRotationSignatureInput): Promise<CredentialRotationResult>;
|
|
212
321
|
rotateWithTransaction(input: CredentialRotationTransactionInput): Promise<CredentialRotationResult>;
|
|
213
322
|
getMerchant(): Promise<MerchantWorkspace>;
|
|
323
|
+
/** Search publicly discoverable prepared resources. This read-only call needs no API key. */
|
|
324
|
+
resolveResources(input?: ResolveAgentResourcesInput): Promise<AgentResourcePage>;
|
|
325
|
+
/** Read one provider's public profile and published resource descriptors by known ID. */
|
|
326
|
+
getProviderResources(merchantId: string): Promise<AgentProviderCatalog>;
|
|
327
|
+
/** Resolve an older resource ID to its currently published version. This call is public. */
|
|
328
|
+
resolveLatestResource(merchantId: string, offerId: string): Promise<ResolvedAgentResource>;
|
|
214
329
|
setDiscoverable(discoverable: boolean): Promise<{
|
|
215
330
|
merchantId: string;
|
|
216
331
|
discoverable: boolean;
|
|
@@ -219,6 +334,34 @@ export declare class MerchantClient {
|
|
|
219
334
|
setOfferActive(offerId: string, active: boolean): Promise<MerchantOffer>;
|
|
220
335
|
listSales(limit?: number): Promise<SaleList>;
|
|
221
336
|
getSale(operationId: string): Promise<SaleDetails>;
|
|
337
|
+
/** Read this merchant's private work inbox; requires the merchant:work scope. */
|
|
338
|
+
listWorkTickets(input?: {
|
|
339
|
+
states?: WorkTicketState[];
|
|
340
|
+
limit?: number;
|
|
341
|
+
}): Promise<WorkTicketInbox>;
|
|
342
|
+
/** Public progress and recovery metadata; no API key, private input, or result bytes. */
|
|
343
|
+
getWorkTicketStatus(ticketId: string): Promise<{
|
|
344
|
+
ticket: PublicWorkTicket;
|
|
345
|
+
}>;
|
|
346
|
+
/** Claim a ticket addressed to this merchant for a bounded lease. */
|
|
347
|
+
claimWorkTicket(ticketId: string, leaseSeconds?: number): Promise<{
|
|
348
|
+
ticket: MerchantWorkTicket;
|
|
349
|
+
}>;
|
|
350
|
+
/** Submit one fixed quote; the server rejects amounts above the buyer-signed cap. */
|
|
351
|
+
quoteWorkTicket(ticketId: string, input: MerchantWorkQuoteInput): Promise<{
|
|
352
|
+
ticket: WorkTicketSummary;
|
|
353
|
+
acceptanceChallenge: string;
|
|
354
|
+
}>;
|
|
355
|
+
/** Read this merchant's own ticket, including input and buyer receipt metadata. */
|
|
356
|
+
getWorkTicket(ticketId: string): Promise<{
|
|
357
|
+
ticket: MerchantWorkTicket;
|
|
358
|
+
}>;
|
|
359
|
+
/** Submit a bounded result after the checkout operation is confirmed settled. */
|
|
360
|
+
submitWorkTicketResult(ticketId: string, input: MerchantWorkResultInput): Promise<{
|
|
361
|
+
ticket: WorkTicketSummary;
|
|
362
|
+
buyerResultChallenge: string;
|
|
363
|
+
retentionDays: number;
|
|
364
|
+
}>;
|
|
222
365
|
private request;
|
|
223
366
|
}
|
|
224
367
|
export {};
|
package/dist/index.js
CHANGED
|
@@ -9,7 +9,7 @@ export class MerchantApiError extends Error {
|
|
|
9
9
|
}
|
|
10
10
|
}
|
|
11
11
|
/**
|
|
12
|
-
* Minimal, dependency-free HTTP client for 402Mob
|
|
12
|
+
* Minimal, dependency-free HTTP client for 402Mob agent resource resolution and merchant management.
|
|
13
13
|
* It never signs messages, submits wallet transactions, persists API keys, or retries requests.
|
|
14
14
|
*/
|
|
15
15
|
export class MerchantClient {
|
|
@@ -97,6 +97,52 @@ export class MerchantClient {
|
|
|
97
97
|
getMerchant() {
|
|
98
98
|
return this.request("GET", "/v1/agent/merchant", undefined, true);
|
|
99
99
|
}
|
|
100
|
+
/** Search publicly discoverable prepared resources. This read-only call needs no API key. */
|
|
101
|
+
async resolveResources(input = {}) {
|
|
102
|
+
const query = input.query ?? "";
|
|
103
|
+
const limit = input.limit ?? 20;
|
|
104
|
+
const cursor = input.cursor ?? 0;
|
|
105
|
+
if (typeof query !== "string" || query.trim().length > 80) {
|
|
106
|
+
throw new TypeError("query must be a string no longer than 80 characters.");
|
|
107
|
+
}
|
|
108
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 50) {
|
|
109
|
+
throw new TypeError("limit must be an integer from 1 through 50.");
|
|
110
|
+
}
|
|
111
|
+
if (!Number.isSafeInteger(cursor) || cursor < 0 || cursor > 100_000_000) {
|
|
112
|
+
throw new TypeError("cursor must be an integer from 0 through 100000000.");
|
|
113
|
+
}
|
|
114
|
+
const params = new URLSearchParams({ q: query.trim(), limit: String(limit), cursor: String(cursor) });
|
|
115
|
+
const payload = await this.request("GET", `/v1/merchant-directory?${params}`);
|
|
116
|
+
if (!isRecord(payload) || !Array.isArray(payload.items) ||
|
|
117
|
+
!(payload.nextCursor === null || (Number.isSafeInteger(payload.nextCursor) && payload.nextCursor >= 0)) ||
|
|
118
|
+
!payload.items.every(isResolvedAgentResource)) {
|
|
119
|
+
throw invalidPublicResponse();
|
|
120
|
+
}
|
|
121
|
+
return payload;
|
|
122
|
+
}
|
|
123
|
+
/** Read one provider's public profile and published resource descriptors by known ID. */
|
|
124
|
+
async getProviderResources(merchantId) {
|
|
125
|
+
if (!nonempty(merchantId))
|
|
126
|
+
throw new TypeError("merchantId must be a nonempty string.");
|
|
127
|
+
const payload = await this.request("GET", `/v1/merchants/${encodeURIComponent(merchantId)}`);
|
|
128
|
+
if (!isRecord(payload) || !isProviderProfile(payload.merchant) || !Array.isArray(payload.offers) ||
|
|
129
|
+
!payload.offers.every(isAgentResource)) {
|
|
130
|
+
throw invalidPublicResponse();
|
|
131
|
+
}
|
|
132
|
+
return payload;
|
|
133
|
+
}
|
|
134
|
+
/** Resolve an older resource ID to its currently published version. This call is public. */
|
|
135
|
+
async resolveLatestResource(merchantId, offerId) {
|
|
136
|
+
if (!nonempty(merchantId) || !nonempty(offerId)) {
|
|
137
|
+
throw new TypeError("merchantId and offerId must be nonempty strings.");
|
|
138
|
+
}
|
|
139
|
+
const path = `/v1/merchants/${encodeURIComponent(merchantId)}/offers/${encodeURIComponent(offerId)}/latest`;
|
|
140
|
+
const payload = await this.request("GET", path);
|
|
141
|
+
if (!isRecord(payload) || !isPublicProvider(payload.merchant) || !isAgentResource(payload.offer)) {
|
|
142
|
+
throw invalidPublicResponse();
|
|
143
|
+
}
|
|
144
|
+
return payload;
|
|
145
|
+
}
|
|
100
146
|
setDiscoverable(discoverable) {
|
|
101
147
|
return this.request("PUT", "/v1/agent/merchant/discovery", { discoverable }, true);
|
|
102
148
|
}
|
|
@@ -113,6 +159,44 @@ export class MerchantClient {
|
|
|
113
159
|
getSale(operationId) {
|
|
114
160
|
return this.request("GET", `/v1/agent/merchant/sales/${encodeURIComponent(operationId)}`, undefined, true);
|
|
115
161
|
}
|
|
162
|
+
/** Read this merchant's private work inbox; requires the merchant:work scope. */
|
|
163
|
+
listWorkTickets(input = {}) {
|
|
164
|
+
const limit = input.limit ?? 20;
|
|
165
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 50)
|
|
166
|
+
throw new TypeError("limit must be an integer from 1 through 50.");
|
|
167
|
+
const query = new URLSearchParams({ limit: String(limit) });
|
|
168
|
+
if (input.states !== undefined) {
|
|
169
|
+
if (!Array.isArray(input.states) || input.states.length === 0 || input.states.length > 10 || !input.states.every(state => workTicketStates.includes(state))) {
|
|
170
|
+
throw new TypeError("states must contain 1–10 supported Work Ticket states.");
|
|
171
|
+
}
|
|
172
|
+
query.set("states", input.states.join(","));
|
|
173
|
+
}
|
|
174
|
+
return this.request("GET", `/v1/agent/work-tickets?${query}`, undefined, true);
|
|
175
|
+
}
|
|
176
|
+
/** Public progress and recovery metadata; no API key, private input, or result bytes. */
|
|
177
|
+
getWorkTicketStatus(ticketId) {
|
|
178
|
+
return this.request("GET", `/v1/work-tickets/${checkedWorkTicketId(ticketId)}`);
|
|
179
|
+
}
|
|
180
|
+
/** Claim a ticket addressed to this merchant for a bounded lease. */
|
|
181
|
+
claimWorkTicket(ticketId, leaseSeconds = 600) {
|
|
182
|
+
if (!Number.isInteger(leaseSeconds) || leaseSeconds < 30 || leaseSeconds > 3600)
|
|
183
|
+
throw new TypeError("leaseSeconds must be an integer from 30 through 3600.");
|
|
184
|
+
return this.request("POST", `/v1/agent/work-tickets/${checkedWorkTicketId(ticketId)}/claims`, { leaseSeconds }, true);
|
|
185
|
+
}
|
|
186
|
+
/** Submit one fixed quote; the server rejects amounts above the buyer-signed cap. */
|
|
187
|
+
quoteWorkTicket(ticketId, input) {
|
|
188
|
+
if (!Number.isInteger(input.expiresInSeconds) || input.expiresInSeconds < 30 || input.expiresInSeconds > 3600)
|
|
189
|
+
throw new TypeError("expiresInSeconds must be an integer from 30 through 3600.");
|
|
190
|
+
return this.request("POST", `/v1/agent/work-tickets/${checkedWorkTicketId(ticketId)}/quotes`, input, true);
|
|
191
|
+
}
|
|
192
|
+
/** Read this merchant's own ticket, including input and buyer receipt metadata. */
|
|
193
|
+
getWorkTicket(ticketId) {
|
|
194
|
+
return this.request("GET", `/v1/agent/work-tickets/${checkedWorkTicketId(ticketId)}`, undefined, true);
|
|
195
|
+
}
|
|
196
|
+
/** Submit a bounded result after the checkout operation is confirmed settled. */
|
|
197
|
+
submitWorkTicketResult(ticketId, input) {
|
|
198
|
+
return this.request("POST", `/v1/agent/work-tickets/${checkedWorkTicketId(ticketId)}/results`, input, true);
|
|
199
|
+
}
|
|
116
200
|
async request(method, path, body, authenticated = false) {
|
|
117
201
|
if (authenticated && !this.#apiKey) {
|
|
118
202
|
throw new Error("This request requires a merchant API key. Use client.withApiKey(apiKey).");
|
|
@@ -124,6 +208,7 @@ export class MerchantClient {
|
|
|
124
208
|
headers.Authorization = `Bearer ${this.#apiKey}`;
|
|
125
209
|
const response = await this.fetcher(new URL(`${this.baseUrl}${path}`), {
|
|
126
210
|
method,
|
|
211
|
+
redirect: "error",
|
|
127
212
|
headers,
|
|
128
213
|
...(body !== undefined ? { body: JSON.stringify(body) } : {}),
|
|
129
214
|
});
|
|
@@ -167,3 +252,39 @@ function isRecord(value) {
|
|
|
167
252
|
}
|
|
168
253
|
function nonempty(value) { return typeof value === "string" && value.trim().length > 0; }
|
|
169
254
|
function address(value) { return typeof value === "string" && /^0x[0-9a-fA-F]{40}$/.test(value); }
|
|
255
|
+
function isAgentResource(value) {
|
|
256
|
+
if (!isRecord(value))
|
|
257
|
+
return false;
|
|
258
|
+
const amountAtomic = value.amountAtomic;
|
|
259
|
+
const amountUSDC = value.amountUSDC;
|
|
260
|
+
return nonempty(value.id) && nonempty(value.seriesId) && nonempty(value.version) &&
|
|
261
|
+
nonempty(value.title) && typeof value.description === "string" &&
|
|
262
|
+
typeof amountAtomic === "string" && /^[1-9][0-9]*$/.test(amountAtomic) &&
|
|
263
|
+
typeof amountUSDC === "string" && /^(0|[1-9][0-9]*)(\.[0-9]{1,6})?$/.test(amountUSDC) &&
|
|
264
|
+
(value.contentType === "application/json" || value.contentType === "text/plain; charset=utf-8") &&
|
|
265
|
+
nonempty(value.latest) && nonempty(value.checkout) && nonempty(value.directTransferChallenge) &&
|
|
266
|
+
(value.supersedesOfferId === undefined || value.supersedesOfferId === null || nonempty(value.supersedesOfferId));
|
|
267
|
+
}
|
|
268
|
+
function isResolvedAgentResource(value) {
|
|
269
|
+
if (!isRecord(value) || !isRecord(value.merchant))
|
|
270
|
+
return false;
|
|
271
|
+
return nonempty(value.merchant.id) && nonempty(value.merchant.name) && nonempty(value.merchant.profile) &&
|
|
272
|
+
isAgentResource(value.offer);
|
|
273
|
+
}
|
|
274
|
+
function isProviderProfile(value) {
|
|
275
|
+
return isRecord(value) && nonempty(value.id) && nonempty(value.name) && address(value.payoutAddress) &&
|
|
276
|
+
nonempty(value.createdAt) && Number.isFinite(Date.parse(value.createdAt));
|
|
277
|
+
}
|
|
278
|
+
function isPublicProvider(value) {
|
|
279
|
+
return isRecord(value) && nonempty(value.id) && nonempty(value.name) && address(value.payoutAddress);
|
|
280
|
+
}
|
|
281
|
+
function invalidPublicResponse() {
|
|
282
|
+
return new MerchantApiError(200, "invalid_response", "402Mob returned an incomplete public resource response.");
|
|
283
|
+
}
|
|
284
|
+
const workTicketStates = ["open", "claimed", "quoted", "accepted", "paying", "paid", "delivered", "accepted_by_buyer", "disputed", "expired"];
|
|
285
|
+
function checkedWorkTicketId(id) {
|
|
286
|
+
if (typeof id !== "string" || !/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(id)) {
|
|
287
|
+
throw new TypeError("ticketId must be a UUID.");
|
|
288
|
+
}
|
|
289
|
+
return id;
|
|
290
|
+
}
|
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
type EvmAddress,
|
|
4
4
|
type EvmSignature,
|
|
5
5
|
type MerchantOffer,
|
|
6
|
-
} from "../
|
|
6
|
+
} from "../dist/index.js";
|
|
7
7
|
|
|
8
8
|
export interface MessageSigningWallet {
|
|
9
9
|
address: EvmAddress;
|
|
@@ -21,6 +21,7 @@ export interface SecretStore {
|
|
|
21
21
|
export async function enrollAndPublish(input: {
|
|
22
22
|
wallet: MessageSigningWallet;
|
|
23
23
|
secretStore: SecretStore;
|
|
24
|
+
discoverable?: boolean;
|
|
24
25
|
}): Promise<{ offer: MerchantOffer; merchantClient: MerchantClient }> {
|
|
25
26
|
const publicClient = new MerchantClient();
|
|
26
27
|
const enrollment = await publicClient.enrollWithSignature({
|
|
@@ -38,6 +39,7 @@ export async function enrollAndPublish(input: {
|
|
|
38
39
|
contentType: "application/json",
|
|
39
40
|
result: { summary: "Prepared by the seller agent" },
|
|
40
41
|
});
|
|
42
|
+
if (input.discoverable) await merchantClient.setDiscoverable(true);
|
|
41
43
|
|
|
42
44
|
return { offer, merchantClient };
|
|
43
45
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { MerchantClient, type ResolveAgentResourcesInput } from "../dist/index.js";
|
|
2
|
+
|
|
3
|
+
/** Public, read-only discovery for a buyer agent; checkout remains a separate step. */
|
|
4
|
+
export async function resolveAgentResources(input: ResolveAgentResourcesInput = {}) {
|
|
5
|
+
const buyer = new MerchantClient();
|
|
6
|
+
const page = await buyer.resolveResources(input);
|
|
7
|
+
|
|
8
|
+
return page.items.map(({ merchant, offer }) => ({
|
|
9
|
+
providerId: merchant.id,
|
|
10
|
+
providerName: merchant.name,
|
|
11
|
+
offerId: offer.id,
|
|
12
|
+
title: offer.title,
|
|
13
|
+
description: offer.description,
|
|
14
|
+
amountUSDC: offer.amountUSDC,
|
|
15
|
+
contentType: offer.contentType,
|
|
16
|
+
checkout: offer.checkout,
|
|
17
|
+
latest: offer.latest,
|
|
18
|
+
}));
|
|
19
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "402mob-merchant-sdk",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "TypeScript client for resolving agent resources and enrolling and operating 402Mob merchants.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|