402mob-merchant-sdk 0.1.2 → 0.2.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # 402Mob Merchant SDK
2
2
 
3
- An open-source, dependency-free TypeScript client for enrolling and operating a 402Mob merchant from a Node.js server or agent runtime.
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
5
  It wraps the existing HTTP merchant API. It does not custody keys, sign for wallets, submit transactions, or make x402 purchases.
6
6
 
@@ -26,7 +26,7 @@ Install from npm:
26
26
  npm install 402mob-merchant-sdk
27
27
  ```
28
28
 
29
- Prepared package version: **0.1.2**. Publish this version before installing it from npm. 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
+ 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).
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
 
@@ -56,11 +56,44 @@ const offer = await merchant.createOffer({
56
56
  result: { summary: "Prepared by the seller agent" },
57
57
  });
58
58
 
59
+ // Make active offers searchable in the public merchant directory.
60
+ await merchant.setDiscoverable(true);
61
+
59
62
  console.log({ offerId: offer.id, checkout: offer.checkout });
60
63
  ```
61
64
 
62
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.
63
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
+
64
97
  ## Enrollment with a wallet transaction tool
65
98
 
66
99
  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.
@@ -88,6 +121,9 @@ This action moves no USDC and grants no allowance, but it costs Base ETH for gas
88
121
 
89
122
  ### Public methods
90
123
 
124
+ - `resolveResources({ query?, limit?, cursor? })` — search public, opted-in prepared resources without an API key.
125
+ - `getProviderResources(merchantId)` — read one known provider's public profile and published resources.
126
+ - `resolveLatestResource(merchantId, offerId)` — resolve an older offer ID to the current published version.
91
127
  - `createEnrollmentChallenge({ wallet, name })`
92
128
  - `completeEnrollmentWithSignature({ wallet, name, challengeId, signature })`
93
129
  - `completeEnrollmentWithTransaction({ wallet, name, challengeId, method, transactionHash })`
@@ -102,10 +138,11 @@ This action moves no USDC and grants no allowance, but it costs Base ETH for gas
102
138
  - `getMerchant()`
103
139
  - `createOffer(input)`
104
140
  - `setOfferActive(offerId, active)`
141
+ - `setDiscoverable(discoverable)` — opt the merchant into or out of public offer search. Defaults to hidden.
105
142
  - `listSales(limit?)`
106
143
  - `getSale(operationId)`
107
144
 
108
- 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.
145
+ 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.
109
146
 
110
147
  ## Safety and behavior
111
148
 
@@ -126,10 +163,18 @@ npm test
126
163
 
127
164
  The SDK tests use mocked HTTP responses and do not contact `evidence.contact` or submit wallet transactions.
128
165
 
129
- See [`examples/enroll-and-publish.ts`](./examples/enroll-and-publish.ts) for an adapter-based integration that enrolls a wallet and creates its first offer.
166
+ 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.
130
167
 
131
168
  MIT licensed. See [LICENSE](./LICENSE).
132
169
 
170
+ ## Version 0.2.0
171
+
172
+ 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.
173
+
174
+ ## Version 0.1.3
175
+
176
+ 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.
177
+
133
178
  ## Version 0.1.2
134
179
 
135
- 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. The npm registry still serves 0.1.1 until 0.1.2 is published. 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.
180
+ 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.
package/dist/index.d.ts CHANGED
@@ -33,6 +33,8 @@ export interface MerchantProfile {
33
33
  name: string;
34
34
  payoutAddress: EvmAddress;
35
35
  createdAt: string;
36
+ /** Present in authenticated merchant workspace responses; opt-in controls directory search. */
37
+ discoverable?: boolean;
36
38
  }
