feat(x402-metrics): add asset_symbol label for per-token queries - #531

Closed
bussyjd wants to merge 1 commit into
fix/prometheus-rule-increase-7dfrom
feat/x402-asset-symbol-label
Closed

feat(x402-metrics): add asset_symbol label for per-token queries#531
bussyjd wants to merge 1 commit into
fix/prometheus-rule-increase-7dfrom
feat/x402-asset-symbol-label

Conversation

@bussyjd

Copy link
Copy Markdown
Contributor

Summary

Adds asset_symbol to the 6 verifier metric vecs so per-token revenue is queryable in PromQL directly, without the frontend having to join metrics with the ServiceOffer CR's spec.payment.asset.symbol at query time.

Stack note: this is technically branched off fix/prom-retention-window-alignment (PR A) because that PR introduces pruneSeriesNotIn, the lastPaymentSuccess gauge, and x402-prometheus-rules.yaml — all of which this PR extends. Listed merge target is main per the stacked-PR convention; once PR A lands, this should rebase cleanly. If reviewing before PR A merges, set the GitHub base to fix/prom-retention-window-alignment to see only the asset-symbol diff.

Cardinality math (no series multiplication)

Each ServiceOffer pins exactly one asset via spec.payment.asset.symbol. That means within the existing (offer_namespace, offer_name) group, asset_symbol is functionally constant — adding it does not multiply series count.

Per-vec series count before: 6 vecs * N offers * C chains-per-offer
Per-vec series count after: 6 vecs * N offers * C chains-per-offer * 1 asset-per-offer

The don't label what you can derive heuristic exists to prevent multiplicative blowups (chain x pod x pod_owner style). The single-asset-per-offer invariant means there's no multiplication to prevent — the same internal-consistency argument that already justifies chain (also CR-derived) on these vecs applies to asset_symbol.

Current test cluster (N=3 offers):demo-hello, silvermesh-paid, silvermesh-obol -> +3 series per vec, max +18 series total cluster-wide. Negligible.

Before / after PromQL

Before (frontend joins charged-requests metric with CR spec.payment.asset.symbol in JS):

sum by (offer_namespace, offer_name, chain) (
increase(obol_x402_verifier_charged_requests_total[7d])
)
# then JS-side: foreach series, look up asset_symbol via /api/sell/list -> CR

After (direct):

sum by (asset_symbol) (x402:revenue:7d_by_offer_chain_asset_symbol)
# -> { asset_symbol="USDC" } 4231
# -> { asset_symbol="OBOL" } 88

Recording-rule migration plan

Non-breaking. Existing rules stay in place for one release:

  • x402:revenue:24h_by_offer_chain -> kept (frontend currently reads this)
  • x402:revenue:24h_by_offer_chain_asset_symbol -> new sibling
  • x402:revenue:7d_by_offer_chain -> kept
  • x402:revenue:7d_by_offer_chain_asset_symbol -> new sibling

Frontend can migrate to the _asset_symbol rules in a follow-up PR; the old rules continue to produce the same series (adding a label to the raw counter is non-breaking for queries that aggregate it away).

x402:settlement_rate:1h_by_offer_chain is intentionally not duplicated — it's a ratio; the asset wouldn't change the answer meaningfully.

Changes

  • internal/x402/metrics.go -> 6 vecs gain asset_symbol; pruneSeriesNotIn key now 4-tuple
  • internal/x402/verifier.go -> load() live-set uses 4-tuple; prometheusLabels() emits rule.AssetSymbol with "unknown" defensive fallback
  • internal/embed/infrastructure/base/templates/x402-prometheus-rules.yaml -> two new _asset_symbol-suffixed recording rules
  • internal/x402/verifier_test.go -> existing label-map assertions updated; 3 new tests added

Test plan

  • go build ./... clean
  • go vet ./internal/x402/... ./internal/embed/... clean
  • go test ./internal/x402/... green (all 6 vecs + new tests pass)
  • go test ./internal/embed/... green (CRD/manifest tests no regression)
  • Existing TestVerifier_Reload_PrunesDeletedOfferSeries from PR A still passes — asset-symbol-aware key doesn't break basic GC
  • TestVerifier_PrometheusLabels_IncludesAssetSymbol -> asserts USDC is emitted
  • TestVerifier_PrometheusLabels_DefaultsToUnknownIfEmpty -> asserts "unknown" fallback when AssetSymbol is empty
  • TestVerifier_PruneSeriesNotIn_DistinguishesAssetSymbol -> dropping USDC route while keeping OBOL route on the same offer leaves OBOL series alive (catches asset-repin leaks)
  • Live-cluster verification: scrape obol_x402_verifier_charged_requests_total{asset_symbol="OBOL"} after a paid OBOL request to silvermesh-obol returns 1
  • Recording rule rendered through helmfile: kubectl get prometheusrule -n x402 x402-verifier -o yaml shows both _by_offer_chain and _by_offer_chain_asset_symbol rules

@bussyjd
bussyjd changed the base branch from main to fix/prometheus-rule-increase-7dMay 24, 2026 06:48
Currently the verifier emits (offer_namespace, offer_name, chain).
Answering "what's my OBOL revenue?" requires joining metrics with
the ServiceOffer CR's spec.payment.asset.symbol at the frontend.
With asset_symbol on the label set, the answer is a direct PromQL
aggregation.
Cardinality cost: zero. Each offer pins exactly one asset (A=1
per offer), so the new dimension is functionally constant within
the existing (ns, name) group — no series multiplication. The
"don't label what you can derive" guidance exists to prevent
*multiplicative* blowups (chain x pod x pod_owner style); the
single-asset-per-offer invariant means there's no multiplication
to prevent.
The argument for adding asset_symbol is identical to the argument
that already justifies `chain` on these vecs: both are
CR-derived, both are query-meaningful, both have bounded values.
Changes:
- 6 metric vecs: label slice gains "asset_symbol"
- pruneSeriesNotIn key now (ns, name, chain, asset_symbol) so
asset-repin doesn't leak the old series
- verifier.load() live-set built with the same 4-tuple
- prometheusLabels() emits rule.AssetSymbol (or "unknown" if
empty as defensive fallback)
- New _asset_symbol-suffixed recording rules added side-by-side
with existing rules; existing rules unchanged (non-breaking)
- Tests: emission asserts asset_symbol; prune test asserts
asset-repin doesn't leak
Frontend can simplify the existing metric x CR join in a future PR
once it migrates to the _asset_symbol-suffixed rule.
Findings from: plans/integration-test-L7-paid-flow-20260524.md
(OBOL parity smoke surfaced this as a real gap when validating
the WalletStrip / EarningsStrip per-token columns).
@bussyjd

Copy link
Copy Markdown
ContributorAuthor

Superseded by bundle PR #536 — closing in favor of the consolidated merge target. Original branch and history preserved.

@bussyjdbussyjd closed this May 24, 2026
@OisinKyne
OisinKyne deleted the feat/x402-asset-symbol-label branch July 1, 2026 12:33
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.

1 participant

@bussyjd
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

