Bazaar: seller-side discovery metadata helpers (and list Lens's own endpoints) - #154
Conversation
The buyer is software. An agent choosing between two price feeds cannot open your docs - it has only what the listing declares, and ranking (Miracle656#129) can only rank on what is declared. So description is the first positional argument of every param constructor, and a declaration missing one does not validate. It is not a warning you can ship past. src/bazaar/declare.ts: - param.string/number/integer/boolean/enumOf, with example and default, emitting JSON Schema fragments - declareHttpResource and declareMcpTool, assembling the resource block, accepts (CAIP-2 network filled from the listing network) and the extensions.bazaar declaration, picking queryParams vs body/bodyType from the HTTP method - validateDeclaration, which runs the catalog's own validateListing so local and remote cannot drift, then adds the seller-side rules the catalog cannot enforce: missing or too-short parameter descriptions, a weak resource description, a routeTemplate naming an undeclared param, and a listing with no price - assertDeclaration, the throwing form, so a bad listing fails the boot instead of silently never appearing in the Bazaar src/bazaar/lensListings.ts declares the routes middleware/x402.ts already gates - /price, /candles, /pools, /price/twap - plus the MCP face of the price feed, each through assertDeclaration at module load. 23 tests in src/__tests__/bazaarDeclare.test.ts, and a walkthrough in docs/x402/bazaar-seller-guide.md.
|
@DevTobis Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits. You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀 |
Miracle656
left a comment
There was a problem hiding this comment.
Approved and merging. Verified locally on a merge with main: tsc --noEmit clean, full suite 347 passed / 1 skipped.
The framing here is the reason this is good, and it's stated in one line at the top of the module: the buyer is software. An agent choosing between two price feeds cannot open your docs, cannot infer that assetA accepts "CODE:ISSUER", and cannot ask. It has the listing and nothing else. Everything downstream follows from taking that seriously.
Two consequences you drew that most implementations miss:
description is the first positional argument of every param.* constructor. That is the enforcement — not a lint rule, not a docs convention, but a signature you cannot satisfy while forgetting the thing that matters. An options-bag { description? } would have been the obvious API and would have quietly produced half-described catalogs.
A missing description fails validation rather than warning. This is the right severity, and the justification is the one I'd have given: ranking (#129) can only rank on what is declared, so metadata quality is a hard ceiling on search quality no matter how good the retrieval gets. A warning you can ship past is a catalog that degrades one listing at a time.
Reusing the catalog's own validateListing instead of reimplementing the rules is the structural decision I checked most carefully, because a parallel validator is worse than none — it drifts, and then a listing passes locally and is rejected in production for a reason the seller cannot see. Composing on the same function means "passes here" and "the catalog accepts it" cannot diverge. It also means this lands cleanly on top of #150's traversal work rather than beside it.
undeclared_route_param — catching a routeTemplate that names a :param absent from pathParams — is a nice touch: that mistake produces a listing an agent will construct a malformed call from, and nothing else in the pipeline would have caught it.
Declaring Lens's own gated routes with the helpers in lensListings.ts is what makes this real rather than aspirational. It's also the honest test: an API whose author won't use it on their own endpoints usually has a reason.
Thanks — this and #153 together move the Bazaar work from 'implemented' to 'usable by someone who isn't you'.
closes #134
Summary
Seller-side helpers for declaring Bazaar discovery metadata, plus Lens's own gated routes declared with them.
The design decision that matters
The issue's core point is that per-parameter descriptions must be first-class, not an afterthought. I made that structural rather than aspirational:
descriptionis the first positional argument of everyparam.*constructor. You cannot forget it by accident — the signature is the enforcement.assetA: stringand no prose caps search quality for everyone in the catalog, not just for its own seller.What's here
src/bazaar/declare.tsparam.string / number / integer / boolean / enumOf, each takingdescriptionfirst and optionalrequired(default true),exampleanddefault. Anexampleis worth more to an agent than another sentence of prose, so it is a first-class option.declareHttpResourceanddeclareMcpTool— assemble the resource block,accepts, and theextensions.bazaardeclaration with proper JSON Schema (additionalProperties: false, arequiredlist built from the flags). The HTTP builder picksqueryParamsvsbody+bodyTypefrom the method, so you never have to remember which slot a POST uses. The CAIP-2networkonacceptsis filled from the listing's network unless you set it explicitly.validateDeclaration/assertDeclaration.src/bazaar/lensListings.ts— the dogfood./price/:assetA/:assetB,/candles/:assetA/:assetB,/pools,/price/twap/:assetA/:assetB(the routessrc/middleware/x402.tsactually gates), plus the MCP face of the price feed. Each goes throughassertDeclarationat module load.docs/x402/bazaar-seller-guide.md, 23 tests, and a changeset.Validation reuses the catalog's own validator
validateDeclarationcallsvalidateListingfromsrc/bazaar/validation.tsunder the hood rather than reimplementing it. That matters: local and remote cannot drift apart, and the problems come back in the catalog's own{ field, code, message }shape — a seller sees the same field path and code locally that the catalog would have soft-dropped them with. Then it adds the rules the catalog cannot enforce, because by the time a listing reaches the catalog the metadata is all it has:missing_param_descriptionparam_description_too_shortweak_resource_descriptionundeclared_route_paramrouteTemplatenames a:paramabsent frompathParams— an agent knows the URL shape but not what goes in the slotmissing_acceptsassertDeclarationis the throwing form, listing every problem with its field path. Called at module load, a malformed listing fails the boot rather than quietly never appearing in the Bazaar — which is the failure mode worth designing against, because nothing tells you it happened.Acceptance criteria
/poolsis 7 lines;/pricewith two described path params is 15.validateDeclaration/assertDeclaration, reusingvalidateListing.declareHttpResource,declareMcpTool.src/bazaar/lensListings.ts.On "appear in the catalog", and on the timing question
Two places where I want to be straight rather than tick a box.
The listings are built and validated, but nothing registers them yet. Automatic cataloging from the discovery extension is #130, and the registration path it will use (
registerBazaarResource, and emitting the extension on the 402 payment path) is that issue's surface. I did not want to half-build #130 inside #134 and leave two partial implementations to reconcile —lensListings()is a pure function returning validatedRegisterBazaarResourceInput[], which is exactly the input #130 needs, so it should be a short step. Say if you would rather I wire it here instead."Say in the PR how long it actually took someone who had not seen it before." I can't honestly answer that — I built the helpers, so I am the worst possible measurement. What I can report: declaring the four Lens routes against the finished API took a few minutes each, and the helpers caught two real mistakes in my own dogfooding that would otherwise have shipped — a
routeTemplatenaming:assetBbefore I had declared it, and a first-draft/poolsdescription too thin to rank on. That is the mechanism working, but it is not the user study the criterion asks for. Worth handing to someone who hasn't seen it and recording the real number.Verification
npx vitest run src/__tests__/bazaarDeclare.test.ts— 23 passednpx tsc --noEmit— no errors from any file in this changeOne pre-existing failure, not from this change:
tests/mcp.test.tsfails withCannot find package '@modelcontextprotocol/sdk/server/index.js', andsrc/mcp/server.tsproduces 4 typecheck errors for the same reason. The package is declared inpackage.jsonbut is not present innode_modules. I confirmed it by stashing this branch and re-running on cleanmain— identical failure. Left alone deliberately rather than bundling an unrelated dependency fix into this PR.