') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); feat(monetize): ServiceOffer type=dataset — CRD, catalog & x402 extras wiring (P1) by bussyjd · Pull Request #640 · ObolNetwork/obol-stack · GitHub
Skip to content

feat(monetize): ServiceOffer type=dataset — CRD, catalog & x402 extras wiring (P1) - #640

Open
bussyjd wants to merge 1 commit into
mainfrom
feat/dataset-offer-p1
Open

feat(monetize): ServiceOffer type=dataset — CRD, catalog & x402 extras wiring (P1)#640
bussyjd wants to merge 1 commit into
mainfrom
feat/dataset-offer-p1

Conversation

@bussyjd

Copy link
Copy Markdown
Contributor

P1 — ServiceOffertype=dataset (declarations + pipeline wiring)

First slice of the decentralized fine-tuning plan: make a versioned dataset a first-class type=dataset ServiceOffer so the existing controller→Middleware→HTTPRoute→ForwardAuth→catalog pipeline publishes and gates it with no new serving code. This PR is declarations + pipeline glue + parity tests only — it deliberately does not build the dataset server, version log, or download client (later phases).

What it adds

SurfaceChange
CRD (internal/monetizeapi/types.go)dataset added to spec.type enum; new ServiceOfferDataset{manifestHash, version, fileHash, sizeBytes} spec block (mirrors ServiceOfferAgent); PriceTable.PerMB; IsDataset() helper
Verifier extras (internal/x402)RouteRule.Dataset* fields; routeRuleFromOffer dataset branch (hashes lowercased); effectivePriceperMB case; mergeDatasetExtras() adds accepts[].extra.dataset{manifestHash,version,fileHash,sizeBytes} to the 402, wired after mergeAgentExtras
Catalog (internal/schemas, render.go)dataset in the catalog type enum + perMB in priceUnit enum + 3 additive dataset* properties; ServiceCatalogEntry.Dataset* fields; populated in buildServiceCatalogJSON; perMB handled in offerPriceRawAndUnit + describeOfferPrice
Generatedserviceoffer-crd.yaml + zz_generated.deepcopy.go regenerated via just generate (committed)

Deliberately unchanged (parity with http)

  • normalizeOfferType folds datasethttp render branch (a download isn't chat-completions) → generic 402 copy + bazaarGenericJSON. No bespoke datasetCopy() / chat schema. Dataset metadata reaches buyers only via extra.dataset.
  • No obol sell dataset CLI, no server, no download/versionlog — those are P2/P3.

Tests (matching the other types)

  • TestServiceOfferCRD_Fields extended (enum + spec fields) + new TestServiceOfferCRD_DatasetFields (dataset block schema + perMB)
  • dataset_extras_test.go: noop / all-fields / nil-extra / omit-zero / additive-preservation (mirrors agent_extras_test.go)
  • TestRouteRuleFromOffer_DatasetAdvertisesDatasetMetadata (lowercasing + perMB price + all 4 fields)
  • TestBuildServiceCatalogJSON_DatasetOfferSurfacesVersion + …_NonDatasetOmitsDatasetFields (omitempty)
  • TestBuildBazaarExtension dataset case; TestFallbackOfferType dataset case; TestDescribeOfferPriceperMB (USDC + OBOL)
  • TestHTMLAware_DatasetUsesGenericHTTPCopy (pins the http-fold)

Status

just generate clean (no drift), go build ./... clean, go vet clean, full go test ./... green. Net diff: 15 files, +415/−11 (~190 LOC production, rest tests + generated).

…s wiring
P1 of the decentralized fine-tuning plan: make a versioned dataset a
first-class type=dataset ServiceOffer so the existing
controller -> Middleware -> HTTPRoute -> ForwardAuth -> catalog pipeline
publishes and gates it with no new serving code. Declarations + pipeline
wiring + parity tests only; the dataset server/versionlog/download client
are later phases.
- CRD: dataset enum value; ServiceOfferDataset{manifestHash,version,
fileHash,sizeBytes} spec block (mirrors ServiceOfferAgent);
PriceTable.PerMB; IsDataset(); regenerated serviceoffer-crd.yaml + deepcopy.
- x402: RouteRule.Dataset* fields; routeRuleFromOffer dataset branch
(hex digests lowercased); effectivePrice perMB; mergeDatasetExtras()
adds accepts[].extra.dataset{...} to the 402, wired after mergeAgentExtras.
- catalog: schema type enum + perMB priceUnit + additive dataset* properties;
ServiceCatalogEntry.Dataset*; buildServiceCatalogJSON population; perMB in
offerPriceRawAndUnit + describeOfferPrice.
- dataset folds to the http render branch in normalizeOfferType (a download
is not chat-completions): generic 402 copy + bazaarGenericJSON, no bespoke
copy. Version metadata reaches buyers only via extra.dataset.
- parity tests across CRD fields/block, mergeDatasetExtras, route-rule,
catalog surface + omitempty, bazaar, fallbackOfferType, describeOfferPrice
(incl. perMB precedence), and the HTML 402 copy fold.
go build/vet, full go test ./..., and just generate (idempotent) all green.
@bussyjd
bussyjdforce-pushed the feat/dataset-offer-p1 branch from e2c8280 to 380703bCompareJune 14, 2026 14:29
@OisinKyne

Copy link
Copy Markdown
Contributor

who's the buyer of datasets? what datasets are particularly suited to x402 + discover and purchase from an unknown party? what aspects need a new type versus selling it under http?

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@bussyjd@OisinKyne