402mob-merchant-sdk 0.2.0 → 0.3.1

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
@@ -2,7 +2,7 @@
2
2
 
3
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
- This guide documents SDK version **0.2.0**, which adds public agent resource resolution. 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).
29
+ Version **0.3.1** requires standard 65-byte EIP-191 signatures for enrollment and credential rotation, rejecting malformed or compact 64-byte signatures before submission. It retains resource resolution and seller Work Ticket methods from 0.3.0. See the [npm package](https://www.npmjs.com/package/402mob-merchant-sdk) and [source](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
 
@@ -94,6 +94,27 @@ if (page.items[0]) {
94
94
 
95
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
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
+
97
118
  ## Enrollment with a wallet transaction tool
98
119
 
99
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.
@@ -178,3 +199,16 @@ Adds `setDiscoverable(discoverable)`, which calls the authenticated `PUT /v1/age
178
199
  ## Version 0.1.2
179
200
 
180
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
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 available from version 0.3.0.
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;
@@ -182,6 +183,66 @@ export interface SaleDetails {
182
183
  responseHash: string | null;
183
184
  deliveryReceipts: unknown[];
184
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
+ };
185
246
  export interface MerchantClientOptions {
186
247
  /** Defaults to https://evidence.contact. */
187
248
  baseUrl?: string;
@@ -273,6 +334,34 @@ export declare class MerchantClient {
273
334
  setOfferActive(offerId: string, active: boolean): Promise<MerchantOffer>;
274
335
  listSales(limit?: number): Promise<SaleList>;
275
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
+ }>;
276
365
  private request;
277
366
  }
278
367
  export {};
package/dist/index.js CHANGED
@@ -1,3 +1,9 @@
1
+ function requireEip191Signature(signature) {
2
+ if (!/^0x[0-9a-fA-F]{130}$/.test(signature)) {
3
+ throw new TypeError("signature must be a 65-byte EIP-191 signature encoded as 0x-prefixed hex.");
4
+ }
5
+ return signature;
6
+ }
1
7
  export class MerchantApiError extends Error {
2
8
  status;
3
9
  code;
@@ -44,7 +50,7 @@ export class MerchantClient {
44
50
  return this.request("POST", "/v1/merchants/enrollment-challenges", input);
45
51
  }
46
52
  completeEnrollmentWithSignature(input) {
47
- return this.request("POST", "/v1/merchants/enrollments", input);
53
+ return this.request("POST", "/v1/merchants/enrollments", { ...input, signature: requireEip191Signature(input.signature) });
48
54
  }
49
55
  completeEnrollmentWithTransaction(input) {
50
56
  return this.request("POST", "/v1/merchants/enrollments/transaction", input);
@@ -52,7 +58,7 @@ export class MerchantClient {
52
58
  /** Request a challenge, ask the caller's wallet adapter to sign its exact message, then enroll. */
53
59
  async enrollWithSignature(input) {
54
60
  const challenge = await this.createEnrollmentChallenge({ wallet: input.wallet, name: input.name });
55
- const signature = await input.signMessage(challenge.message);
61
+ const signature = requireEip191Signature(await input.signMessage(challenge.message));
56
62
  return this.completeEnrollmentWithSignature({
57
63
  wallet: input.wallet,
58
64
  name: input.name,
@@ -75,14 +81,14 @@ export class MerchantClient {
75
81
  return this.request("POST", "/v1/merchants/credential-challenges", { wallet });
76
82
  }
77
83
  rotateCredentialWithSignature(wallet, challengeId, signature) {
78
- return this.request("POST", "/v1/merchants/credentials/rotate", { wallet, challengeId, signature });
84
+ return this.request("POST", "/v1/merchants/credentials/rotate", { wallet, challengeId, signature: requireEip191Signature(signature) });
79
85
  }
80
86
  rotateCredentialWithTransaction(input) {
81
87
  return this.request("POST", "/v1/merchants/credentials/rotate-transaction", input);
82
88
  }
83
89
  async rotateWithSignature(input) {
84
90
  const challenge = await this.createCredentialRotationChallenge(input.wallet);
85
- const signature = await input.signMessage(challenge.message);
91
+ const signature = requireEip191Signature(await input.signMessage(challenge.message));
86
92
  return this.rotateCredentialWithSignature(input.wallet, challenge.challengeId, signature);
87
93
  }
88
94
  async rotateWithTransaction(input) {
@@ -159,6 +165,44 @@ export class MerchantClient {
159
165
  getSale(operationId) {
160
166
  return this.request("GET", `/v1/agent/merchant/sales/${encodeURIComponent(operationId)}`, undefined, true);
161
167
  }
168
+ /** Read this merchant's private work inbox; requires the merchant:work scope. */
169
+ listWorkTickets(input = {}) {
170
+ const limit = input.limit ?? 20;
171
+ if (!Number.isInteger(limit) || limit < 1 || limit > 50)
172
+ throw new TypeError("limit must be an integer from 1 through 50.");
173
+ const query = new URLSearchParams({ limit: String(limit) });
174
+ if (input.states !== undefined) {
175
+ if (!Array.isArray(input.states) || input.states.length === 0 || input.states.length > 10 || !input.states.every(state => workTicketStates.includes(state))) {
176
+ throw new TypeError("states must contain 1–10 supported Work Ticket states.");
177
+ }
178
+ query.set("states", input.states.join(","));
179
+ }
180
+ return this.request("GET", `/v1/agent/work-tickets?${query}`, undefined, true);
181
+ }
182
+ /** Public progress and recovery metadata; no API key, private input, or result bytes. */
183
+ getWorkTicketStatus(ticketId) {
184
+ return this.request("GET", `/v1/work-tickets/${checkedWorkTicketId(ticketId)}`);
185
+ }
186
+ /** Claim a ticket addressed to this merchant for a bounded lease. */
187
+ claimWorkTicket(ticketId, leaseSeconds = 600) {
188
+ if (!Number.isInteger(leaseSeconds) || leaseSeconds < 30 || leaseSeconds > 3600)
189
+ throw new TypeError("leaseSeconds must be an integer from 30 through 3600.");
190
+ return this.request("POST", `/v1/agent/work-tickets/${checkedWorkTicketId(ticketId)}/claims`, { leaseSeconds }, true);
191
+ }
192
+ /** Submit one fixed quote; the server rejects amounts above the buyer-signed cap. */
193
+ quoteWorkTicket(ticketId, input) {
194
+ if (!Number.isInteger(input.expiresInSeconds) || input.expiresInSeconds < 30 || input.expiresInSeconds > 3600)
195
+ throw new TypeError("expiresInSeconds must be an integer from 30 through 3600.");
196
+ return this.request("POST", `/v1/agent/work-tickets/${checkedWorkTicketId(ticketId)}/quotes`, input, true);
197
+ }
198
+ /** Read this merchant's own ticket, including input and buyer receipt metadata. */
199
+ getWorkTicket(ticketId) {
200
+ return this.request("GET", `/v1/agent/work-tickets/${checkedWorkTicketId(ticketId)}`, undefined, true);
201
+ }
202
+ /** Submit a bounded result after the checkout operation is confirmed settled. */
203
+ submitWorkTicketResult(ticketId, input) {
204
+ return this.request("POST", `/v1/agent/work-tickets/${checkedWorkTicketId(ticketId)}/results`, input, true);
205
+ }
162
206
  async request(method, path, body, authenticated = false) {
163
207
  if (authenticated && !this.#apiKey) {
164
208
  throw new Error("This request requires a merchant API key. Use client.withApiKey(apiKey).");
@@ -170,6 +214,7 @@ export class MerchantClient {
170
214
  headers.Authorization = `Bearer ${this.#apiKey}`;
171
215
  const response = await this.fetcher(new URL(`${this.baseUrl}${path}`), {
172
216
  method,
217
+ redirect: "error",
173
218
  headers,
174
219
  ...(body !== undefined ? { body: JSON.stringify(body) } : {}),
175
220
  });
@@ -242,3 +287,10 @@ function isPublicProvider(value) {
242
287
  function invalidPublicResponse() {
243
288
  return new MerchantApiError(200, "invalid_response", "402Mob returned an incomplete public resource response.");
244
289
  }
290
+ const workTicketStates = ["open", "claimed", "quoted", "accepted", "paying", "paid", "delivered", "accepted_by_buyer", "disputed", "expired"];
291
+ function checkedWorkTicketId(id) {
292
+ 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)) {
293
+ throw new TypeError("ticketId must be a UUID.");
294
+ }
295
+ return id;
296
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "402mob-merchant-sdk",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "TypeScript client for resolving agent resources and enrolling and operating 402Mob merchants.",
5
5
  "license": "MIT",
6
6
  "repository": {