feat(x402-metrics): add asset_symbol label for per-token queries - #531

Closed
bussyjd wants to merge 1 commit into
fix/prometheus-rule-increase-7dfrom
feat/x402-asset-symbol-label
Closed

feat(x402-metrics): add asset_symbol label for per-token queries#531
bussyjd wants to merge 1 commit into
fix/prometheus-rule-increase-7dfrom
feat/x402-asset-symbol-label

Conversation

@bussyjd

Copy link
Copy Markdown
Contributor

Summary

Adds asset_symbol to the 6 verifier metric vecs so per-token revenue is queryable in PromQL directly, without the frontend having to join metrics with the ServiceOffer CR's spec.payment.asset.symbol at query time.

Stack note: this is technically branched off fix/prom-retention-window-alignment (PR A) because that PR introduces pruneSeriesNotIn, the lastPaymentSuccess gauge, and x402-prometheus-rules.yaml — all of which this PR extends. Listed merge target is main per the stacked-PR convention; once PR A lands, this should rebase cleanly. If reviewing before PR A merges, set the GitHub base to fix/prom-retention-window-alignment to see only the asset-symbol diff.

Cardinality math (no series multiplication)

Each ServiceOffer pins exactly one asset via spec.payment.asset.symbol. That means within the existing (offer_namespace, offer_name) group, asset_symbol is functionally constant — adding it does not multiply series count.

Per-vec series count before: 6 vecs * N offers * C chains-per-offer
Per-vec series count after: 6 vecs * N offers * C chains-per-offer * 1 asset-per-offer

The don't label what you can derive heuristic exists to prevent multiplicative blowups (chain x pod x pod_owner style). The single-asset-per-offer invariant means there's no multiplication to prevent — the same internal-consistency argument that already justifies chain (also CR-derived) on these vecs applies to asset_symbol.

Current test cluster (N=3 offers):demo-hello, silvermesh-paid, silvermesh-obol -> +3 series per vec, max +18 series total cluster-wide. Negligible.

Before / after PromQL

Before (frontend joins charged-requests metric with CR spec.payment.asset.symbol in JS):

sum by (offer_namespace, offer_name, chain) (
increase(obol_x402_verifier_charged_requests_total[7d])
)
# then JS-side: foreach series, look up asset_symbol via /api/sell/list -> CR

After (direct):

sum by (asset_symbol) (x402:revenue:7d_by_offer_chain_asset_symbol)
# -> { asset_symbol="USDC" } 4231
# -> { asset_symbol="OBOL" } 88

Recording-rule migration plan

Non-breaking. Existing rules stay in place for one release:

  • x402:revenue:24h_by_offer_chain -> kept (frontend currently reads this)
  • x402:revenue:24h_by_offer_chain_asset_symbol -> new sibling
  • x402:revenue:7d_by_offer_chain -> kept
  • x402:revenue:7d_by_offer_chain_asset_symbol -> new sibling

Frontend can migrate to the _asset_symbol rules in a follow-up PR; the old rules continue to produce the same series (adding a label to the raw counter is non-breaking for queries that aggregate it away).

x402:settlement_rate:1h_by_offer_chain is intentionally not duplicated — it's a ratio; the asset wouldn't change the answer meaningfully.

Changes

  • internal/x402/metrics.go -> 6 vecs gain asset_symbol; pruneSeriesNotIn key now 4-tuple
  • internal/x402/verifier.go -> load() live-set uses 4-tuple; prometheusLabels() emits rule.AssetSymbol with "unknown" defensive fallback
  • internal/embed/infrastructure/base/templates/x402-prometheus-rules.yaml -> two new _asset_symbol-suffixed recording rules
  • internal/x402/verifier_test.go -> existing label-map assertions updated; 3 new tests added

Test plan

  • go build ./... clean
  • go vet ./internal/x402/... ./internal/embed/... clean
  • go test ./internal/x402/... green (all 6 vecs + new tests pass)
  • go test ./internal/embed/... green (CRD/manifest tests no regression)
  • Existing TestVerifier_Reload_PrunesDeletedOfferSeries from PR A still passes — asset-symbol-aware key doesn't break basic GC
  • TestVerifier_PrometheusLabels_IncludesAssetSymbol -> asserts USDC is emitted
  • TestVerifier_PrometheusLabels_DefaultsToUnknownIfEmpty -> asserts "unknown" fallback when AssetSymbol is empty
  • TestVerifier_PruneSeriesNotIn_DistinguishesAssetSymbol -> dropping USDC route while keeping OBOL route on the same offer leaves OBOL series alive (catches asset-repin leaks)
  • Live-cluster verification: scrape obol_x402_verifier_charged_requests_total{asset_symbol="OBOL"} after a paid OBOL request to silvermesh-obol returns 1
  • Recording rule rendered through helmfile: kubectl get prometheusrule -n x402 x402-verifier -o yaml shows both _by_offer_chain and _by_offer_chain_asset_symbol rules

@bussyjd
bussyjd changed the base branch from main to fix/prometheus-rule-increase-7dMay 24, 2026 06:48
Currently the verifier emits (offer_namespace, offer_name, chain).
Answering "what's my OBOL revenue?" requires joining metrics with
the ServiceOffer CR's spec.payment.asset.symbol at the frontend.
With asset_symbol on the label set, the answer is a direct PromQL
aggregation.
Cardinality cost: zero. Each offer pins exactly one asset (A=1
per offer), so the new dimension is functionally constant within
the existing (ns, name) group — no series multiplication. The
"don't label what you can derive" guidance exists to prevent
*multiplicative* blowups (chain x pod x pod_owner style); the
single-asset-per-offer invariant means there's no multiplication
to prevent.
The argument for adding asset_symbol is identical to the argument
that already justifies `chain` on these vecs: both are
CR-derived, both are query-meaningful, both have bounded values.
Changes:
- 6 metric vecs: label slice gains "asset_symbol"
- pruneSeriesNotIn key now (ns, name, chain, asset_symbol) so
asset-repin doesn't leak the old series
- verifier.load() live-set built with the same 4-tuple
- prometheusLabels() emits rule.AssetSymbol (or "unknown" if
empty as defensive fallback)
- New _asset_symbol-suffixed recording rules added side-by-side
with existing rules; existing rules unchanged (non-breaking)
- Tests: emission asserts asset_symbol; prune test asserts
asset-repin doesn't leak
Frontend can simplify the existing metric x CR join in a future PR
once it migrates to the _asset_symbol-suffixed rule.
Findings from: plans/integration-test-L7-paid-flow-20260524.md
(OBOL parity smoke surfaced this as a real gap when validating
the WalletStrip / EarningsStrip per-token columns).
@bussyjd

Copy link
Copy Markdown
ContributorAuthor

Superseded by bundle PR #536 — closing in favor of the consolidated merge target. Original branch and history preserved.

@bussyjdbussyjd closed this May 24, 2026
@OisinKyne
OisinKyne deleted the feat/x402-asset-symbol-label branch July 1, 2026 12:33
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.

1 participant

