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.

SymbolValueMeaning
NETUID123Bittensor subnet
NUM_UIDS256Metagraph width
SAMPLE_EVERY5 blocks60 s at 12 s/block. One row per minute.
LAG60 samplesEmbargo between fit and validation in walk-forward scorers. 60 minutes.
TASK_INTERVAL500 blocks~100 minutes. Periodic bookkeeping cadence.
WEIGHT_CALC_INTERVAL1000 blocks~3.33 h. Recalculate salience.
WEIGHT_SET_INTERVAL360 blocks~1.2 h. Submit saved weights if the UID vector still matches.
BURN_PCT0.35After 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_DAYS60In config.py. model.py reads it; the unused getattr fallback inside that file is 30.
INDICES_PER_DAY1440One sample per minute.
BLOCKS_PER_DAY72005 samples × 1440.
MAX_INDEX_HISTORY86,40060 × 1440. Binary scorer trims to this many rows.
MAX_BLOCK_HISTORY432,00060 × 7200. Other scorers trim hist/price to this.
SEED42sklearn random_state and numpy generators that take a seed.
EMA α0.3Live assignment in validator.py. The comment above it still says 0.15 and quotes a ~5666-block average age for that older value.
Young UID36,000 blocks / 0.0001First nonzero embedding younger than 36,000 blocks (~5 days) gets a flat 0.0001 before burn. Mature UIDs share the remainder.
MAX_PAYLOAD_BYTES25 × 220Fetch rejected if Content-Length or Content-Range exceeds this.
PAYLOAD_MATURITY_BLOCKS50,4007 days at 12 s. Public decrypt is not attempted earlier.
TLOCK_PROD_SUGGESTED_LOCK_SECONDS604,800Matches 50,400 × 12. The CLI default TLOCK_DEFAULT_LOCK_SECONDS is 30.
ALG_LABEL_V2x25519-hkdf-sha256+chacha20poly1305+drand-tlockRequired alg string on V2 payloads.
DRAND_BEACON_IDquicknethttps://api.drand.sh/v2
CLUSTER_SCAN_MAX_ROWS100,000Clone/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:

datalog
https://pub-879ad825983e43529792665f4f510cd6.r2.dev/datalog.db
FLOW datalog
https://pub-879ad825983e43529792665f4f510cd6.r2.dev/flow_datalog.db
prices
https://pub-ba8c1b8edb8046edaccecbd26b5ca7f8.r2.dev/latest_prices.json

2. 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.

SAMPLE block % 5 = 0 FETCH R2, ≤25 MB PRICE ROW E locked here MATURE +50400, Drand SCORE / SET 1000 / 360 ciphertext stored under (sidx, hotkey) plaintext written back onto the same sidx

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:

Host
ends with .r2.dev or .r2.cloudflarestorage.com
Path
exactly one segment; that segment equals the hotkey, case-insensitive; not a directory
Body
JSON object, V2 schema, size ≤ 25 MB (HEAD Content-Length, else Range 0-0 Content-Range)

Required 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.

binding = SHA256( hk || ":" || ascii(round) || ":" || owner_pk || ":" || pke )

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.

PLAINTEXT JSON, tickers + hk C = AEAD(K) aad = binding W_OWNER X25519 wrap of K W_TIME IBE of ske||K OWNER BOX not in this repo VALIDATOR Drand sig, +7d

Public decrypt, after maturity and after a Drand signature is in hand:

1
Reject if owner_pk ≠ configured owner key.
2
Recompute binding. Reject if it does not match the payload field.
3
tlock.tld(W_time, sig)ske || K (64 bytes). The reader accepts both 356- and 372-byte ciphertext forms.
4
Check X25519(ske).public == pke.
5
Unwrap W_owner with the same HKDF. Reject if the unwrapped bytes are not exactly K.
6
Open 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.