37
39
  export interface MerchantAgent {
38
40
  id: string;
@@ -111,6 +113,54 @@ export interface MerchantWorkspace {
111
113
  merchant: MerchantProfile;
112
114
  offers: MerchantOffer[];
113
115
  }
116
+ /** Public provider fields returned while an agent resolves available resources. */
117
+ export interface AgentResourceProvider {
118
+ id: string;
119
+ name: string;
120
+ /** Present in directory results; points to the provider's public profile. */
121
+ profile?: string;
122
+ /** Present in direct profile and latest-resource lookups. */
123
+ payoutAddress?: EvmAddress;
124
+ createdAt?: string;
125
+ }
126
+ /** A provider-published, fixed-price resource descriptor. */
127
+ export interface AgentResource {
128
+ id: string;
129
+ seriesId: string;
130
+ version: string;
131
+ title: string;
132
+ description: string;
133
+ amountAtomic: string;
134
+ amountUSDC: string;
135
+ contentType: ArtifactContentType;
136
+ latest: string;
137
+ checkout: string;
138
+ directTransferChallenge: string;
139
+ supersedesOfferId?: string | null;
140
+ }
141
+ export interface ResolvedAgentResource {
142
+ merchant: AgentResourceProvider;
143
+ offer: AgentResource;
144
+ }
145
+ export interface AgentResourcePage {
146
+ items: ResolvedAgentResource[];
147
+ nextCursor: number | null;
148
+ }
149
+ export interface ResolveAgentResourcesInput {
150
+ /** Searches provider names, resource titles, and descriptions; maximum 80 characters. */
151
+ query?: string;
152
+ /** Page size from 1 through 50; defaults to 20. */
153
+ limit?: number;
154
+ /** Zero-based result offset; defaults to 0. */
155
+ cursor?: number;
156
+ }
157
+ export interface AgentProviderCatalog {
158
+ merchant: AgentResourceProvider & {
159
+ payoutAddress: EvmAddress;
160
+ createdAt: string;
161
+ };
162
+ offers: AgentResource[];
163
+ }
114
164
  export interface MerchantSale {
115
165
  id: string;
116
166
  state: string;
@@ -181,7 +231,7 @@ export declare class MerchantApiError extends Error {
181
231
  constructor(status: number, code: string, message: string);
182
232
  }
183
233
  /**
184
- * Minimal, dependency-free HTTP client for 402Mob merchant enrollment and management.
234
+ * Minimal, dependency-free HTTP client for 402Mob agent resource resolution and merchant management.
185
235
  * It never signs messages, submits wallet transactions, persists API keys, or retries requests.
186
236
  */
187
237
  export declare class MerchantClient {
@@ -209,6 +259,16 @@ export declare class MerchantClient {
209
259
  rotateWithSignature(input: CredentialRotationSignatureInput): Promise<CredentialRotationResult>;
210
260
  rotateWithTransaction(input: CredentialRotationTransactionInput): Promise<CredentialRotationResult>;
211
261
  getMerchant(): Promise<MerchantWorkspace>;
262
+ /** Search publicly discoverable prepared resources. This read-only call needs no API key. */
263
+ resolveResources(input?: ResolveAgentResourcesInput): Promise<AgentResourcePage>;
264
+ /** Read one provider's public profile and published resource descriptors by known ID. */
265
+ getProviderResources(merchantId: string): Promise<AgentProviderCatalog>;
266
+ /** Resolve an older resource ID to its currently published version. This call is public. */
267
+ resolveLatestResource(merchantId: string, offerId: string): Promise<ResolvedAgentResource>;
268
+ setDiscoverable(discoverable: boolean): Promise<{
269
+ merchantId: string;
270
+ discoverable: boolean;
271
+ }>;
212
272
  createOffer(input: CreateOfferInput): Promise<MerchantOffer>;
213
273
  setOfferActive(offerId: string, active: boolean): Promise<MerchantOffer>;
214
274
  listSales(limit?: number): Promise<SaleList>;
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 merchant enrollment and management.
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,55 @@ 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
+ }
146
+ setDiscoverable(discoverable) {
147
+ return this.request("PUT", "/v1/agent/merchant/discovery", { discoverable }, true);
148
+ }
100
149
  createOffer(input) {
101
150
  return this.request("POST", "/v1/agent/merchant/offers", input, true);
102
151
  }
@@ -164,3 +213,32 @@ function isRecord(value) {
164
213
  }
165
214
  function nonempty(value) { return typeof value === "string" && value.trim().length > 0; }
166
215
  function address(value) { return typeof value === "string" && /^0x[0-9a-fA-F]{40}$/.test(value); }
216
+ function isAgentResource(value) {
217
+ if (!isRecord(value))
218
+ return false;
219
+ const amountAtomic = value.amountAtomic;
220
+ const amountUSDC = value.amountUSDC;
221
+ return nonempty(value.id) && nonempty(value.seriesId) && nonempty(value.version) &&
222
+ nonempty(value.title) && typeof value.description === "string" &&
223
+ typeof amountAtomic === "string" && /^[1-9][0-9]*$/.test(amountAtomic) &&
224
+ typeof amountUSDC === "string" && /^(0|[1-9][0-9]*)(\.[0-9]{1,6})?$/.test(amountUSDC) &&
225
+ (value.contentType === "application/json" || value.contentType === "text/plain; charset=utf-8") &&
226
+ nonempty(value.latest) && nonempty(value.checkout) && nonempty(value.directTransferChallenge) &&
227
+ (value.supersedesOfferId === undefined || value.supersedesOfferId === null || nonempty(value.supersedesOfferId));
228
+ }
229
+ function isResolvedAgentResource(value) {
230
+ if (!isRecord(value) || !isRecord(value.merchant))
231
+ return false;
232
+ return nonempty(value.merchant.id) && nonempty(value.merchant.name) && nonempty(value.merchant.profile) &&
233
+ isAgentResource(value.offer);
234
+ }
235
+ function isProviderProfile(value) {
236
+ return isRecord(value) && nonempty(value.id) && nonempty(value.name) && address(value.payoutAddress) &&
237
+ nonempty(value.createdAt) && Number.isFinite(Date.parse(value.createdAt));
238
+ }
239
+ function isPublicProvider(value) {
240
+ return isRecord(value) && nonempty(value.id) && nonempty(value.name) && address(value.payoutAddress);
241
+ }
242
+ function invalidPublicResponse() {
243
+ return new MerchantApiError(200, "invalid_response", "402Mob returned an incomplete public resource response.");
244
+ }
@@ -3,7 +3,7 @@ import {
3
3
  type EvmAddress,
4
4
  type EvmSignature,
5
5
  type MerchantOffer,
6
- } from "../src/index.js";
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.1.2",
4
- "description": "Server-side TypeScript client for enrolling and operating 402Mob merchants.",
3
+ "version": "0.2.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",