@bussyjd
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(x402-metrics): add asset_symbol label for per-token queries - #531

Closed
bussyjd wants to merge 1 commit into
fix/prometheus-rule-increase-7dfrom
feat/x402-asset-symbol-label
Closed

feat(x402-metrics): add asset_symbol label for per-token queries#531
bussyjd wants to merge 1 commit into
fix/prometheus-rule-increase-7dfrom
feat/x402-asset-symbol-label

Conversation

@bussyjd

Copy link
Copy Markdown
Contributor

Summary

Adds asset_symbol to the 6 verifier metric vecs so per-token revenue is queryable in PromQL directly, without the frontend having to join metrics with the ServiceOffer CR's spec.payment.asset.symbol at query time.

Stack note: this is technically branched off fix/prom-retention-window-alignment (PR A) because that PR introduces pruneSeriesNotIn, the lastPaymentSuccess gauge, and x402-prometheus-rules.yaml — all of which this PR extends. Listed merge target is main per the stacked-PR convention; once PR A lands, this should rebase cleanly. If reviewing before PR A merges, set the GitHub base to fix/prom-retention-window-alignment to see only the asset-symbol diff.

Cardinality math (no series multiplication)

Each ServiceOffer pins exactly one asset via spec.payment.asset.symbol. That means within the existing (offer_namespace, offer_name) group, asset_symbol is functionally constant — adding it does not multiply series count.

Per-vec series count before: 6 vecs * N offers * C chains-per-offer
Per-vec series count after: 6 vecs * N offers * C chains-per-offer * 1 asset-per-offer

The don't label what you can derive heuristic exists to prevent multiplicative blowups (chain x pod x pod_owner style). The single-asset-per-offer invariant means there's no multiplication to prevent — the same internal-consistency argument that already justifies chain (also CR-derived) on these vecs applies to asset_symbol.

Current test cluster (N=3 offers):demo-hello, silvermesh-paid, silvermesh-obol -> +3 series per vec, max +18 series total cluster-wide. Negligible.

Before / after PromQL

Before (frontend joins charged-requests metric with CR spec.payment.asset.symbol in JS):

sum by (offer_namespace, offer_name, chain) (
increase(obol_x402_verifier_charged_requests_total[7d])
)
# then JS-side: foreach series, look up asset_symbol via /api/sell/list -> CR

After (direct):

sum by (asset_symbol) (x402:revenue:7d_by_offer_chain_asset_symbol)
# -> { asset_symbol="USDC" } 4231
# -> { asset_symbol="OBOL" } 88

Recording-rule migration plan

Non-breaking. Existing rules stay in place for one release:

  • x402:revenue:24h_by_offer_chain -> kept (frontend currently reads this)
  • x402:revenue:24h_by_offer_chain_asset_symbol -> new sibling
  • x402:revenue:7d_by_offer_chain -> kept
  • x402:revenue:7d_by_offer_chain_asset_symbol -> new sibling

Frontend can migrate to the _asset_symbol rules in a follow-up PR; the old rules continue to produce the same series (adding a label to the raw counter is non-breaking for queries that aggregate it away).

x402:settlement_rate:1h_by_offer_chain is intentionally not duplicated — it's a ratio; the asset wouldn't change the answer meaningfully.

Changes

  • internal/x402/metrics.go -> 6 vecs gain asset_symbol; pruneSeriesNotIn key now 4-tuple
  • internal/x402/verifier.go -> load() live-set uses 4-tuple; prometheusLabels() emits rule.AssetSymbol with "unknown" defensive fallback
  • internal/embed/infrastructure/base/templates/x402-prometheus-rules.yaml -> two new _asset_symbol-suffixed recording rules
  • internal/x402/verifier_test.go -> existing label-map assertions updated; 3 new tests added

Test plan

  • go build ./... clean
  • go vet ./internal/x402/... ./internal/embed/... clean
  • go test ./internal/x402/... green (all 6 vecs + new tests pass)
  • go test ./internal/embed/... green (CRD/manifest tests no regression)
  • Existing TestVerifier_Reload_PrunesDeletedOfferSeries from PR A still passes — asset-symbol-aware key doesn't break basic GC
  • TestVerifier_PrometheusLabels_IncludesAssetSymbol -> asserts USDC is emitted
  • TestVerifier_PrometheusLabels_DefaultsToUnknownIfEmpty -> asserts "unknown" fallback when AssetSymbol is empty
  • TestVerifier_PruneSeriesNotIn_DistinguishesAssetSymbol -> dropping USDC route while keeping OBOL route on the same offer leaves OBOL series alive (catches asset-repin leaks)
  • Live-cluster verification: scrape obol_x402_verifier_charged_requests_total{asset_symbol="OBOL"} after a paid OBOL request to silvermesh-obol returns 1
  • Recording rule rendered through helmfile: kubectl get prometheusrule -n x402 x402-verifier -o yaml shows both _by_offer_chain and _by_offer_chain_asset_symbol rules

@bussyjd
bussyjd changed the base branch from main to fix/prometheus-rule-increase-7dMay 24, 2026 06:48
Currently the verifier emits (offer_namespace, offer_name, chain).
Answering "what's my OBOL revenue?" requires joining metrics with
the ServiceOffer CR's spec.payment.asset.symbol at the frontend.
With asset_symbol on the label set, the answer is a direct PromQL
aggregation.
Cardinality cost: zero. Each offer pins exactly one asset (A=1
per offer), so the new dimension is functionally constant within
the existing (ns, name) group — no series multiplication. The
"don't label what you can derive" guidance exists to prevent
*multiplicative* blowups (chain x pod x pod_owner style); the
single-asset-per-offer invariant means there's no multiplication
to prevent.
The argument for adding asset_symbol is identical to the argument
that already justifies `chain` on these vecs: both are
CR-derived, both are query-meaningful, both have bounded values.
Changes:
- 6 metric vecs: label slice gains "asset_symbol"
- pruneSeriesNotIn key now (ns, name, chain, asset_symbol) so
asset-repin doesn't leak the old series
- verifier.load() live-set built with the same 4-tuple
- prometheusLabels() emits rule.AssetSymbol (or "unknown" if
empty as defensive fallback)
- New _asset_symbol-suffixed recording rules added side-by-side
with existing rules; existing rules unchanged (non-breaking)
- Tests: emission asserts asset_symbol; prune test asserts
asset-repin doesn't leak
Frontend can simplify the existing metric x CR join in a future PR
once it migrates to the _asset_symbol-suffixed rule.
Findings from: plans/integration-test-L7-paid-flow-20260524.md
(OBOL parity smoke surfaced this as a real gap when validating
the WalletStrip / EarningsStrip per-token columns).
@bussyjd

Copy link
Copy Markdown
ContributorAuthor

Superseded by bundle PR #536 — closing in favor of the consolidated merge target. Original branch and history preserved.

@bussyjdbussyjd closed this May 24, 2026
@OisinKyne
OisinKyne deleted the feat/x402-asset-symbol-label branch July 1, 2026 12:33
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.

1 participant