TableKeyContents
blocksidxSequential index → chain block number.
challenge_metatickerdim, blocks_ahead.
challenge_data(ticker, sidx)price or price_data JSON, hotkeys JSON, embeddings blob.
inv.challenge_datasameFLOW only. Same columns.
raw_payloads(ts, hotkey)Ciphertext until maturity (plus grace).
drand_cacheroundBeacon signature bytes.
breakout_stateassetSerialized 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_publishVACUUM 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")).

WFLOW, on = 0.25 × Σ wothers / 0.75 = 0.25 × 23.625 / 0.75 = 7.875

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.

NameTickerdimHorizonloss_funcwPost-burn % at 7.875
FLOW-BTCFLOW281–336 h, eventflow0 → 7.87525.000%
MULTI-BREAKOUTMULTIBREAKOUT2 / assetevent, ≤ 43,200 blocksrange_breakout_multi5.015.873%
FUNDING-XSECFUNDINGXSEC1 / asset2400 blocks (8 h)funding_xsec4.012.698%
ETH-LBFGSETHLBFGS17300 (1 h)lbfgs3.511.111%
XSEC-RANKMULTIXSEC1 / asset1200 (4 h)xsec_rank3.09.524%
BTC-LBFGS-6HBTCLBFGS171800 (6 h)lbfgs2.8759.127%
ETH-HITFIRST-100METHHITFIRST3500 (~1.67 h)hitfirst1.253.968%
ETH-1H-BINARYETH2300binary1.03.175%
CHFUSD-1H-BINARYCHFUSD2300binary1.03.175%
XAGUSD-1H-BINARYXAGUSD2300binary1.03.175%
CADUSD-1H-BINARYCADUSD2300binary0.51.587%
NZDUSD-1H-BINARYNZDUSD2300binary0.51.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:

sj = (1 / Σc wc) Σc wc ŝj,c

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.

IndexRole
[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,

overlap = mean( argmaxi = argmaxj )    penalty = (1 − max overlap)2

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).

continuation = trigger ± 0.25 × (high − low)   in the break direction
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.

RANGE 4d window, ≥1% TRIGGER close outside FREEZE PAIR [P_c, P_r] at sidx RESOLVE +25% / −25% / stale

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.

rt,a = Pt+h,a / Pt,a − 1
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 |β|.

wj,seg = |βj| × max( (AUCmeta − 0.5) / 0.5 , 0 )

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.

Δft,a = ft+h,a − ft,a
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.

SL = E (1 − d · sl_frac)
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:

b = tp1 / sl
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
stop index ≤ TP2 index and the stop printed. A TP1 that printed earlier does not save it. Event SL.
TP2
TP2 printed before any stop. Event TP2.
open
No terminal print, and the last panel hour is still before the horizon. Leave unresolved. Do not score a partial window (that would change the gate on the next expanding-panel recompute).
TP1
Horizon reached, TP1 printed, TP2 never, no stop. Event TP1, close at the last in-window row.
F
Horizon reached, neither target, no stop. Event 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].

OPEN E from the row WALK PATH wicks if present SL stop-first TP2 full target TP1 / F R AND PATH then tail, then f

17. FLOW scores and gate

flow.FlowConfig · compute_flow_salience

Launch table (release §1.8), also the dataclass defaults and the challenge spec:

SymbolValueRole
δ0.30TP2 completion bonus
λ0.50TP1 partial factor
γ0.25Path penalty on r_MAE
τ1.00Tail amplifier: multiply by 1+τ when the hour is a tail hour
tail_mult2.0Tail iff σ24h > 2 σ30d
ewma_α0.05Per-regime EWMA of paid scores
κ672 hDecay from last resolution. 2× the longest horizon.
probation168 hNo pay until first resolve + 168 h
gate_block_h168 hEvidence block width
gate_min_n4Blocks before t is defined
z_in / z_out1.25 / 0.50Latch hysteresis
dust0.02Uncleared share of the hourly pool
period_loss_cap0.25Off-chain settlement breaker (see §19)
smooth_hours24Trailing mean of hourly allocations

