Appendix / Machinery
Technical
appendix
Parameters, encodings, scoring, storage, and the collateral contract as implemented in the validator tree.
0. What this is
A description of subnet 123 as the code runs it. Numbers come from config.py, flow.py, model.py, ledger.py, validator.py, cycle.py, generate_and_encrypt.py, the per-challenge scorers, and FlowCollateralPool.sol. Where a comment and a live assignment disagree, the assignment wins and the comment is noted.
The public validator never holds the owner private key. It decrypts only after Drand publishes a signature for the payload round, and only after the row has aged PAYLOAD_MATURITY_BLOCKS. Instant decrypt of the owner wrap is owner-side tooling, kept out of this tree.
FLOW emissions are still at weight 0 unless MANTIS_FLOW_WEIGHT is set. The turn-on weight is a one-line change, not a clock. The announced date is 2026-09-14.
1. Constants
config.py · validator.py · flow.py · cycle.py
Subnet 123. Metagraph width 256 UIDs. Block time treated as 12 seconds everywhere that converts blocks to hours (flow.SECONDS_PER_BLOCK). Sample index sidx = block // 5.
| Symbol | Value | Meaning |
|---|---|---|
NETUID | 123 | Bittensor subnet |
NUM_UIDS | 256 | Metagraph width |
SAMPLE_EVERY | 5 blocks | 60 s at 12 s/block. One row per minute. |
LAG | 60 samples | Embargo between fit and validation in walk-forward scorers. 60 minutes. |
TASK_INTERVAL | 500 blocks | ~100 minutes. Periodic bookkeeping cadence. |
WEIGHT_CALC_INTERVAL | 1000 blocks | ~3.33 h. Recalculate salience. |
WEIGHT_SET_INTERVAL | 360 blocks | ~1.2 h. Submit saved weights if the UID vector still matches. |
BURN_PCT | 0.35 | After salience is mapped to UIDs, every weight is scaled by 0.65 and UID 0 receives +0.35. FLOW’s __burn__ key also maps to UID 0, on top of this. |
MAX_DAYS | 60 | In config.py. model.py reads it; the unused getattr fallback inside that file is 30. |
INDICES_PER_DAY | 1440 | One sample per minute. |
BLOCKS_PER_DAY | 7200 | 5 samples × 1440. |
MAX_INDEX_HISTORY | 86,400 | 60 × 1440. Binary scorer trims to this many rows. |
MAX_BLOCK_HISTORY | 432,000 | 60 × 7200. Other scorers trim hist/price to this. |
SEED | 42 | sklearn random_state and numpy generators that take a seed. |
| EMA α | 0.3 | Live assignment in validator.py. The comment above it still says 0.15 and quotes a ~5666-block average age for that older value. |
| Young UID | 36,000 blocks / 0.0001 | First nonzero embedding younger than 36,000 blocks (~5 days) gets a flat 0.0001 before burn. Mature UIDs share the remainder. |
MAX_PAYLOAD_BYTES | 25 × 220 | Fetch rejected if Content-Length or Content-Range exceeds this. |
PAYLOAD_MATURITY_BLOCKS | 50,400 | 7 days at 12 s. Public decrypt is not attempted earlier. |
TLOCK_PROD_SUGGESTED_LOCK_SECONDS | 604,800 | Matches 50,400 × 12. The CLI default TLOCK_DEFAULT_LOCK_SECONDS is 30. |
ALG_LABEL_V2 | x25519-hkdf-sha256+chacha20poly1305+drand-tlock | Required alg string on V2 payloads. |
DRAND_BEACON_ID | quicknet | https://api.drand.sh/v2 |
CLUSTER_SCAN_MAX_ROWS | 100,000 | Clone/cohort detectors use the most recent N rows. |
Drand public key (BLS12-381, pinned):
83cf0f2896adee7eb8b5f01fcad3912212c437e0073e911fb90022d3e760183c 8c4b450b6a0a6c3ac6a5776a2d1064510d1fec758c921cc22b0e17e63aaf4bcb 5ed66304de9cf809bd274ca73bab4af5a6e9c76a4bc09e76eae8991ef5ece45a
Owner HPKE public key (X25519, 32 bytes hex). A V2 payload whose owner_pk does not match this, case-insensitive, decrypts to nothing:
fbfe185ded7a4e6865effceb23cbac32894170587674e751ac237a06f72b3067
Published archives:
https://pub-879ad825983e43529792665f4f510cd6.r2.dev/datalog.dbhttps://pub-879ad825983e43529792665f4f510cd6.r2.dev/flow_datalog.dbhttps://pub-ba8c1b8edb8046edaccecbd26b5ca7f8.r2.dev/latest_prices.json2. Validator loop
validator.py · cycle.py · ledger.append_step · model.multi_salience
On a sample block the validator reads every on-chain commitment, downloads the object, writes a price row immediately, and stores the ciphertext. Embeddings are backfilled later onto those already-priced rows. Scoring is a separate thread on WEIGHT_CALC_INTERVAL. On-chain set_weights is a third cadence and will no-op if the saved UID vector does not match the current metagraph.
Price keys for FLOW may include BTC_HIGH and BTC_LOW: min of venue highs and max of venue lows over the minute. Rows without those channels resolve close-only. That is a priced degradation, not a different challenge.
Empty salience, all-zero totals, a single unique nonzero weight, or coefficient of variation below 0.001 on the nonzero weights: the calc thread returns without saving. A later set interval then has nothing new to submit.
3. Commit and fetch
cycle.get_miner_payloads · comms.download
The miner commits one URL on-chain (subtensor.commit). Every sample the validator reads the full commitment map and fetches in batches of 32.
A URL is accepted only if all of the following hold:
.r2.dev or .r2.cloudflarestorage.comRequired V2 keys: v, round, hk, owner_pk, C, W_owner, W_time, binding, alg. v must be the integer 2. Anything else is dropped at download.
The plaintext, once opened, must be a JSON object whose hotkey field equals the hotkey the ciphertext was stored under. Vectors are then checked per ticker. Values outside the challenge’s legal range become zeros for that ticker. LBFGS and FLOW have dedicated sanitizers; other flat vectors must lie in [-1, 1].
4. V2 envelope
generate_and_encrypt.generate_v2 · ledger._decrypt_v2_payload
One content key K (32 random bytes) encrypts the plaintext under ChaCha20-Poly1305. That key is wrapped twice. The two wraps are bound to each other by a SHA-256 over identity fields, used as AEAD associated data on both the content ciphertext and the owner wrap.
pke is the X25519 public key of a one-shot ephemeral secret ske. The owner wrap:
shared = X25519(ske, owner_pk) wrap_key = HKDF-SHA256(shared, info="mantis-owner-wrap", L=32+12)[:32] W_owner = ChaCha20Poly1305(wrap_key).encrypt(wrap_nonce, K, aad=binding)
The time wrap is Drand IBE (timelock crate pin ccccca019409c89f31fd687352db8060bfb4aae6). Plaintext of the lock is the 64-byte concatenation ske || K, hex-encoded, locked to round:
round = floor( (now + lock_seconds - genesis_time) / period ) W_time = tlock.tle(round, hex(ske || K), 32 random bytes)
Live payloads use the 356-byte W_time encoding. The PyPI timelock package is the old 372-byte stack and will not open.
Public decrypt, after maturity and after a Drand signature is in hand:
owner_pk ≠ configured owner key.tlock.tld(W_time, sig) → ske || K (64 bytes). The reader accepts both 356- and 372-byte ciphertext forms.X25519(ske).public == pke.W_owner with the same HKDF. Reject if the unwrapped bytes are not exactly K.C with K and the binding as AAD.A missing Drand signature is retried 3 times with a 1 s sleep (DRAND_SIGNATURE_RETRIES, DRAND_SIGNATURE_RETRY_DELAY). Payloads younger than PAYLOAD_MATURITY_BLOCKS + DRAND_RETAIN_GRACE_BLOCKS (50,400 + 50,400) are kept pending. Past that grace they are written as zero vectors and deleted. Failed decrypts are also zeros.
The owner wrap is a consistency check on the public path, not a decrypt path for validators. Anyone with the beacon signature and the published key can repeat the same six steps.
5. Storage
ledger.py
SQLite, WAL, synchronous=NORMAL. One connection. FLOW rows live in a second file, flow_datalog.db, ATTACHed as schema inv. Shared tables stay in the main file because one raw payload covers every ticker.
| Table | Key | Contents |
|---|---|---|
blocks | idx | Sequential index → chain block number. |
challenge_meta | ticker | dim, blocks_ahead. |
challenge_data | (ticker, sidx) | price or price_data JSON, hotkeys JSON, embeddings blob. |
inv.challenge_data | same | FLOW only. Same columns. |
raw_payloads | (ts, hotkey) | Ciphertext until maturity (plus grace). |
drand_cache | round | Beacon signature bytes. |
breakout_state | asset | Serialized RangeBreakoutTracker. |
Embedding blob: json(sorted_hotkeys) || 0x00 || row-major array. Width is self-describing: n * dim * 2 is float16, n * dim * 4 is float32. FLOW is written float32 because trade_id must be an exact integer out to 224; float16 is exact only to 2048. Every other ticker is float16.
Publish FLOW with DataLog.snapshot_for_publish → VACUUM inv INTO. Copying the live file plus -wal / -shm mid-write is a torn read. The main datalog publish path is separate. PRAGMA quick_check is used on snapshots only; downloads are not gated on it (the production archive has known transient payload-region flags).
TRADE-MIX historical rows are purged on datalog open. The assets constant remains because other price rows still carry those names.
6. Roster and emission
config.CHALLENGES · FLOW_TARGET_FRACTION
Twelve active challenges. TRADE-MIX removed 2026-08-17. Weights below are the weight field, not percentages. FLOW’s field is float(os.environ.get("MANTIS_FLOW_WEIGHT", "0.0")).
After that assignment the weight sum is 31.5. Shares of the post-burn pool (the 65% that is not the UID-0 reserve) are w / 31.5. Until the env var is set, FLOW is 0 and the other eleven split 23.625 in the same ratios, scaled up. The 35% burn does not change.
| Name | Ticker | dim | Horizon | loss_func | w | Post-burn % at 7.875 |
|---|---|---|---|---|---|---|
| FLOW-BTC | FLOW | 28 | 1–336 h, event | flow | 0 → 7.875 | 25.000% |
| MULTI-BREAKOUT | MULTIBREAKOUT | 2 / asset | event, ≤ 43,200 blocks | range_breakout_multi | 5.0 | 15.873% |
| FUNDING-XSEC | FUNDINGXSEC | 1 / asset | 2400 blocks (8 h) | funding_xsec | 4.0 | 12.698% |
| ETH-LBFGS | ETHLBFGS | 17 | 300 (1 h) | lbfgs | 3.5 | 11.111% |
| XSEC-RANK | MULTIXSEC | 1 / asset | 1200 (4 h) | xsec_rank | 3.0 | 9.524% |
| BTC-LBFGS-6H | BTCLBFGS | 17 | 1800 (6 h) | lbfgs | 2.875 | 9.127% |
| ETH-HITFIRST-100M | ETHHITFIRST | 3 | 500 (~1.67 h) | hitfirst | 1.25 | 3.968% |
| ETH-1H-BINARY | ETH | 2 | 300 | binary | 1.0 | 3.175% |
| CHFUSD-1H-BINARY | CHFUSD | 2 | 300 | binary | 1.0 | 3.175% |
| XAGUSD-1H-BINARY | XAGUSD | 2 | 300 | binary | 1.0 | 3.175% |
| CADUSD-1H-BINARY | CADUSD | 2 | 300 | binary | 0.5 | 1.587% |
| NZDUSD-1H-BINARY | NZDUSD | 2 | 300 | binary | 0.5 | 1.587% |
HITFIRST’s price_key is ETH. LBFGS tickers use price_key ETH and BTC. MULTI-BREAKOUT declares gate_top_pct: 0.10 on the spec; the current scorer does not read that field. Percentages rounded to three decimals; they sum to 100.
Asset lists
MULTI-BREAKOUT and XSEC-RANK share 33 names:
BTC ETH XRP SOL TRX DOGE ADA BCH XMR LINK LEO HYPE XLM ZEC SUI LTC AVAX HBAR SHIB TON CRO DOT UNI MNT BGB TAO AAVE PEPE NEAR ICP ETC ONDO SKY
FUNDING-XSEC, 20 perps:
BTC ETH SOL XRP DOGE ADA AVAX LINK DOT SUI NEAR AAVE UNI LTC HBAR PEPE TRX SHIB TAO ONDO
7. Weight aggregation
model.multi_salience · validator.py calc_worker
Each challenge returns a map hotkey → nonnegative score. That map is renormalized to sum 1 (FLOW’s map already includes __burn__). Challenge maps are then weight-averaged:
A challenge that returns empty, or whose scores sum to 0, is omitted and its weight is not in the denominator. Challenges are processed one at a time so peak memory is one payload.
Clone handling
Binary and HITFIRST run a two-tier detector on the design matrix (most recent 100,000 rows). Exact byte-identical clusters collapse onto a lex-min representative (mass summed). Soft-correlated cohorts, with exact-clone members stripped, are equalized so L2’s arbitrary split inside a flat subspace does not flicker per key. LBFGS does the same work inside bucket_forecast._cached_reduce.
LBFGS blend, before the average
Classifier path and Q-path are each discarded if near-uniform (range/mean < 0.01). Survivors are top-50 renormalized with exponential rank decay τ = max(1, k/3), then mixed 0.75 / 0.25 and renormalized again.
UID mapping
__burn__ → UID 0. Other keys that are not on the metagraph are dropped. FLOW’s unearned share therefore arrives on UID 0 before the global 35% reserve. Young UIDs (first nonzero row < 36,000 blocks ago) each take 0.0001 of the pre-burn mass; mature UIDs share 1 − 0.0001 × n_young in proportion to salience. Then burn: every UID × 0.65, UID 0 += 0.35. Then EMA against the last saved vector, keyed by hotkey if present else UID, α = 0.3, renormalize. set_weights is skipped on UID-list mismatch.
8. Binary
model.salience_binary_prediction
Tickers ETH, CADUSD, NZDUSD, CHFUSD, XAGUSD. Dim 2. Horizon 300 blocks. The two numbers are logistic features in [-1, 1], not probabilities. Label: y = 1[r > RET_EPS], default RET_EPS = 0.
Need T ≥ 500 after trim, both classes present. CHUNK_SIZE is not set in config.py; this scorer’s getattr default is 4000 (the cross-section scorers default to 8000).
Feature selection
Per miner, an L2 logistic is refit every SEL_REFIT_DAYS = 5 days on a trailing SEL_FIT_WINDOW_DAYS = 30 day window, embargoed by LAG. Predictions on the following 5-day slice are scored as daily AUCs (SEL_WINDOW_ROWS = 1440). Daily AUCs are combined with exponential decay, half-life SEL_HALFLIFE_DAYS = 15. A miner needs at least SEL_MIN_VALID_WINDOWS = 5 days with SEL_MIN_WINDOW_ROWS = 80 scored rows each.
Top-K with K = 50, then a margin band: anyone within SEL_AUC_MARGIN = 0.005 of the K-th AUC is kept, up to 15 extras. Then drop anyone with AUC ≤ 0.5. The band exists so a rank-50/51 flip under per-validator row skew lands on columns the ElasticNet L1 will shrink toward zero.
Meta-model
Walk-forward OOS base-model scores for the selected columns. ElasticNet logistic, L1 ratio 0.5, C = 1, balanced classes, 2000 iterations. Importance is |βj|. Segments are recency-weighted. L1 plus L2 is the sybil mechanism: identical columns split mass; noise columns go to zero.
9. HITFIRST
hitfirst.compute_hitfirst_salience
Dim 3: [P(up first), P(down first), P(neither)] in (0, 1), renormalized to sum 1. Horizon 500 samples. Need ~5 days of samples (min_days = 5, MIN_REQUIRED_SAMPLES floor).
At each t, σt is a rolling standard deviation of log returns over a window of max(required, MIN_REQUIRED_SAMPLES). Barriers are ±σt in log-price space on the next 500 steps. Label 0 if +σ prints first, 1 if −σ prints first, 2 if neither or a same-index tie. Invalid σ rows are dropped.
Submitted rows (any nonzero) are logit-transformed on the up and down coordinates; unsubmitted are 0. Two independent L2 logistics, C = 1, balanced, 500 iterations. Each fit is averaged across 4 leave-one-block-out replicates with deterministic proportional cuts (no RNG). Importance is |βup| + |βdown|, then normalized.
The function signature still takes half_life_days = 5.0. The implemented path does not use it. There is no walk-forward.
10. LBFGS
bucket_forecast.py · MINER_GUIDE §3.3
Dim 17. ETH at 300 blocks, BTC at 1800. Two independent paths, blended 75/25 in multi_salience after each path is top-50 rank-decayed.
| Index | Role |
|---|---|
[0:5] | Regime probabilities over 5 buckets bounded at ±1σ, ±2σ. (0, 1), sum 1. Argmax is the classifier label. |
[5:8] | Bucket 0 exceedance at {0.5, 1.0, 2.0}σ |
[8:11] | Bucket 1, same thresholds |
[11:14] | Bucket 3 |
[14:17] | Bucket 4. Center bucket 2 has no Q-path. |
Classifier path
Per-class L2 logistics on the 5-way argmax, walk-forward. Importance is Σc βj,c2. Segments that do not beat a random baseline are dropped. Recency-weighted. After aggregation, a uniqueness penalty on the last walk-forward chunk of argmax series: miners ranked by current importance, each compared to all higher-ranked peers,
Exact copies of a higher-ranked series go to 0. Independent series stay near 1. There is no 85% hard cutoff in this file; that figure in older notes is not the implemented rule. Then keep top 25 inside the scorer (bucket_forecast.TOP_K) before the outer 50-renorm.
Q-path
Twelve binary L2 logistics on logit exceedance probabilities. Importance is mean |βj| across the twelve. Same walk-forward / recency skeleton. Top 25 inside the scorer.
11. MULTI-BREAKOUT
range_breakout.py · config.MULTI_BREAKOUT_CHALLENGE
Per asset, a tracker keeps a rolling window of 28,800 blocks (4 days) of samples. Need at least half that many prices in the window or there is no range. Range width must be at least 1% of spot. Price history is trimmed to twice the lookback.
Trigger: close prints above the window high, or below the window low, and that side has no pending event. Barriers are 25% of range width from the trigger print (the challenge spec; the module-level default 10% is overridden by from_dict / constructor args from config).
reversal = trigger ∓ 0.25 × (high − low)
One pending high and one pending low per asset. Resolution is close-only against those barriers. If current_block − trigger_block > 43,200 (~6 days) the pending event is discarded unlabeled. Embeddings frozen at trigger are the standing [P_cont, P_rev] pair.
Scoring
Need 50 completed events network-wide. Episodes: sort by trigger_sidx, start a new episode when the gap exceeds 1440 samples (~1 day). A miner qualifies with AUC(P_cont, label) > 0.5, prediction std ≥ 0.03, both labels present, and at least 2 distinct episodes.
Qualified columns are z-scored (nan → 0). Sample weights give each episode equal total mass, then rescale so weights sum to T. L2 logistic, C = 0.01, balanced, 1000 iterations. Importance |βj|, L1-normalized, drop below 1e-6.
An older uniqueness/AUC-softmax path remains in the file as _compute_multi_breakout_salience_old and is not called.
12. XSEC-RANK
xsec_rank.compute_xsec_rank_salience
Dim 1 per asset, 33 assets, flattened as (T, H, 33). Horizon 1200 blocks = 240 samples. Score in [-1, 1]. Need T_u = T − ahead ≥ 500.
yt,a = 1[ rt,a > mediana(rt,·) ]
Nonpositive prices are NaN and dropped. The design is pooled to (T_u × 33, H). Walk-forward segments of CHUNK_T = 8000 with LAG = 60. Feature selection: per-miner univariate AUC on a window before the validation slice, top 20. Meta: L2 logistic C = 0.5, balanced. When the timeline is long enough, the meta-fit is a block bootstrap: 20 replicates, block length 120 (two hours), starts sorted for determinism, seed 42 + b. Prediction uses the signed mean coefficient; importance uses a 4-group median-of-means of |β|.
Segments combined with γ = 0.51/3 (half-life 3 segments).
13. FUNDING-XSEC
funding_xsec.compute_funding_xsec_salience
Same meta-model as XSEC-RANK, 20 assets, horizon 2400 blocks (8 h). Label is on the change in funding, not the level, so a common factor with autocorrelation ~1 is differenced out. Cross-sectional median then removes the remaining market-wide move. Base rate is 50% by construction.
yt,a = 1[ Δft,a > mediana(Δft,·) ]
Forward pairing is by sidx, not row index. For each t, find the row whose sidx is closest to sidx[t] + ahead within 15% of ahead (minimum 1). Gaps from downtime therefore do not silently misalign an 8 h settlement.
A miner column with temporal standard deviation < 10−4 on an asset is zeroed before pooling (stale submitter). Walk-forward embargo is max(LAG, ahead). Training rows for a validation start at t are cut at val_start − ahead so a forward-looking label cannot leak. Same bootstrap and recency as XSEC-RANK.
14. FLOW encoding
flow.py · config.FLOW_CHALLENGE
Dim 28 = 4 regimes × 7 fields, stored float32. Horizon is not blocks_ahead (that field is 0). One open trade per regime at a time. Opens rate-limited to one per hour per regime. Constant-emission: repeat the current tuple; a new trade_id opens.
per regime, in order A B C D: [d, f, sl_frac, tp1_frac, tp2_frac, h_hours, trade_id] d -1 short, +1 long, 0 flat (row ignored) f Kelly fraction, [0.01, 0.25] sl_frac stop distance / entry, [1e-4, 0.50] tp1_frac first target / entry, (sl-adjacent) 1e-4 ≤ tp1 < tp2 tp2_frac second target / entry, tp1 < tp2 ≤ 0.50 h_hours A [1, 24], B [24, 48], C [72, 168], D [168, 336] trade_id integer in (0, 2^24); reuse after first sighting is inert
Brackets are fractions of the validator-locked entry E on the open row. Nothing in the payload is an absolute price. E is the close in price_data at that sidx, written at block arrival, before decrypt.
TP1 = E (1 + d · tp1_frac)
TP2 = E (1 + d · tp2_frac)
Implied-p gate
A tuple that otherwise looks legal is rejected unless the one-target Kelly inversion sits off the boundary:
p = (f b + 1) / (b + 1)
require p_bound ≤ p ≤ 1 − p_bound, p_bound = 0.01
Rejected tuples still consume the trade_id, so a broken id cannot be retried forever. d is rounded to int; only ±1 is a trade. NaN / inf in the 28-vector become 0 before decode.
15. FLOW decode
flow.decode_trades
Per miner, per regime, a cursor walks rows. Hours are sidx * SAMPLE_EVERY * 12 / 3600 = sidx / 60 if sidx is the sample index (one per minute). If sidx_arr is omitted, row number is used in its place.
A row opens a trade when all of these hold: d ∈ {−1, +1}, 0 < trade_id < 224, id not in seen_ids, no live trade in the regime (t > open_until_row), at least min_open_gap_h = 1 hour since the last open in that regime, the tuple passes _valid_tuple, and E > 0.
seen_ids remembers every id ever opened or consumed in the regime. Recycling 1, 2, 1, 2 is inert. That matches the contract, which keys (regime, id) permanently via (regime << 32) | trade_id.
While a trade is open, later rows in that regime do nothing. open_until_row is the resolve row, or T if still live at the panel end. Dropped payloads delay an open by at most one sample; they do not cancel a live trade.
16. FLOW resolution
flow._resolve
Walk from open_row + 1 to the first sample with hour ≥ horizon_hour (searchsorted, right). If that window is empty, the trade is still open (resolve_row = -1) and does not enter scoring.
With wick channels: adverse series is low for a long and high for a short; favorable is the opposite. Without wicks, both series are the close. Stop: adverse print has crossed SL. Target: favorable print has crossed TP1 or TP2.
First-event, stop-first on a tie (same row):
SL.TP2.TP1, close at the last in-window row.F, close at the last in-window row.MAE is the worst adverse excursion in price units up to the resolve row, capped at the stop distance, then r_MAE = MAE / |E − SL| ∈ [0, 1].
17. FLOW scores and gate
flow.FlowConfig · compute_flow_salience
Launch table (release §1.8), also the dataclass defaults and the challenge spec:
| Symbol | Value | Role |
|---|---|---|
| δ | 0.30 | TP2 completion bonus |
| λ | 0.50 | TP1 partial factor |
| γ | 0.25 | Path penalty on r_MAE |
| τ | 1.00 | Tail amplifier: multiply by 1+τ when the hour is a tail hour |
| tail_mult | 2.0 | Tail iff σ24h > 2 σ30d |
| ewma_α | 0.05 | Per-regime EWMA of paid scores |
| κ | 672 h | Decay from last resolution. 2× the longest horizon. |
| probation | 168 h | No pay until first resolve + 168 h |
| gate_block_h | 168 h | Evidence block width |
| gate_min_n | 4 | Blocks before t is defined |
| z_in / z_out | 1.25 / 0.50 | Latch hysteresis |
| dust | 0.02 | Uncleared share of the hourly pool |
| period_loss_cap | 0.25 | Off-chain settlement breaker (see §19) |
| smooth_hours | 24 | Trailing mean of hourly allocations |
Per-trade R
s = −1. No path penalty. s_base = −1.s = R(TP2)(1+δ) − γ r_MAE. s_base = R(TP2)(1+δ).s = R(TP1) λ − γ r_MAE. s_base = R(TP1) λ.s = R(close) − γ r_MAE. s_base = R(close).Then, on the resolve row: s_weighted = s × (1 + τ 1tail) × f. If a live collateral_fn is present, multiply again by bet / f, i.e. the paid score is sized by the posted bet. s_base is never multiplied by collateral. An unposted or late bet still latches the gate and still earns 0.
Tail flag
Hourly log-return series, last close in each hour, forward-filled. For hour i ≥ 48: σ24 is the sample std of the previous 24 hourly returns (need i ≥ 24). σ30 is the std of the previous 720 hours, but only if that window has at least 168 points. Flag if σ30 > 0 and σ24 > 2 σ30. Mapped back to sample rows by hour index.
t-statistic
On each resolve, s_base is added to block floor(resolve_hour / 168). When a block updates:
t(x) = mean(x) / s(x) × √n (ddof=1; −∞ if n < 4 or s ≤ 1e-12)
Latch: off → on at t ≥ 1.25; on → off at t < 0.50. The rolling window lets a strong recent record clear without waiting out a long cold prefix. The full history keeps a long consistent record cleared through a flat month.
If the panel spans less than 168 hours, the function returns {__burn__: 1.0} and stops.
18. FLOW payment
flow.compute_flow_salience hourly loop
Hours from floor(hours[0])+1 to floor(hours[-1]). Each hour, ingest resolutions with resolve_hour ≤ h, update per-regime EWMA, recompute t, update latch. Then:
if first_resolve is None or h < first_resolve + 168: skip S = sum(ewma[0..3]) # additive, no floor if S ≤ 0: skip if static collateral map (no collateral_fn): S *= skin; skip if skin ≤ 0 score = S * exp( -(h - last_resolve) / 672 ) cleared = latched keys that have a score this hour uncleared = the rest with a score main = 0.98; dust = 0.02 cleared split main pro rata by score uncleared split dust pro rata by score if no cleared key: main is not paid (burns) output[hk] = mean( paid[t] for t in last 24 hours ) __burn__ = 1 - sum(output)
The whole allocation, including latch state, is a pure function of the shared datalog plus the public TradePosted log (for the bet pricer). Validators do not carry latch in local state across processes.
Collateral snapshot for the pricer is pinned to the newest block height that is a multiple of 300 (~1 hour) so validators inside the same window see one chain head. Free-look: a bet with posted_ts > open_ts + 900 prices at 0 on both emission and settlement. Open time is converted to wall time via chain_ts + (open_hour − now_hour) × 3600 so both legs void the same bets.
19. Settlement math
flow._money_r · compute_collateral_settlement · flow_collateral.pro_rata_payouts
Money R is not the score. No δ, no λ, no γ, no tail, no f in the R itself:
R(TP2), floored at −1.R(close at resolve row), floored at −1.claim α = max(R, 0) × bet
Because the pricer returns bet / f, the older formula R × f × collateral lands on the same bet. A trade with no bet or a voided late bet is 0 both ways.
Off-chain period aggregation (compute_collateral_settlement): trades whose resolve_hour lies in [period_start_h, period_end_h), in resolve order. Cap for the book that period is the max collateral among those trades. Gross losses stop at 0.25 × cap; every later trade in the period is void for money in both directions. Net negative → loss; net positive → claim. Losses then clamped to the cap. If there are losses and no claims, the batch is deferred (carry_losses) for the next period. The contract does the same roll when settle is called with an empty winner list.
Integer rao split: floor each winner’s share; remainder to the largest claim (tie: sort key (-claim, hotkey)). Over-collection refunds (gross debit minus net) are applied in the same settle batch via compose_settle_payouts. The contract requires Σ winRao = pool or it reverts NotZeroSum.
20. FlowCollateralPool
flow_collateral/src/FlowCollateralPool.sol · config.py
0xD9c805202b16671A2901307fBC9A8750E2453427https://lite.chain.opentensor.ai0x5186318Ba00Ca115d92C37D2b646eA3867C2c7540x379d4712e9902a9ca2ba1b827f4cb2c48ec03b510b107bf700c92592d9df067a (config; on-chain via setContractColdkey, one-shot)FLOW_COLLATERAL_ADDRESS=off forces f-only weightingAlpha is not an ERC-20. Custody is the staking-v2 precompile at 0x…0805: contract-owned stake delegated to the miner’s hotkey. sr25519 verification is 0x…0403. Amounts are rao (1 α = 109 rao). Share-pool rounding slack on pulls: 10,000 rao. Minimum position: 1 α. Settlement moves below 106 rao (0.001 α) are skipped and emitted SettleMoveSkipped rather than reverting the batch.
Period clock
else 1 + floor( (now − periodZero) / 168 hours )
closeBatch cannot debit a period that has not started, and cannot target a period more than lastSettledPeriod + 2. settle requires periodId == lastSettledPeriod + 1 and now ≥ periodZero + periodId × 168h. The quarter-book breaker is therefore per real week at any settle velocity.
Trade key and expiry
regime ∈ {0,1,2,3}, trade_id ≠ 0 as uint32
expiry = now + {24, 48, 168, 336}h[regime] + 48h
D-regime worst case is 384 h = MAX_OPEN_SECONDS. The miner cannot shorten expiry (a bet cannot be swept out from under its own resolution) and cannot stretch it past the regime ceiling. sweepExpired is permissionless after now > expiry. A loss never posted before expiry is not collected.
Position
One slot per hotkey. Creation is addCollateralSigned only: hotkey sr25519 over
keccak256( "FLOWFUND" || chainid || contract || hotkey || msg.sender
|| refundColdkey || amount || nonce )
with nonce > postNonce[hotkey]. msg.sender becomes the sole depositor. Refund coldkey is fixed. Later addCollateral is depositor-only; the refund argument is ignored (kept for ABI compatibility). withdraw up to balance − openExposure. A flat book below MIN_COLLATERAL after a withdrawal is fully exited and deleted. Physical send is clamped to held − pooledOut.
evict: hotkey-signed "FLOWEVICT" || chainid || contract || hotkey || nonce. Requires openCount == 0 and pooledOut == 0. Pays only the recorded refund coldkey. Anyone may relay.
Bets
postTrade (depositor) or postTradeSigned (hotkey over "FLOWPOST" || chainid || contract || hotkey || tradeKey || collateralRao || nonce). Reserve ≤ 25% of current balance (EXPOSURE_CAP_BPS = 2500). Immutable. Direction and levels are not on chain. TradeExists if the key is already open.
closeBatch
Owner only. Per item: unknown key reverts; lossRao > exposure reverts; loss is then min’d with balance and with the breaker
d = periodDebited[hk][period]
which is the rearrangement of “cumulative debit this period ≤ one quarter of period-start balance” using current balance and already-debited d. Excess is not collected. Wins/flats close at 0. Debited rao moves into poolAccum[period] and pooledOut on the book; physical sweep waits for settle. Sources aggregate per hotkey per period, not per trade.
settle
Owner only. Empty winners: roll pool and source lists into periodId+1, emit PoolRolled. Nonempty: credit each winner’s book, require sum equals pool, move physical alpha loser → winners[0] → other winners, each move clamped to held and skippable below MIN_MOVE. Books are authoritative; a skipped physical move is drift, repairable with reconcile (excess-only on source, deficit-only on dest; cannot touch booked balance or parked pool).
There is no slash. Alpha leaves a book through a settlement debit (zero-sum, to winners) or the depositor’s withdrawal. The owner cannot open, resize, or extend a bet.
21. Microstructure log
microstructure.py
Not an input to any scorer. Append-only JSONL, one file per UTC day, gzipped on rollover. Each line carries a SHA-256 of the line contents chained to the previous hash (persisted in .chain, but recovered from the newest file’s last valid hash on restart so a torn side-file cannot fork the chain). A torn last line is dropped. Cadence 60 s. Sources fail soft (null field, line still written).
BTCUSDT depth 5000. Best bid/ask, mid, spread bps, bid/ask depth and imbalance inside 25 / 50 / 100 bps of mid.truncated if 1000-trade cap hit.22. Reproducibility
config.py env pin · model.set_global_seed
config is imported before numpy in every scoring module. At import it sets, if unset: OpenMP / MKL / OpenBLAS / BLIS / NumExpr / vecLib thread counts to 1, MKL_DYNAMIC and OMP_DYNAMIC false, PYTHONHASHSEED=0, CUBLAS_WORKSPACE_CONFIG=:4096:8. CUDA is hidden unless MANTIS_ALLOW_CUDA is 1/true/yes. Single-threaded BLAS keeps reductions associative-stable across machines; that is the dominant vtrust requirement.
set_global_seed(42) then seeds Python / numpy / torch and hammers already-loaded BLAS via threadpoolctl, for the case where a transitive import loaded numpy before the env block ran. Torch deterministic algorithms on, cuDNN benchmark off.
Agreement is still not bit-identical across datalogs. Validators can have different row counts (downtime, late decrypt). HITFIRST’s leave-block-out cuts are proportional to T so nearby lengths produce nearby replicate sets. XSEC / funding bootstrap seeds are 42 + b. Binary selection was rewritten from a single mid-split AUC to a recency-weighted daily AUC for the same reason: the old statistic’s noise floor exceeded the gap between adjacent ranks.
23. Module map
| File | Role |
|---|---|
config.py | Challenge table, network constants, encryption pins, FLOW collateral addresses, env pinning. |
validator.py | Sample loop, weight calc thread, EMA 0.3, young-UID floor, burn, set_weights. |
cycle.py | Commitment read, R2 allowlist, 25 MB cap, batch-32 fetch. |
comms.py | V2 schema check, size probe, download. |
generate_and_encrypt.py | Miner-side V2 envelope. Supported submission surface with flow_post.py. |
ledger.py | SQLite, price-at-arrival, maturity decrypt (public path only), sanitizers, VACUUM INTO. |
model.py | multi_salience dispatch, binary ElasticNet, clone collapse, LBFGS 75/25 blend. |
hitfirst.py | Barrier race, 4-block LBO L2. |
bucket_forecast.py | LBFGS classifier + Q-path, uniqueness penalty, inner top-25. |
range_breakout.py | Per-asset state machine, episode-balanced logistic. |
xsec_rank.py | Median-beat label, top-20, 20×120 bootstrap. |
funding_xsec.py | Same meta on funding Δ, sidx pairing, stale-std zero, embargo max(LAG, ahead). |
flow.py | Decode, resolve, gate, hourly pay, settlement batch, trade_events. |
flow_collateral.py | ABI client, trade_key, 900 s grace, bet pricer, rao pro-rata. |
flow_post.py | Miner CLI: fund / evict / book / post / status / audit. |
microstructure.py | Hash-chained JSONL, unused in scoring. |
../flow_collateral/src/FlowCollateralPool.sol | The pool. One copy. |
The settlement daemon is not in the public tree. It holds owner keys. What it may write is bounded on chain: closeBatch, settle, reconcile. Every number it posts is recomputable from the matured panel, the tape, and TradePosted / TradeClosed logs. flow_post.py audit replays those logs and checks no debit exceeded a posted bet.
There is no bundled submission UI. Interactive wrappers run the caller’s code with the caller’s keys.
24. Worked numbers
Hours from sidx
A sample at sidx = 1440 is hour 1440 × 5 × 12 / 3600 = 24. One day of rows. Regime A’s 24 h ceiling is 1440 samples after open; D’s 336 h is 20,160 samples.
A legal A-regime tuple
Long, f = 0.10, sl = 2%, tp1 = 2%, tp2 = 4%, h = 12, id = 7.
p = (0.10 × 1 + 1) / (1 + 1) = 0.55 ∈ [0.01, 0.99]
At E = 100,000: SL = 98,000, TP1 = 102,000, TP2 = 104,000. Stop distance = 2,000. A TP2 fill is R = 2. Score s = 2 × 1.30 − 0.25 r_MAE. If the path never went adverse, r_MAE = 0 and s = 2.6. Paid contribution before tail is 2.6 × 0.10 = 0.26, or 2.6 × bet if a collateral_fn is live (the 0.10 cancels). A stop is s = −1, paid −0.10 or −bet. Money R on a stop is −1 regardless of a wick through the level.
Illegal: too much f for the payoff
f = 0.25, sl = 5%, tp1 = 1% (b = 0.2): p = (0.25×0.2 + 1) / 1.2 = 0.875, legal. Same f with tp1 = 0.02% and sl = 5% (b = 0.004): p = (0.001 + 1) / 1.004 ≈ 0.996, rejected. The id is consumed.
Emission turn-on
Others sum to 23.625. 25% of a 31.5 pool is 7.875. After burn, UID 0 still takes 35% of the subnet emission. FLOW’s 25% is 25% of the remaining 65%, i.e. 16.25% of gross subnet emission, plus whatever lands on UID 0 from __burn__ inside FLOW and from the global burn.
Young UID
10 young UIDs take 0.001 of pre-burn mass. 99.9% is split among mature salience. After ×0.65 and +0.35 on UID 0, a young UID shows as 0.000065 plus nothing else, unless it is UID 0.
Free-look
Trade opens at panel hour 100. Validator’s now_hour is 120, chain timestamp 1,787,100,000. Open wall time = 1,787,100,000 + (100 − 120) × 3600 = 1,787,028,000. A TradePosted at 1,787,028,901 is 901 s late and prices at 0. At 1,787,028,900 it is accepted.
On-chain breaker
Book 100 α, nothing yet debited this week: room = (100 − 0) / 4 = 25 α. After a 25 α debit, d = 25, bal = 75, room = (75 − 75) / 4 = 0. Further losses that week collect 0. The off-chain scorer’s period_loss_cap = 0.25 of the max collateral among the period’s trades is the same intent, applied before the owner builds the closeBatch arrays.
Same-candle stop
Long, one-minute bar high through TP2 and low through SL. i_sl == i_tp2. The test is i_sl ≤ min(i_tp2, len−1), so the event is SL. Path penalty is not applied. Money R is −1. This is the documented tie-break, not an accident of array order.
The FLOW paper (FLOW_RELEASE.pdf) has the simulation evidence behind these constants. This page is the running code.