@bussyjd
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(x402-metrics): add asset_symbol label for per-token queries - #531

Closed
bussyjd wants to merge 1 commit into
fix/prometheus-rule-increase-7dfrom
feat/x402-asset-symbol-label
Closed

feat(x402-metrics): add asset_symbol label for per-token queries#531
bussyjd wants to merge 1 commit into
fix/prometheus-rule-increase-7dfrom
feat/x402-asset-symbol-label

Conversation

@bussyjd

Copy link
Copy Markdown
Contributor

Summary

Adds asset_symbol to the 6 verifier metric vecs so per-token revenue is queryable in PromQL directly, without the frontend having to join metrics with the ServiceOffer CR's spec.payment.asset.symbol at query time.

Stack note: this is technically branched off fix/prom-retention-window-alignment (PR A) because that PR introduces pruneSeriesNotIn, the lastPaymentSuccess gauge, and x402-prometheus-rules.yaml — all of which this PR extends. Listed merge target is main per the stacked-PR convention; once PR A lands, this should rebase cleanly. If reviewing before PR A merges, set the GitHub base to fix/prom-retention-window-alignment to see only the asset-symbol diff.

Cardinality math (no series multiplication)

Each ServiceOffer pins exactly one asset via spec.payment.asset.symbol. That means within the existing (offer_namespace, offer_name) group, asset_symbol is functionally constant — adding it does not multiply series count.

Per-vec series count before: 6 vecs * N offers * C chains-per-offer
Per-vec series count after: 6 vecs * N offers * C chains-per-offer * 1 asset-per-offer

The don't label what you can derive heuristic exists to prevent multiplicative blowups (chain x pod x pod_owner style). The single-asset-per-offer invariant means there's no multiplication to prevent — the same internal-consistency argument that already justifies chain (also CR-derived) on these vecs applies to asset_symbol.

Current test cluster (N=3 offers):demo-hello, silvermesh-paid, silvermesh-obol -> +3 series per vec, max +18 series total cluster-wide. Negligible.

Before / after PromQL

Before (frontend joins charged-requests metric with CR spec.payment.asset.symbol in JS):

sum by (offer_namespace, offer_name, chain) (
increase(obol_x402_verifier_charged_requests_total[7d])
)
# then JS-side: foreach series, look up asset_symbol via /api/sell/list -> CR

After (direct):

sum by (asset_symbol) (x402:revenue:7d_by_offer_chain_asset_symbol)
# -> { asset_symbol="USDC" } 4231
# -> { asset_symbol="OBOL" } 88

Recording-rule migration plan

Non-breaking. Existing rules stay in place for one release:

  • x402:revenue:24h_by_offer_chain -> kept (frontend currently reads this)
  • x402:revenue:24h_by_offer_chain_asset_symbol -> new sibling
  • x402:revenue:7d_by_offer_chain -> kept
  • x402:revenue:7d_by_offer_chain_asset_symbol -> new sibling

Frontend can migrate to the _asset_symbol rules in a follow-up PR; the old rules continue to produce the same series (adding a label to the raw counter is non-breaking for queries that aggregate it away).

x402:settlement_rate:1h_by_offer_chain is intentionally not duplicated — it's a ratio; the asset wouldn't change the answer meaningfully.

Changes

  • internal/x402/metrics.go -> 6 vecs gain asset_symbol; pruneSeriesNotIn key now 4-tuple
  • internal/x402/verifier.go -> load() live-set uses 4-tuple; prometheusLabels() emits rule.AssetSymbol with "unknown" defensive fallback
  • internal/embed/infrastructure/base/templates/x402-prometheus-rules.yaml -> two new _asset_symbol-suffixed recording rules
  • internal/x402/verifier_test.go -> existing label-map assertions updated; 3 new tests added

Test plan

  • go build ./... clean
  • go vet ./internal/x402/... ./internal/embed/... clean
  • go test ./internal/x402/... green (all 6 vecs + new tests pass)
  • go test ./internal/embed/... green (CRD/manifest tests no regression)
  • Existing TestVerifier_Reload_PrunesDeletedOfferSeries from PR A still passes — asset-symbol-aware key doesn't break basic GC
  • TestVerifier_PrometheusLabels_IncludesAssetSymbol -> asserts USDC is emitted
  • TestVerifier_PrometheusLabels_DefaultsToUnknownIfEmpty -> asserts "unknown" fallback when AssetSymbol is empty
  • TestVerifier_PruneSeriesNotIn_DistinguishesAssetSymbol -> dropping USDC route while keeping OBOL route on the same offer leaves OBOL series alive (catches asset-repin leaks)
  • Live-cluster verification: scrape obol_x402_verifier_charged_requests_total{asset_symbol="OBOL"} after a paid OBOL request to silvermesh-obol returns 1
  • Recording rule rendered through helmfile: kubectl get prometheusrule -n x402 x402-verifier -o yaml shows both _by_offer_chain and _by_offer_chain_asset_symbol rules

@bussyjd
bussyjd changed the base branch from main to fix/prometheus-rule-increase-7dMay 24, 2026 06:48
Currently the verifier emits (offer_namespace, offer_name, chain).
Answering "what's my OBOL revenue?" requires joining metrics with
the ServiceOffer CR's spec.payment.asset.symbol at the frontend.
With asset_symbol on the label set, the answer is a direct PromQL
aggregation.
Cardinality cost: zero. Each offer pins exactly one asset (A=1
per offer), so the new dimension is functionally constant within
the existing (ns, name) group — no series multiplication. The
"don't label what you can derive" guidance exists to prevent
*multiplicative* blowups (chain x pod x pod_owner style); the
single-asset-per-offer invariant means there's no multiplication
to prevent.
The argument for adding asset_symbol is identical to the argument
that already justifies `chain` on these vecs: both are
CR-derived, both are query-meaningful, both have bounded values.
Changes:
- 6 metric vecs: label slice gains "asset_symbol"
- pruneSeriesNotIn key now (ns, name, chain, asset_symbol) so
asset-repin doesn't leak the old series
- verifier.load() live-set built with the same 4-tuple
- prometheusLabels() emits rule.AssetSymbol (or "unknown" if
empty as defensive fallback)
- New _asset_symbol-suffixed recording rules added side-by-side
with existing rules; existing rules unchanged (non-breaking)
- Tests: emission asserts asset_symbol; prune test asserts
asset-repin doesn't leak
Frontend can simplify the existing metric x CR join in a future PR
once it migrates to the _asset_symbol-suffixed rule.
Findings from: plans/integration-test-L7-paid-flow-20260524.md
(OBOL parity smoke surfaced this as a real gap when validating
the WalletStrip / EarningsStrip per-token columns).
@bussyjd

Copy link
Copy Markdown
ContributorAuthor

Superseded by bundle PR #536 — closing in favor of the consolidated merge target. Original branch and history preserved.

@bussyjdbussyjd closed this May 24, 2026
@OisinKyne
OisinKyne deleted the feat/x402-asset-symbol-label branch July 1, 2026 12:33
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.

1 participant

@bussyjd
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

