x402-demand / docs /PROCESS.md
Priceman614's picture
x402-demand v1: listing-only checkpoint, card, figures, metrics, process doc, code
8bf7463 verified
|
Raw History Blame Contribute Delete
11.6 kB

x402 Demand β€” how the model was built

This is the full record of how the x402-demand checkpoints were made: the source data, the label and why it is shaped that way, the splits, the baselines a checkpoint had to beat, the training recipe, the results, and what was published. Written so the run can be repeated, extended, or challenged.

Owner: Ryan Price (Spark Intelligence Station). Started 2026-09-23.


0. Publication policy

Artifact Published? Where
Checkpoints (weights, tokenizer, config) yes HF model repo
Metrics (results/*.json) and figures (results/*.png) yes HF model card
Demo app yes HF Space
This document, the training/eval code on the model repo (as docs), no public git β€”
The dataset (snapshots, data/rows.parquet, per-row predictions) NO stays on the Spark box

The dataset is deliberately withheld. The daily snapshots are the lab's own longitudinal record of the CDP Bazaar and are the durable asset; the model card describes the data in aggregate only. data/ and runs/ are gitignored so nothing row-level can be committed by accident. The figures show aggregate rates and scores, never rows. The demo's example listings are invented, not drawn from the data.


1. Source data

  • What: the Coinbase CDP x402 discovery index (the "Bazaar"), GET https://api.cdp.coinbase.com/platform/v2/x402/discovery/resources, fetched once a day, fully paginated (1,000/page), by the lab's Scout:Bazaar (spark-lab/agents/scouts.py). Each daily file is vault/raw/bazaar/YYYY-MM-DD.json; this project reads only its cdp list.
  • Per listing: resource (URL), type, description, price (USDC atomic units, 6 decimals), network, lastUpdated, and quality = {l30DaysTotalCalls, l30DaysUniquePayers, lastCalledAt}.
  • Window used for v1: 31 snapshots, 2026-08-24 β†’ 2026-09-23. 14.2k-16.2k listings per day; 26,642 distinct resources seen; 2,464 hosts in the modelled rows.
  • Completeness check: daily counts are stable (no truncated pages). 1,297 resources (4.9%) disappear and reappear β€” "flicker". The label below is built from lastCalledAt timestamps rather than from presence, so it is robust to that.

The fact that shaped the label

The index evicts a listing exactly 30 days after its quality.lastCalledAt (measured to the second on the lab's own listing on 2026-09-12; lastUpdated does not matter). So "will this listing still be in the index in N days?" is mostly already decided by a timestamp we can read: anything called in the last 30-N days survives by rule. A model trained on survival would mostly learn that arithmetic.

2. The label

paid_Hd = 1 iff the listing receives at least one paid call in the H days after the anchor snapshot, i.e. some later snapshot shows lastCalledAt ∈ (ref_t, ref_t + H].

  • ref_t is the snapshot's reference time = the newest lastCalledAt in it (β‰ˆ fetch time).
  • A listing that is evicted, or stays listed without a new call, is 0.
  • H ∈ {7, 14}. A horizon is labelled only if the whole window (+1 day of slack) is observed.
  • Result: 330,035 labelled rows (paid_7d, anchors 0-21), base rate 0.240; 241,825 rows for paid_14d (anchors 0-15), base rate 0.324. In words: only a quarter of listed x402 services get paid even once in a week.

What the label is not: it is not "is this a legitimate service". A self-call to keep a listing alive counts as a paid call. See Β§7.

3. Features (all computed from snapshots ≀ t β€” no look-ahead)

Listing: URL, host, path depth, path parameter present, description (+ length), price in USD, network. Telemetry: 30-day calls and unique payers, calls per payer, days since last paid call, days since listing updated, days observed in the index (with a left-censoring flag for listings already present on day 0), new-listing flag, presence and payer/call deltas vs 7 days earlier. Host aggregates at t: number of listings, share of the host's listings called in the last 7 days, share with exactly one call per payer, median payers, networks used.

4. Splits β€” two walls, both enforced in common.py

  1. Host wall. Hosts are hashed (sha256) into 100 buckets. Buckets 80-99 are test hosts, never seen in training. Many hosts mass-list hundreds of near-identical endpoints (one lists 840); without this wall a model memorises the host.
  2. Time wall. A training row's label window must end before the first test anchor, so no training label observes the test period.
    • paid_7d: train anchors 0-9, validation anchor 10, test anchors 17-21.
    • paid_14d: train anchor 0, validation anchor 1, test anchor 15 (31 days only allows one clean 14-day window; this horizon is secondary until more history exists).
  3. Calibration hosts. For the neural models, train-side buckets 70-79 are held out from training and used for model selection and temperature fitting, so calibration is measured on hosts the model has not seen.

Test set for paid_7d: 13,644 rows over 442 unseen hosts.

Segments reported everywhere: all, new_listings (first seen at the anchor: the cold start), small_hosts(<=20) (hosts with ≀ 20 listings: the long tail, less factory noise), stale(>7d since call).

5. Baselines (baselines.py) β€” what a checkpoint has to beat

paid_7d, test all AUC new listings AUC small hosts AUC ECE (all)
Recency rule (isotonic on days since last call) 0.635 0.416 0.696 0.095
Logistic regression, numeric features 0.678 0.500 0.731 0.052
LightGBM, numeric + network 0.672 0.514 0.737 0.076

Top LightGBM features by gain: 30-day calls, host listing count, host 7-day activity, days since update, host one-call-per-payer share, price.

Reading: telemetry gets you to AUC ~0.68 on unseen hosts. Brand-new listings are at chance for every telemetry model: they have no history, and their first "call" is often the settle that listed them.

6. Neural model (train_laya.py)

  • Base: convaiinnovations/laya (ModernBERT-large encoder + decision head, 421M, Apache-2.0). Output is a Laya checkpoint: it loads with laya.load() and answers typed questions.
  • Questions trained: two noul (yes/no probability) questions, paid_7d and paid_14d, with the instructions in train_laya.py:QUESTIONS. Use the same wording at inference.
  • Variants (only the input state differs):
    • text: {resource, description, price_usd, network}. Works for any listing, including one not yet listed. The text is identical across a resource's anchors, so each resource is one item whose soft target = the fraction of its training anchors that were paid.
    • full: the listing plus telemetry as text (paid_calls_30d, unique_payers_30d, days_since_last_paid_call, days_since_listing_updated, host_listing_count, host_share_called_in_last_7d). Two anchors are sampled per resource, with hard targets.
  • Objective: a single-GPU port of Laya's upstream RLCD recipe. REINFORCE on a strictly proper scoring rule (log + 0.75 spherical) with a group-mean baseline (G=4, Οƒ 0.4β†’0.1), plus a soft cross-entropy term. AdamW with encoder lr 2.5e-5 and head lr 1e-4, weight decay 0.01, warmup and cosine, micro-batch 16 Γ— accumulation 4, bf16 autocast, gradient checkpointing, max_len 384 (head budget 192), 3 epochs. The best epoch by held-out calibration-host AUC is kept.
  • Calibration: one temperature for noul, fitted by LBFGS on the calibration hosts, written to rl_agent_config.json where laya.Agent applies it.
  • Hardware: DGX Spark GB10, ~8.5 GB of GPU memory per run.

7. Results

All numbers are on the held-out test set (future anchors Γ— unseen hosts). Source files: runs/laya-*/eval.json, runs/bootstrap_full_vs_baselines.json, published as results/metrics.json.

paid_7d, ROC AUC

Model All (n=13,644) New listings (n=221) Small hosts ≀20 (n=6,135) ECE (all)
Recency rule 0.635 0.416 0.696 0.095
Logistic regression 0.678 0.500 0.731 0.052
LightGBM 0.672 0.514 0.737 0.076
Laya Β· listing only (text) 0.639 0.575 0.668 0.116
Laya Β· listing + telemetry (full) 0.732 0.581 0.754 0.110 β†’ 0.079 isotonic

paid_14d (one test anchor, n=2,631): LightGBM 0.652, text 0.653, full 0.733.

Significance (paired bootstrap, resampling HOSTS, 1,000 draws)

Rows within a host are strongly correlated, so resampling rows would overstate certainty.

Comparison median Ξ”AUC 95% CI P(Ξ” ≀ 0)
full vs LightGBM +0.058 [+0.010, +0.110] 0.014
full vs text +0.092 [+0.051, +0.141] 0.000
LightGBM + text score vs LightGBM, same calibration-host rows +0.027 [βˆ’0.008, +0.073] 0.076
… new listings only +0.080 [βˆ’0.002, +0.153] 0.026
LightGBM + full score vs LightGBM, same rows +0.017 [βˆ’0.017, +0.068] 0.202

Reading

  • full is the best single model and clears the strongest baseline, significant at the host level. It becomes the paid API.
  • text alone is weaker than telemetry, but it is the only model above chance on cold-start listings and adds a consistent, not yet conclusive, lift when stacked. It is the open "shop window" checkpoint.
  • Both Laya models are over-confident above ~0.5. One temperature cannot fix a top-end-only miscalibration. The API applies an isotonic map fitted on calibration hosts (recalibrate.py β†’ isotonic_paid7d.json): ECE 0.110 β†’ 0.079, Brier 0.180 β†’ 0.164, AUC unchanged by construction. The open checkpoint ships temperature-only, and its card says so.
  • The held-out calibration-host AUC seen during training (0.85 text, 0.91 full) is much higher than test. Those rows share training dates. Only the time + host wall is honest.

Decision (2026-09-23)

Checkpoint Release
text public HF model repo + Space (CPU)
full + isotonic map private β€” served as a pay-per-call x402 endpoint from the SentEdge server; weights backed up to a private HF repo

8. Limitations

  • 31 days of history. One clean 14-day test window, and a single month of seasonality.
  • Paid β‰  legitimate. Keepalive self-calls and wash traffic count as paid. Roughly 9.8k listings show ~1 call per payer, a pattern consistent with scripted callers. A "wash" label needs payer-level data the index does not expose.
  • Index-specific. Trained on the CDP Bazaar only; other x402 directories may differ.
  • Distribution drift. The x402 market is weeks old and changing fast; retrain monthly.

9. Reproduce

python -m venv .venv && .venv/bin/pip install torch --index-url https://download.pytorch.org/whl/cu130
.venv/bin/pip install -r requirements.txt
.venv/bin/python build_dataset.py --snap-dir /path/to/bazaar/snapshots   # -> data/rows.parquet
.venv/bin/python baselines.py                                           # -> runs/baselines.json
USE_TF=0 .venv/bin/python train_laya.py --variant text --epochs 3
USE_TF=0 .venv/bin/python train_laya.py --variant full --epochs 3 --anchors-per-resource 2
USE_TF=0 .venv/bin/python evaluate.py --ckpt runs/laya-text
USE_TF=0 .venv/bin/python evaluate.py --ckpt runs/laya-full
.venv/bin/python make_figures.py                                        # -> results/