Per-trade R

R(P) = d (P − E) / |E − SL|
SL
s = −1. No path penalty. s_base = −1.
TP2
s = R(TP2)(1+δ) − γ r_MAE. s_base = R(TP2)(1+δ).
TP1
s = R(TP1) λ − γ r_MAE. s_base = R(TP1) λ.
F
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 = max( t(all blocks), t(last 4 blocks) )
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.

SCORE EWMA × e−Δ/κ LATCH t, 1.25 / 0.50 CLEARED 98% pro rata by score DUST 2% uncleared, S>0 BURN TRAIL 24h mean, then __burn__

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:

SL
−1, even across a gap past the stop.
TP2
R(TP2), floored at −1.
TP1 / F
R(close at resolve row), floored at −1.
loss α = max(−R, 0) × bet
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

Address
0xD9c805202b16671A2901307fBC9A8750E2453427
Chain
964 (Bittensor EVM). RPC default https://lite.chain.opentensor.ai
Deploy
block 8,859,422 (2026-08-16)
periodZero
1,787,004,000 = 2026-08-17 22:00:00 UTC. Immutable.
netuid
123, immutable
Owner
0x5186318Ba00Ca115d92C37D2b646eA3867C2c754
Mirror coldkey
0x379d4712e9902a9ca2ba1b827f4cb2c48ec03b510b107bf700c92592d9df067a (config; on-chain via setContractColdkey, one-shot)
Disable
FLOW_COLLATERAL_ADDRESS=off forces f-only weighting

Alpha 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

currentPeriod = 0 if now < periodZero
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

tradeKey = (regime << 32) | trade_id
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

room = (bal − 3d) / 4   if bal > 3d else 0
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.

FUND hotkey-signed POST size + key only CLOSE ≤ bet, ≤ 1/4 wk SETTLE week + ended SWEEP permissionless

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).

book
Binance spot BTCUSDT depth 5000. Best bid/ask, mid, spread bps, bid/ask depth and imbalance inside 25 / 50 / 100 bps of mid.
flow
aggTrades over the interval: taker buy/sell, imbalance, trade count, large (≥ 1 BTC) count, VWAP. truncated if 1000-trade cap hit.
deriv
USD-M mark, last funding, open interest.
prices
Optional validator price map, when the caller supplies it.

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

FileRole
config.pyChallenge table, network constants, encryption pins, FLOW collateral addresses, env pinning.
validator.pySample loop, weight calc thread, EMA 0.3, young-UID floor, burn, set_weights.
cycle.pyCommitment read, R2 allowlist, 25 MB cap, batch-32 fetch.
comms.pyV2 schema check, size probe, download.
generate_and_encrypt.pyMiner-side V2 envelope. Supported submission surface with flow_post.py.
ledger.pySQLite, price-at-arrival, maturity decrypt (public path only), sanitizers, VACUUM INTO.
model.pymulti_salience dispatch, binary ElasticNet, clone collapse, LBFGS 75/25 blend.
hitfirst.pyBarrier race, 4-block LBO L2.
bucket_forecast.pyLBFGS classifier + Q-path, uniqueness penalty, inner top-25.
range_breakout.pyPer-asset state machine, episode-balanced logistic.
xsec_rank.pyMedian-beat label, top-20, 20×120 bootstrap.
funding_xsec.pySame meta on funding Δ, sidx pairing, stale-std zero, embargo max(LAG, ahead).
flow.pyDecode, resolve, gate, hourly pay, settlement batch, trade_events.
flow_collateral.pyABI client, trade_key, 900 s grace, bet pricer, rao pro-rata.
flow_post.pyMiner CLI: fund / evict / book / post / status / audit.
microstructure.pyHash-chained JSONL, unused in scoring.
../flow_collateral/src/FlowCollateralPool.solThe 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.

b = 0.02 / 0.02 = 1
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.

Follow the capital See the product