feat(x402-metrics): add asset_symbol label for per-token queries - #531

Closed
bussyjd wants to merge 1 commit into
fix/prometheus-rule-increase-7dfrom
feat/x402-asset-symbol-label
Closed

feat(x402-metrics): add asset_symbol label for per-token queries#531
bussyjd wants to merge 1 commit into
fix/prometheus-rule-increase-7dfrom
feat/x402-asset-symbol-label

Conversation

@bussyjd

Copy link
Copy Markdown
Contributor

Summary

Adds asset_symbol to the 6 verifier metric vecs so per-token revenue is queryable in PromQL directly, without the frontend having to join metrics with the ServiceOffer CR's spec.payment.asset.symbol at query time.

Stack note: this is technically branched off fix/prom-retention-window-alignment (PR A) because that PR introduces pruneSeriesNotIn, the lastPaymentSuccess gauge, and x402-prometheus-rules.yaml — all of which this PR extends. Listed merge target is main per the stacked-PR convention; once PR A lands, this should rebase cleanly. If reviewing before PR A merges, set the GitHub base to fix/prom-retention-window-alignment to see only the asset-symbol diff.

Cardinality math (no series multiplication)

Each ServiceOffer pins exactly one asset via spec.payment.asset.symbol. That means within the existing (offer_namespace, offer_name) group, asset_symbol is functionally constant — adding it does not multiply series count.

Per-vec series count before: 6 vecs * N offers * C chains-per-offer
Per-vec series count after: 6 vecs * N offers * C chains-per-offer * 1 asset-per-offer

The don't label what you can derive heuristic exists to prevent multiplicative blowups (chain x pod x pod_owner style). The single-asset-per-offer invariant means there's no multiplication to prevent — the same internal-consistency argument that already justifies chain (also CR-derived) on these vecs applies to asset_symbol.

Current test cluster (N=3 offers):demo-hello, silvermesh-paid, silvermesh-obol -> +3 series per vec, max +18 series total cluster-wide. Negligible.

Before / after PromQL

Before (frontend joins charged-requests metric with CR spec.payment.asset.symbol in JS):

sum by (offer_namespace, offer_name, chain) (
increase(obol_x402_verifier_charged_requests_total[7d])
)
# then JS-side: foreach series, look up asset_symbol via /api/sell/list -> CR

After (direct):

sum by (asset_symbol) (x402:revenue:7d_by_offer_chain_asset_symbol)
# -> { asset_symbol="USDC" } 4231
# -> { asset_symbol="OBOL" } 88

Recording-rule migration plan

Non-breaking. Existing rules stay in place for one release:

  • x402:revenue:24h_by_offer_chain -> kept (frontend currently reads this)
  • x402:revenue:24h_by_offer_chain_asset_symbol -> new sibling
  • x402:revenue:7d_by_offer_chain -> kept
  • x402:revenue:7d_by_offer_chain_asset_symbol -> new sibling

Frontend can migrate to the _asset_symbol rules in a follow-up PR; the old rules continue to produce the same series (adding a label to the raw counter is non-breaking for queries that aggregate it away).

x402:settlement_rate:1h_by_offer_chain is intentionally not duplicated — it's a ratio; the asset wouldn't change the answer meaningfully.

Changes

  • internal/x402/metrics.go -> 6 vecs gain asset_symbol; pruneSeriesNotIn key now 4-tuple
  • internal/x402/verifier.go -> load() live-set uses 4-tuple; prometheusLabels() emits rule.AssetSymbol with "unknown" defensive fallback
  • internal/embed/infrastructure/base/templates/x402-prometheus-rules.yaml -> two new _asset_symbol-suffixed recording rules
  • internal/x402/verifier_test.go -> existing label-map assertions updated; 3 new tests added

Test plan

  • go build ./... clean
  • go vet ./internal/x402/... ./internal/embed/... clean
  • go test ./internal/x402/... green (all 6 vecs + new tests pass)
  • go test ./internal/embed/... green (CRD/manifest tests no regression)
  • Existing TestVerifier_Reload_PrunesDeletedOfferSeries from PR A still passes — asset-symbol-aware key doesn't break basic GC
  • TestVerifier_PrometheusLabels_IncludesAssetSymbol -> asserts USDC is emitted
  • TestVerifier_PrometheusLabels_DefaultsToUnknownIfEmpty -> asserts "unknown" fallback when AssetSymbol is empty
  • TestVerifier_PruneSeriesNotIn_DistinguishesAssetSymbol -> dropping USDC route while keeping OBOL route on the same offer leaves OBOL series alive (catches asset-repin leaks)
  • Live-cluster verification: scrape obol_x402_verifier_charged_requests_total{asset_symbol="OBOL"} after a paid OBOL request to silvermesh-obol returns 1
  • Recording rule rendered through helmfile: kubectl get prometheusrule -n x402 x402-verifier -o yaml shows both _by_offer_chain and _by_offer_chain_asset_symbol rules

@bussyjd
bussyjd changed the base branch from main to fix/prometheus-rule-increase-7dMay 24, 2026 06:48
Currently the verifier emits (offer_namespace, offer_name, chain).
Answering "what's my OBOL revenue?" requires joining metrics with
the ServiceOffer CR's spec.payment.asset.symbol at the frontend.
With asset_symbol on the label set, the answer is a direct PromQL
aggregation.
Cardinality cost: zero. Each offer pins exactly one asset (A=1
per offer), so the new dimension is functionally constant within
the existing (ns, name) group — no series multiplication. The
"don't label what you can derive" guidance exists to prevent
*multiplicative* blowups (chain x pod x pod_owner style); the
single-asset-per-offer invariant means there's no multiplication
to prevent.
The argument for adding asset_symbol is identical to the argument
that already justifies `chain` on these vecs: both are
CR-derived, both are query-meaningful, both have bounded values.
Changes:
- 6 metric vecs: label slice gains "asset_symbol"
- pruneSeriesNotIn key now (ns, name, chain, asset_symbol) so
asset-repin doesn't leak the old series
- verifier.load() live-set built with the same 4-tuple
- prometheusLabels() emits rule.AssetSymbol (or "unknown" if
empty as defensive fallback)
- New _asset_symbol-suffixed recording rules added side-by-side
with existing rules; existing rules unchanged (non-breaking)
- Tests: emission asserts asset_symbol; prune test asserts
asset-repin doesn't leak
Frontend can simplify the existing metric x CR join in a future PR
once it migrates to the _asset_symbol-suffixed rule.
Findings from: plans/integration-test-L7-paid-flow-20260524.md
(OBOL parity smoke surfaced this as a real gap when validating
the WalletStrip / EarningsStrip per-token columns).
@bussyjd

Copy link
Copy Markdown
ContributorAuthor

Superseded by bundle PR #536 — closing in favor of the consolidated merge target. Original branch and history preserved.

@bussyjdbussyjd closed this May 24, 2026
@OisinKyne
OisinKyne deleted the feat/x402-asset-symbol-label branch July 1, 2026 12:33
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.

1 participant

@bussyjd
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(x402-metrics): add asset_symbol label for per-token queries - #531

Closed
bussyjd wants to merge 1 commit into
fix/prometheus-rule-increase-7dfrom
feat/x402-asset-symbol-label
Closed

feat(x402-metrics): add asset_symbol label for per-token queries#531
bussyjd wants to merge 1 commit into
fix/prometheus-rule-increase-7dfrom
feat/x402-asset-symbol-label

Conversation

@bussyjd

Copy link
Copy Markdown
Contributor

Summary

Adds asset_symbol to the 6 verifier metric vecs so per-token revenue is queryable in PromQL directly, without the frontend having to join metrics with the ServiceOffer CR's spec.payment.asset.symbol at query time.

Stack note: this is technically branched off fix/prom-retention-window-alignment (PR A) because that PR introduces pruneSeriesNotIn, the lastPaymentSuccess gauge, and x402-prometheus-rules.yaml — all of which this PR extends. Listed merge target is main per the stacked-PR convention; once PR A lands, this should rebase cleanly. If reviewing before PR A merges, set the GitHub base to fix/prom-retention-window-alignment to see only the asset-symbol diff.

Cardinality math (no series multiplication)

Each ServiceOffer pins exactly one asset via spec.payment.asset.symbol. That means within the existing (offer_namespace, offer_name) group, asset_symbol is functionally constant — adding it does not multiply series count.

Per-vec series count before: 6 vecs * N offers * C chains-per-offer
Per-vec series count after: 6 vecs * N offers * C chains-per-offer * 1 asset-per-offer

The don't label what you can derive heuristic exists to prevent multiplicative blowups (chain x pod x pod_owner style). The single-asset-per-offer invariant means there's no multiplication to prevent — the same internal-consistency argument that already justifies chain (also CR-derived) on these vecs applies to asset_symbol.

Current test cluster (N=3 offers):demo-hello, silvermesh-paid, silvermesh-obol -> +3 series per vec, max +18 series total cluster-wide. Negligible.

Before / after PromQL

Before (frontend joins charged-requests metric with CR spec.payment.asset.symbol in JS):

sum by (offer_namespace, offer_name, chain) (
increase(obol_x402_verifier_charged_requests_total[7d])
)
# then JS-side: foreach series, look up asset_symbol via /api/sell/list -> CR

After (direct):

sum by (asset_symbol) (x402:revenue:7d_by_offer_chain_asset_symbol)
# -> { asset_symbol="USDC" } 4231
# -> { asset_symbol="OBOL" } 88

Recording-rule migration plan

Non-breaking. Existing rules stay in place for one release:

  • x402:revenue:24h_by_offer_chain -> kept (frontend currently reads this)
  • x402:revenue:24h_by_offer_chain_asset_symbol -> new sibling
  • x402:revenue:7d_by_offer_chain -> kept
  • x402:revenue:7d_by_offer_chain_asset_symbol -> new sibling

Frontend can migrate to the _asset_symbol rules in a follow-up PR; the old rules continue to produce the same series (adding a label to the raw counter is non-breaking for queries that aggregate it away).

x402:settlement_rate:1h_by_offer_chain is intentionally not duplicated — it's a ratio; the asset wouldn't change the answer meaningfully.

Changes

  • internal/x402/metrics.go -> 6 vecs gain asset_symbol; pruneSeriesNotIn key now 4-tuple
  • internal/x402/verifier.go -> load() live-set uses 4-tuple; prometheusLabels() emits rule.AssetSymbol with "unknown" defensive fallback
  • internal/embed/infrastructure/base/templates/x402-prometheus-rules.yaml -> two new _asset_symbol-suffixed recording rules
  • internal/x402/verifier_test.go -> existing label-map assertions updated; 3 new tests added

Test plan

  • go build ./... clean
  • go vet ./internal/x402/... ./internal/embed/... clean
  • go test ./internal/x402/... green (all 6 vecs + new tests pass)
  • go test ./internal/embed/... green (CRD/manifest tests no regression)
  • Existing TestVerifier_Reload_PrunesDeletedOfferSeries from PR A still passes — asset-symbol-aware key doesn't break basic GC
  • TestVerifier_PrometheusLabels_IncludesAssetSymbol -> asserts USDC is emitted
  • TestVerifier_PrometheusLabels_DefaultsToUnknownIfEmpty -> asserts "unknown" fallback when AssetSymbol is empty
  • TestVerifier_PruneSeriesNotIn_DistinguishesAssetSymbol -> dropping USDC route while keeping OBOL route on the same offer leaves OBOL series alive (catches asset-repin leaks)
  • Live-cluster verification: scrape obol_x402_verifier_charged_requests_total{asset_symbol="OBOL"} after a paid OBOL request to silvermesh-obol returns 1
  • Recording rule rendered through helmfile: kubectl get prometheusrule -n x402 x402-verifier -o yaml shows both _by_offer_chain and _by_offer_chain_asset_symbol rules

@bussyjd
bussyjd changed the base branch from main to fix/prometheus-rule-increase-7dMay 24, 2026 06:48
Currently the verifier emits (offer_namespace, offer_name, chain).
Answering "what's my OBOL revenue?" requires joining metrics with
the ServiceOffer CR's spec.payment.asset.symbol at the frontend.
With asset_symbol on the label set, the answer is a direct PromQL
aggregation.
Cardinality cost: zero. Each offer pins exactly one asset (A=1
per offer), so the new dimension is functionally constant within
the existing (ns, name) group — no series multiplication. The
"don't label what you can derive" guidance exists to prevent
*multiplicative* blowups (chain x pod x pod_owner style); the
single-asset-per-offer invariant means there's no multiplication
to prevent.
The argument for adding asset_symbol is identical to the argument
that already justifies `chain` on these vecs: both are
CR-derived, both are query-meaningful, both have bounded values.
Changes:
- 6 metric vecs: label slice gains "asset_symbol"
- pruneSeriesNotIn key now (ns, name, chain, asset_symbol) so
asset-repin doesn't leak the old series
- verifier.load() live-set built with the same 4-tuple
- prometheusLabels() emits rule.AssetSymbol (or "unknown" if
empty as defensive fallback)
- New _asset_symbol-suffixed recording rules added side-by-side
with existing rules; existing rules unchanged (non-breaking)
- Tests: emission asserts asset_symbol; prune test asserts
asset-repin doesn't leak
Frontend can simplify the existing metric x CR join in a future PR
once it migrates to the _asset_symbol-suffixed rule.
Findings from: plans/integration-test-L7-paid-flow-20260524.md
(OBOL parity smoke surfaced this as a real gap when validating
the WalletStrip / EarningsStrip per-token columns).
@bussyjd

Copy link
Copy Markdown
ContributorAuthor

Superseded by bundle PR #536 — closing in favor of the consolidated merge target. Original branch and history preserved.

@bussyjdbussyjd closed this May 24, 2026
@OisinKyne
OisinKyne deleted the feat/x402-asset-symbol-label branch July 1, 2026 12:33
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.

1 participant

@bussyjd
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(x402-metrics): add asset_symbol label for per-token queries - #531

Closed
bussyjd wants to merge 1 commit into
fix/prometheus-rule-increase-7dfrom
feat/x402-asset-symbol-label
Closed

feat(x402-metrics): add asset_symbol label for per-token queries#531
bussyjd wants to merge 1 commit into
fix/prometheus-rule-increase-7dfrom
feat/x402-asset-symbol-label

Conversation

@bussyjd

Copy link
Copy Markdown
Contributor

Summary

Adds asset_symbol to the 6 verifier metric vecs so per-token revenue is queryable in PromQL directly, without the frontend having to join metrics with the ServiceOffer CR's spec.payment.asset.symbol at query time.

Stack note: this is technically branched off fix/prom-retention-window-alignment (PR A) because that PR introduces pruneSeriesNotIn, the lastPaymentSuccess gauge, and x402-prometheus-rules.yaml — all of which this PR extends. Listed merge target is main per the stacked-PR convention; once PR A lands, this should rebase cleanly. If reviewing before PR A merges, set the GitHub base to fix/prom-retention-window-alignment to see only the asset-symbol diff.

Cardinality math (no series multiplication)

Each ServiceOffer pins exactly one asset via spec.payment.asset.symbol. That means within the existing (offer_namespace, offer_name) group, asset_symbol is functionally constant — adding it does not multiply series count.

Per-vec series count before: 6 vecs * N offers * C chains-per-offer
Per-vec series count after: 6 vecs * N offers * C chains-per-offer * 1 asset-per-offer

The don't label what you can derive heuristic exists to prevent multiplicative blowups (chain x pod x pod_owner style). The single-asset-per-offer invariant means there's no multiplication to prevent — the same internal-consistency argument that already justifies chain (also CR-derived) on these vecs applies to asset_symbol.

Current test cluster (N=3 offers):demo-hello, silvermesh-paid, silvermesh-obol -> +3 series per vec, max +18 series total cluster-wide. Negligible.

Before / after PromQL

Before (frontend joins charged-requests metric with CR spec.payment.asset.symbol in JS):

sum by (offer_namespace, offer_name, chain) (
increase(obol_x402_verifier_charged_requests_total[7d])
)
# then JS-side: foreach series, look up asset_symbol via /api/sell/list -> CR

After (direct):

sum by (asset_symbol) (x402:revenue:7d_by_offer_chain_asset_symbol)
# -> { asset_symbol="USDC" } 4231
# -> { asset_symbol="OBOL" } 88

Recording-rule migration plan

Non-breaking. Existing rules stay in place for one release:

  • x402:revenue:24h_by_offer_chain -> kept (frontend currently reads this)
  • x402:revenue:24h_by_offer_chain_asset_symbol -> new sibling
  • x402:revenue:7d_by_offer_chain -> kept
  • x402:revenue:7d_by_offer_chain_asset_symbol -> new sibling

Frontend can migrate to the _asset_symbol rules in a follow-up PR; the old rules continue to produce the same series (adding a label to the raw counter is non-breaking for queries that aggregate it away).

x402:settlement_rate:1h_by_offer_chain is intentionally not duplicated — it's a ratio; the asset wouldn't change the answer meaningfully.

Changes

  • internal/x402/metrics.go -> 6 vecs gain asset_symbol; pruneSeriesNotIn key now 4-tuple
  • internal/x402/verifier.go -> load() live-set uses 4-tuple; prometheusLabels() emits rule.AssetSymbol with "unknown" defensive fallback
  • internal/embed/infrastructure/base/templates/x402-prometheus-rules.yaml -> two new _asset_symbol-suffixed recording rules
  • internal/x402/verifier_test.go -> existing label-map assertions updated; 3 new tests added

Test plan

  • go build ./... clean
  • go vet ./internal/x402/... ./internal/embed/... clean
  • go test ./internal/x402/... green (all 6 vecs + new tests pass)
  • go test ./internal/embed/... green (CRD/manifest tests no regression)
  • Existing TestVerifier_Reload_PrunesDeletedOfferSeries from PR A still passes — asset-symbol-aware key doesn't break basic GC
  • TestVerifier_PrometheusLabels_IncludesAssetSymbol -> asserts USDC is emitted
  • TestVerifier_PrometheusLabels_DefaultsToUnknownIfEmpty -> asserts "unknown" fallback when AssetSymbol is empty
  • TestVerifier_PruneSeriesNotIn_DistinguishesAssetSymbol -> dropping USDC route while keeping OBOL route on the same offer leaves OBOL series alive (catches asset-repin leaks)
  • Live-cluster verification: scrape obol_x402_verifier_charged_requests_total{asset_symbol="OBOL"} after a paid OBOL request to silvermesh-obol returns 1
  • Recording rule rendered through helmfile: kubectl get prometheusrule -n x402 x402-verifier -o yaml shows both _by_offer_chain and _by_offer_chain_asset_symbol rules

@bussyjd
bussyjd changed the base branch from main to fix/prometheus-rule-increase-7dMay 24, 2026 06:48
Currently the verifier emits (offer_namespace, offer_name, chain).
Answering "what's my OBOL revenue?" requires joining metrics with
the ServiceOffer CR's spec.payment.asset.symbol at the frontend.
With asset_symbol on the label set, the answer is a direct PromQL
aggregation.
Cardinality cost: zero. Each offer pins exactly one asset (A=1
per offer), so the new dimension is functionally constant within
the existing (ns, name) group — no series multiplication. The
"don't label what you can derive" guidance exists to prevent
*multiplicative* blowups (chain x pod x pod_owner style); the
single-asset-per-offer invariant means there's no multiplication
to prevent.
The argument for adding asset_symbol is identical to the argument
that already justifies `chain` on these vecs: both are
CR-derived, both are query-meaningful, both have bounded values.
Changes:
- 6 metric vecs: label slice gains "asset_symbol"
- pruneSeriesNotIn key now (ns, name, chain, asset_symbol) so
asset-repin doesn't leak the old series
- verifier.load() live-set built with the same 4-tuple
- prometheusLabels() emits rule.AssetSymbol (or "unknown" if
empty as defensive fallback)
- New _asset_symbol-suffixed recording rules added side-by-side
with existing rules; existing rules unchanged (non-breaking)
- Tests: emission asserts asset_symbol; prune test asserts
asset-repin doesn't leak
Frontend can simplify the existing metric x CR join in a future PR
once it migrates to the _asset_symbol-suffixed rule.
Findings from: plans/integration-test-L7-paid-flow-20260524.md
(OBOL parity smoke surfaced this as a real gap when validating
the WalletStrip / EarningsStrip per-token columns).
@bussyjd

Copy link
Copy Markdown
ContributorAuthor

Superseded by bundle PR #536 — closing in favor of the consolidated merge target. Original branch and history preserved.

@bussyjdbussyjd closed this May 24, 2026
@OisinKyne
OisinKyne deleted the feat/x402-asset-symbol-label branch July 1, 2026 12:33
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.

1 participant

@bussyjd
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

feat(x402-metrics): add asset_symbol label for per-token queries - #531

Closed
bussyjd wants to merge 1 commit into
fix/prometheus-rule-increase-7dfrom
feat/x402-asset-symbol-label
Closed

feat(x402-metrics): add asset_symbol label for per-token queries#531
bussyjd wants to merge 1 commit into
fix/prometheus-rule-increase-7dfrom
feat/x402-asset-symbol-label

Conversation

@bussyjd

Copy link
Copy Markdown
Contributor

Summary

Adds asset_symbol to the 6 verifier metric vecs so per-token revenue is queryable in PromQL directly, without the frontend having to join metrics with the ServiceOffer CR's spec.payment.asset.symbol at query time.

Stack note: this is technically branched off fix/prom-retention-window-alignment (PR A) because that PR introduces pruneSeriesNotIn, the lastPaymentSuccess gauge, and x402-prometheus-rules.yaml — all of which this PR extends. Listed merge target is main per the stacked-PR convention; once PR A lands, this should rebase cleanly. If reviewing before PR A merges, set the GitHub base to fix/prom-retention-window-alignment to see only the asset-symbol diff.

Cardinality math (no series multiplication)

Each ServiceOffer pins exactly one asset via spec.payment.asset.symbol. That means within the existing (offer_namespace, offer_name) group, asset_symbol is functionally constant — adding it does not multiply series count.

Per-vec series count before: 6 vecs * N offers * C chains-per-offer
Per-vec series count after: 6 vecs * N offers * C chains-per-offer * 1 asset-per-offer

The don't label what you can derive heuristic exists to prevent multiplicative blowups (chain x pod x pod_owner style). The single-asset-per-offer invariant means there's no multiplication to prevent — the same internal-consistency argument that already justifies chain (also CR-derived) on these vecs applies to asset_symbol.

Current test cluster (N=3 offers):demo-hello, silvermesh-paid, silvermesh-obol -> +3 series per vec, max +18 series total cluster-wide. Negligible.

Before / after PromQL

Before (frontend joins charged-requests metric with CR spec.payment.asset.symbol in JS):

sum by (offer_namespace, offer_name, chain) (
increase(obol_x402_verifier_charged_requests_total[7d])
)
# then JS-side: foreach series, look up asset_symbol via /api/sell/list -> CR

After (direct):

sum by (asset_symbol) (x402:revenue:7d_by_offer_chain_asset_symbol)
# -> { asset_symbol="USDC" } 4231
# -> { asset_symbol="OBOL" } 88

Recording-rule migration plan

Non-breaking. Existing rules stay in place for one release:

  • x402:revenue:24h_by_offer_chain -> kept (frontend currently reads this)
  • x402:revenue:24h_by_offer_chain_asset_symbol -> new sibling
  • x402:revenue:7d_by_offer_chain -> kept
  • x402:revenue:7d_by_offer_chain_asset_symbol -> new sibling

Frontend can migrate to the _asset_symbol rules in a follow-up PR; the old rules continue to produce the same series (adding a label to the raw counter is non-breaking for queries that aggregate it away).

x402:settlement_rate:1h_by_offer_chain is intentionally not duplicated — it's a ratio; the asset wouldn't change the answer meaningfully.

Changes

  • internal/x402/metrics.go -> 6 vecs gain asset_symbol; pruneSeriesNotIn key now 4-tuple
  • internal/x402/verifier.go -> load() live-set uses 4-tuple; prometheusLabels() emits rule.AssetSymbol with "unknown" defensive fallback
  • internal/embed/infrastructure/base/templates/x402-prometheus-rules.yaml -> two new _asset_symbol-suffixed recording rules
  • internal/x402/verifier_test.go -> existing label-map assertions updated; 3 new tests added

Test plan

  • go build ./... clean
  • go vet ./internal/x402/... ./internal/embed/... clean
  • go test ./internal/x402/... green (all 6 vecs + new tests pass)
  • go test ./internal/embed/... green (CRD/manifest tests no regression)
  • Existing TestVerifier_Reload_PrunesDeletedOfferSeries from PR A still passes — asset-symbol-aware key doesn't break basic GC
  • TestVerifier_PrometheusLabels_IncludesAssetSymbol -> asserts USDC is emitted
  • TestVerifier_PrometheusLabels_DefaultsToUnknownIfEmpty -> asserts "unknown" fallback when AssetSymbol is empty
  • TestVerifier_PruneSeriesNotIn_DistinguishesAssetSymbol -> dropping USDC route while keeping OBOL route on the same offer leaves OBOL series alive (catches asset-repin leaks)
  • Live-cluster verification: scrape obol_x402_verifier_charged_requests_total{asset_symbol="OBOL"} after a paid OBOL request to silvermesh-obol returns 1
  • Recording rule rendered through helmfile: kubectl get prometheusrule -n x402 x402-verifier -o yaml shows both _by_offer_chain and _by_offer_chain_asset_symbol rules

@bussyjd
bussyjd changed the base branch from main to fix/prometheus-rule-increase-7dMay 24, 2026 06:48
Currently the verifier emits (offer_namespace, offer_name, chain).
Answering "what's my OBOL revenue?" requires joining metrics with
the ServiceOffer CR's spec.payment.asset.symbol at the frontend.
With asset_symbol on the label set, the answer is a direct PromQL
aggregation.
Cardinality cost: zero. Each offer pins exactly one asset (A=1
per offer), so the new dimension is functionally constant within
the existing (ns, name) group — no series multiplication. The
"don't label what you can derive" guidance exists to prevent
*multiplicative* blowups (chain x pod x pod_owner style); the
single-asset-per-offer invariant means there's no multiplication
to prevent.
The argument for adding asset_symbol is identical to the argument
that already justifies `chain` on these vecs: both are
CR-derived, both are query-meaningful, both have bounded values.
Changes:
- 6 metric vecs: label slice gains "asset_symbol"
- pruneSeriesNotIn key now (ns, name, chain, asset_symbol) so
asset-repin doesn't leak the old series
- verifier.load() live-set built with the same 4-tuple
- prometheusLabels() emits rule.AssetSymbol (or "unknown" if
empty as defensive fallback)
- New _asset_symbol-suffixed recording rules added side-by-side
with existing rules; existing rules unchanged (non-breaking)
- Tests: emission asserts asset_symbol; prune test asserts
asset-repin doesn't leak
Frontend can simplify the existing metric x CR join in a future PR
once it migrates to the _asset_symbol-suffixed rule.
Findings from: plans/integration-test-L7-paid-flow-20260524.md
(OBOL parity smoke surfaced this as a real gap when validating
the WalletStrip / EarningsStrip per-token columns).
@bussyjd

Copy link
Copy Markdown
ContributorAuthor

Superseded by bundle PR #536 — closing in favor of the consolidated merge target. Original branch and history preserved.

@bussyjdbussyjd closed this May 24, 2026
@OisinKyne
OisinKyne deleted the feat/x402-asset-symbol-label branch July 1, 2026 12:33
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.

1 participant

@bussyjd