SNAPFLY

Documentation

What the brain is, what it is shown, what is read out of it, and — the part that matters most — what it demonstrably cannot do.

model version snapfly-malecns1.0-lif-0.3.0 · figures generated 2026-09-13 08:57:04+00:00 · gate run 2026-09-12 11:33:30 on cpu

every figure below is read from a generated file, not written into the page — brain/export/build_figures.py

01

What this is

A real fly brain, wired exactly as it was measured, shown your photograph.

SNAPFLY simulates the visual system of one male Drosophila melanogaster whose entire nervous system was reconstructed from electron microscopy. Every neuron, every connection between them, every synapse count and every excitatory or inhibitory sign in this model is taken from that measurement. Nothing is learned, nothing is fitted, nothing is invented.

You upload a photograph. It is projected onto his right eye as light, run through the optic lobe and the central brain for 700 simulated milliseconds, and what comes back is a vector of millivolts: how hard each of his object-detecting cell types was driven by your image, minus how hard the same cells were driven by a blank field of the same brightness. That vector is hashed. The hash becomes a ticker. If he responded at all, a token launches.

SEEN does not mean recognition. It means his optic lobe responded to your photograph above a measured noise floor. Section 7 shows, with the numbers, that no channel in this model separates photographs of flies from photographs of anything else. We tested it, it failed, and we did not invent a threshold to hide that.

A note on how this page is built. Three figures published here were wrong before, in the same way each time: a number reported without saying which selection produced it. Every figure now comes out of a generated file, each table names the run and the readout it was taken from, and a figure that cannot be sourced does not appear. Section 7 carries the corrections.

02

The connectome

MaleCNS v1.0 — a complete central nervous system of an adult male fly, released under CC-BY.

Released by HHMI Janelia Research Campus (FlyEM), the Cambridge Drosophila Connectomics Group at the MRC Laboratory of Molecular Biology, and Google Research. It covers the brain and the ventral nerve cord of a single male animal, so the optic lobes and the motor circuits sit in the same graph as the central brain. This export is dated 2026-09-11.

count
neurons kept (status Traced)165,122
connections (weight >= 3)10,511,038
synapses in those connections104,213,652
distinct cell types11,751
neurons with a soma140,024
neuropil regions (ROIs)146
source · data/malecns_v1/MANIFEST.json

Why the bulk release and not the API. The public neuPrint REST service at neuprint.janelia.org serves male-cns:v0.9. The flat-connectome feather files in the v1.0 release bucket are the current version, so the dataset is built from those. Two filters are applied and no others — status == Traced and synapse weight ≥ 3.

Neurotransmitter. The sign of every edge comes from consensus_nt, falling back to celltype_predicted_nt and then predicted_nt. This choice matters: predicted_nt alone leaves most bodies marked unclear, which would have meant guessing the sign of most of the brain. Here 98.5 % resolve to a named transmitter.

transmitterneuronssign
acetylcholine104,040excitatory
glutamate29,616inhibitory
GABA22,108inhibitory
histamine5,924inhibitory
unclear2,494excitatory (default)
serotonin404excitatory
dopamine395excitatory
octopamine141excitatory
source · data/malecns_v1/MANIFEST.json

Two E:I ratios, and they are not the same number. By cell count it is 1.86 : 1. Weighted by synapses it is 1.62 : 1, because inhibitory cells here are on average more strongly connected. Both are correct; quoting either without saying which is how this kind of figure goes wrong.

source · brain/export/build_figures.py · recomputed from edges.parquet

Glutamate counts as inhibitory because in the fly optic lobe it acts on GluCl channels — Mi9, the slow arm of the ON motion detector, is glutamatergic and inhibitory. Histamine is the photoreceptor transmitter and opens chloride channels on the lamina monopolar cells, so light silences them.

Neuropil. The release carries no per-body region column, so each neuron is assigned the modal primary_post ROI over its own postsynaptic sites, computed by streaming the full synapse-partner table. 164,720 of 165,122 neurons get a region this way.

regionneurons
optic lobe102,617
central brain39,637
ventral nerve cord22,466
unassigned402
source · data/malecns_v1/MANIFEST.json · counts.by_region
03

The eye

Not an idealised lattice. The animal's own columns, as the dataset assigns them.

MaleCNS gives hexagonal column coordinates — assignedOlHex1 and assignedOlHex2 — to lamina cells on the right side only. That is why he is monocular: the left eye has no ground-truth column map in this release, and inventing one would mean fitting geometry we claim not to fit.

On the right, L1, L2 and L3 occupy exactly the same 892 columns, one cell of each type per column. No clustering, no lattice fitting, no interpolation. Laid out on the hex axes and scaled by the interommatidial angle of 5.8°, those columns span a field of view of 194.7°.

Why the lamina and not the photoreceptors. R1–R8 carry no hex column assignment in this dataset. They cannot be the input layer without inventing their positions, so light is injected one synapse downstream, onto L1/L2/L3, as the shunting conductance histamine actually produces there. R7 and R8 are unused, so there is no colour.

ommatidium acceptance 5 deg FWHM, Gaussian interommatidial angle 5.8 deg sigma / spacing 0.366 columns 892, right eye field of view 194.7 deg
source · brain/retina/eye.py, brain/retina/lattice.py

Luminance is Rec. 709 luma under a fixed global normalisation. No per-image autocontrast: a dark photograph stays dark, because auto-levelling would make the verdict depend on a preprocessing step rather than on the image.

Adaptation. Photoreceptors and lamina monopolar cells track local contrast, not absolute brightness. Each ommatidium keeps a slowly adapting local mean with a time constant of 300 ms, and the drive is Weber contrast against it:

c = (lum - a) / (a + 0.05) a = local mean, tau 300 ms, pooled over a Gaussian surround of sigma = 4 ommatidia
source · brain/retina/protocol.py

The spatial surround is load-bearing, not decoration. Purely temporal adaptation would make a static dark disc fade to invisibility within a few hundred milliseconds, because every column would adapt to its own luminance. Pooling over neighbours means a column inside a small dark disc adapts toward its bright surround and stays in strong negative contrast, while a column in the middle of a large dark region adapts to the dark and falls silent. That asymmetry is where small-object selectivity comes from.

04

The model

Two stages, because the two halves of a fly brain do not work the same way.

Columnar optic-lobe neurons do not fire action potentials. They signal with graded potentials around a tonic resting level, and their key inhibition is shunting — it divides rather than subtracts. A current-based spiking model cannot express that at any gain. So the optic lobe is graded and conductance-based, and everything from the LC cells inward spikes.

stageunitsmodel
optic lobe, non-LC98,401graded, conductance-based
LC / LPLC inside the optic lobe4,216leaky integrate-and-fire
central brain39,637leaky integrate-and-fire
ventral nerve cord22,466leaky integrate-and-fire
unassigned402leaky integrate-and-fire
98,401 graded + 66,721 spiking = 165,122.
source · brain/export/build_figures.py · the engine's own rule, graded_mask = (region == OPTIC_LOBE) & ~is_lc

The graded stage. Each unit is a single-compartment conductance model. Voltages are in millivolts relative to rest, so V_rest = 0:

tau_i dV_i/dt = -(V_i - V_rest) + g_e,i (E_exc - V_i) + g_i,i (E_inh - V_i) g_e,i = G * SUM over excitatory j of (n_ij / N_i) r_j g_i,i = G * SUM over inhibitory j of (n_ij / N_i) r_j r_i = relu(V_i - E_inh) E_exc = 10 mV E_inh = -1 mV G = 4
source · brain/engine/hybrid.py

Three things fall out of writing it this way.

  • Division. The steady state is V* = (g_e E_exc + g_i E_inh) / (1 + g_e + g_i). Inhibitory conductance sits in the denominator. That is the whole point: T4's null-direction suppression is shunting. Mi9 and Mi4 do not subtract from Mi1's excitation, they load the cell down and divide it.
  • Stability for free. V is confined to [E_inh, E_exc] by construction, so the stage cannot run away however large G is. G is an operating point, not a safety margin.
  • A rectifier that matters. r = relu(V − E_inh) makes the output the driving force above the inhibitory reversal: 1.0 at rest, exactly 0 when a cell is fully shunted.

N_i is each neuron's total incoming synapse count, so a conductance is a fraction of that cell's own input rather than an unbounded sum. Convergence in this graph spans three orders of magnitude; normalising removes that spread while leaving every sign and every relative weight exactly as the connectome has them.

The spiking stage. Current-based leaky integrate-and-fire, after Shiu, Sterne et al., Nature 2024.

parametervalue
integration step0.1 ms
membrane time constant20 ms
synaptic decay5 ms
resting potential-52 mV
threshold-45 mV
refractory period2.2 ms
axonal + synaptic delay1.8 ms (a 19-step line)
peak PSP of one synapse0.275 mV
source · brain/engine/params.py

Two implementation details are worth stating, because both were bugs first. The delay line is 19 steps, not one fewer: arriving current is added and integrated within the same step. And membrane potential is carried as depolarisation from rest, not as absolute millivolts — a small PSP sitting on a -52 mV baseline in float32 is coarser than the signal itself.

Time constants. Not fitted. Taken from the published fast/slow division of T4/T5's input elements.

cellstausource
L1, L2 / L315 / 25 msBehnia 2014
Mi1, Tm3 — fast ON25 msBehnia 2014; Arenz 2017
Mi4 / Mi9 — slow ON150 / 300 msArenz 2017; Strother 2017
Tm1, Tm2 — fast OFF25 msBehnia 2014
Tm9 — slow OFF300 msSerbe 2016
CT1200 msMeier & Borst 2019
everything else50 msdefault
source · brain/engine/hybrid.py · TAU_BY_TYPE

One deliberate departure. The original specification grouped Tm1, Tm2 and Tm9 together as slow. Here they are fast, fast and slow. Tm1 and Tm2 are the transient OFF arm feeding T5 and Tm9 is the sustained one; making all three slow would remove the OFF pathway's delay line entirely and leave T5 with no mechanism at all.

The three gains, and how each was set.

gainvaluehow it was set
central brain0.06measured stability limit
graded stage4.0operating point
interface1.6derived from threshold
source · brain/engine/params.py · GAIN; brain/engine/hybrid.py · G_GRADED, G_INTERFACE

The published Shiu gain of 1.0 was fitted on the female FlyWire graph. This is not that graph. At weight ≥ 3, with both optic lobes and the whole ventral nerve cord included, the male graph averages 149.1 net signed synapses of input per neuron. One synapse delivers 8.64 mV·ms of charge at gain 1, and reaching threshold takes 140.0 mV·ms, so the network is self-sustaining above

g_crit = 140.0 / (149.1 * 8.64) = 0.109
source · brain/export/build_figures.py · recomputed from edges.parquet

Which convention this uses. Uses the DISCRETE PSP peak factor, which is what the engine normalises against (0.159070). The closed-form continuous factor (0.157490) would give 8.73 and a critical gain of 0.108; that is the figure the Phase 0 report quoted.

The measured transition sits at that value, and the shipped central gain of 0.06 is well below it. A current-based LIF has none of the machinery a real brain uses to stay stable — no spike-frequency adaptation, no synaptic depression, no conductance-based shunting — so the global gain has to absorb all of it.

The interface gain is derived, not swept. It is fixed by requiring that the graded stage's tonic output leaves every LC/LPLC cell below threshold, so darkness is silent by construction and every spike downstream is stimulus-driven. The most strongly driven LC cell receives 4.119 mV of tonic interface current per unit of this gain, against a 7.0 mV swing from rest to threshold, giving a ceiling of 1.70; 1.6 is that value with margin.

source · brain/export/build_figures.py · graded stage settled in darkness, max over LC/LPLC of w_sg @ relu(v_eq - E_inh), per unit interface gain
05

How he looks at your photograph

A fixed, deterministic schedule, identical for every image.

0 ms 100 ms 600 ms 700 ms |--------|-----------------------------------|--------| dark approach: sampling window 1.00 -> 0.80 held |<--------- readout window 100 -> 700 ms ---------->| preceded by 500 ms of pre-adaptation at the mean field frames are 1 ms; no jitter, no noise, no sampling
source · brain/retina/protocol.py; brain/pipeline.py · PRE_ADAPT_MS

The pre-adaptation settles the Weber adaptation state, so the first frame of the photograph is not read as a step change in brightness. Darkness follows, then the image, then an approach in which the sampling window shrinks and is then held.

The window shrinks; the photograph does not grow. The original design had the photo expanding to fill the visual field. That needs a background behind it, and the photo's own rectangular border then becomes a hard edge sweeping outward across the eye — a textbook looming stimulus that every single image would generate regardless of what is in it. So the picture always covers the whole eye and the eye moves into it. Same schedule, same magnification, but the looming signal now comes from the photograph rather than from its frame.

Light reaches L1, L2 and L3 as a shunting conductance, not as a current, because that is what histamine does to those cells. L1 and L2 carry the transient component — added for L1, subtracted for L2; L3 carries the sustained component only.

06

What we read

Drive, not spikes — because in this model the cells we care about never reach threshold.

The fingerprint is the mean synaptic current, in millivolts, that the graded optic lobe holds on each LC and LPLC cell of the right hemisphere over the readout window. Not their spike counts. In this model those cells do not reach the firing threshold, so reading spikes would read a vector of zeros.

This is a documented readout choice, not a tuned parameter. No gain was changed to produce it, and the model still passes its silence-in-darkness test at exactly the same settings.

Differential. Every photograph is run twice — once as itself, and once as a uniform field at the same mean luminance, same protocol, same everything — and the recorded quantity is the difference. Tonic activity cancels exactly.

Receptive fields. A cell is only read where its receptive field is known. The hex column map is propagated forward through the connectome graph, so each downstream neuron inherits a receptive-field centroid from the columns that ultimately feed it. This covers 163,168 of 165,122 neurons.

source · docs/PHASE1D_REPORT.md §1

The verdict rule. Two verdicts. There is no third.

total_drive = SUM over types of |drive_type| total_drive < 4.86e-7 mV -> UNSEEN total_drive >= 4.86e-7 mV -> SEEN
source · brain/verdict.py · NOISE_FLOOR_MV

That floor is measured from noise, not chosen, and not measured from photographs:

controlwhat it ismax total drive
Aidentical blanks, differenced against themselves3.089e-08
Buniform fields differing 1% in mean brightness1.620e-07
Floor = 3 × the larger = 4.86e-07 mV, over 20 pairs of each.
source · docs/noise_floor.json

Where images actually fall:

imagetotal drive× floorverdict
structureless synthetic field (control B)2.23e-08 – 1.62e-070.05 – 0.33UNSEEN
near-uniform wall — flattest photograph1.48e-033,046SEEN
cloudless sky8.37e-0317,214SEEN
Drosophila, side view3.73e-0276,645SEEN
hand, filling the frame1.01e-01207,823SEEN
Each row names the fixture that produced it.
source · docs/fixtures_drive.json; docs/noise_floor.json

A featureless synthetic image sits orders of magnitude below the least structured real photograph, so UNSEEN is reachable and means something. The sky stays SEEN — a gradient is structure, and the floor was not moved to exclude it.

SEEN is otherwise unconditional. There is a second threshold in the code, on the small-object channels, and it is set to null. None means SEEN is unconditional: no threshold separated the classes, so none was invented. Section 7 is why.

07

What he cannot do

Wiring and signs are not enough. The selectivity lives in parameters the connectome does not contain.

This is the most important section on the site. Everything in it is a negative result, and every one of them is a result we published rather than tuned away.

Including our own measurement errors. We audited every figure we had published for where it came from — which selection produced it, and which run. That audit found five defects, two of them gates of ours that reported PASS when they should have reported FAIL. All five are listed further down, in full, with what each one changed. Nothing here was found by anyone else, and nothing was quietly corrected: the figures that were wrong are named alongside the figures that replaced them.

Direction selectivity is absent. A drifting grating in eight directions, direction-selectivity index measured per subtype:

cellDSI (vector)DSI (pref/null)
T4a0.00120.0043
T4b0.00110.0039
T4c0.00130.0045
T4d0.00130.0045
T5a0.00030.0003
T5b0.00020.0002
T5c0.00020.0002
T5d0.00020.0002
Roughly 0.5 in the animal.
source · docs/gates_final.json · g4

And yet the detector is right there in the graph. Each T4 cell's input arms, measured relative to its own Mi1 centre, in hexagonal columns:

subtypeMi4 offset (slow)Mi9 offset (slow)
T4a(+0.717, -0.322)(-0.860, +0.393)
T4b(-0.829, +0.354)(+0.866, -0.388)
T4c(+0.200, +0.739)(-0.234, -0.763)
T4d(-0.499, -0.750)(+0.537, +0.635)
Synapse-weighted input centroid of each arm relative to the same cell's own Mi1 centre, in hexagonal columns.
source · brain/export/build_figures.py · recomputed from edges.parquet and the propagated receptive-field map

Read that table as a new claim, not as one we have made before. An earlier version of this document carried different numbers for it — numbers that existed in no script and that we could not reproduce. The table above is recomputed from the edge list every time this page is built, on a different basis, which is stated here in full so that you can recompute it yourself:

elementchoice
origineach T4 cell's own Mi1 input centroid, synapse-weighted, per cell
framerf.xy — right-eye hex lattice projected to 2-D, normalised to outer radius 1
unitone hexagonal column (divided by the median nearest-neighbour spacing)
reductionper-cell arm centroid minus that cell's own Mi1 origin, then averaged over the subtype
bearingatan2(dy, dx) mod 360 of the mean Mi4 offset
arm angleunsigned angle between the mean Mi4 and mean Mi9 vectors
Other defensible bases exist — subtype-level centroids differenced instead of per-cell differences averaged, cells weighted equally rather than by synapse count, or raw hex axial coordinates. Per-cell differencing is used because it is the only one that asks where an arm sits relative to its own centre.
source · brain/export/build_figures.py · t4_offsets docstring

The claim usually made about the offsets is that the two slow arms sit on opposite sides of Mi1 and that the axis rotates through the subtypes. That is measurable, so it is measured rather than asserted — and it survives the change of basis:

subtypeangle between the two slow armsaxis bearing
T4a179.6°335.8°
T4b179.0°156.9°
T4c178.1°74.9°
T4d173.4°236.4°
180° would be exactly opposite sides of Mi1. The bearings are what 'the axis rotates through the subtypes' means, measured.
source · brain/export/build_figures.py · derived from t4_offsets

All four subtypes put their two slow arms within a few degrees of exactly opposite, and the axis turns between subtypes. Mi9 is glutamatergic and Mi4 GABAergic — both inhibitory; Mi1 and Tm3 are cholinergic. That is an elementary motion detector, correctly wired and correctly signed, sitting in the graph. The model still does not compute with it.

A connectome tells you what is connected to what, and with what sign. It does not tell you how strong each synapse is relative to its neighbours, and for these circuits that is where the computation lives. Published models that do reproduce direction selectivity from connectome data get there by optimising those unknown parameters with deep learning against a motion task. That is a legitimate and impressive result, and it is a different claim from this one: their selectivity is trained in, not read off the wiring.

The escape reflex does not fire. An expanding dark disc against a contracting one:

loomingreceding
Giant Fiber (DNp01) spikes00
LPLC2 differential rate, Hz+0.000+0.000
LPLC1 differential rate, Hz+0.000+0.000
LC4 differential rate, Hz+0.000+0.000
LC6 differential rate, Hz+0.000+0.000
whole-network spikes9,7129,719
Gate G2: FAIL.
source · docs/gates_final.json · g2

A third verdict, FLED, was designed around that neuron firing. It was removed rather than replaced with a hand-written looming detector, which would have meant a classifier deciding in his name.

What does work. A dark disc on a light field, static, differential drive in millivolts, averaged over the cells whose receptive fields actually contain the disc. Gate G1: PASS, on LC11, LC10a.

channel10°20°40°80°120°peak/large
LC11+0.00711+0.00185+0.00020-0.0056336.1
LC10a+0.00005+0.00058+0.00026+0.00006-0.000489.0
LC18+0.00023+0.00068+0.00046+0.00029large
LC4+0.00008+0.00147+0.00833+0.00894+0.00901large
LPLC2+0.00045+0.00578+0.00750+0.01093+0.01046large
RF-matched MEAN throughout — the readout the gate is decided on. A dash is undefined, not zero: no cell of that type has a receptive-field centre within the stimulus.
source · docs/gates_final.json · g1.per_type[...].rf_mean

LC11 and LC10a peak at 20° and fall away — genuine small-object tuning, in the cells the literature says should have it, from wiring and signs alone. LC4 and LPLC2 prefer large objects, which is the right way round for looming detectors. Nothing was tuned to produce either behaviour, and the two families are fed by the same retina through the same gains.

How many cells each of those averages is over — the caveat that matters most about that table:

channel10°20°40°80°120°
LC110251639
LC10a1363268
LC180272572
LC41141430
LPLC21282553
Cells averaged at each size. The 20° peak rests on very few.
source · docs/gates_final.json · g1.per_type[...].rf_mean.n_rf

Five bugs of our own, in our own measurements. All five are the same shape: a number reported without saying which selection, or which run, produced it. They are recorded because catching them changed what we publish, and because the fourth is what made the first three possible.

  • — The looming gate first reported PASS. It computed a looming/receding ratio of inf from a negative denominator. Both legs were noise around zero. The check now treats a non-positive leg as undefined, and the gate reads FAIL.
  • — The size-tuning gate had the same trap, on LC10d. The code was fixed — but the stored gate output was never regenerated, so the published list of small-object types kept LC10d in it on the strength of an inf the fixed gate cannot produce. Rerun, LC10d drops out. G1 still passes, on LC11 and LC10a.
  • — The size-tuning table mixed two readouts: three rows from the RF-matched mean and two from the RF-matched max, which doubled the apparent LC4 and LPLC2 responses. The table above is the RF-matched mean throughout, which is the readout the gate is decided on.
  • “Our flattest photograph, a cloudless sky” was a different fixture — a near-uniform wall. The sky is several times higher. Both now appear in the scale table above, each named.
  • Two gates were published from a different run than the other two: looming and direction selectivity from one file, size tuning from another, measured under different settings. All four gates now come from one run, and the generator refuses to build if any is missing from it.
  • — And one that is not a defect but a gap: the T4 offset table could not be reproduced. It was cited as evidence that the direction-selective architecture is present, and it existed in no script — so there was no way to check it. It has been replaced by a generated table computed on the basis given above, whose numbers differ from the original. The claim it was cited for survives: the two slow arms are 173.4–179.6° apart in all four subtypes, and the axis rotates. The numbers were not swapped quietly; this is the notice.

The same failure, in someone else's system. Four facts we had stored about the launchpad turned out to be stale — the factory address, the interface we transcribed from it, the locker, and a function we intended to call — all copied from one source that had been correct once. None was found by re-reading our notes, because a value written into a file stops being questioned: it looks like something already checked. They were found by asking the chain what was actually happening that day, and getting a different answer.

So the rule we adopted is the same shape as the one above. No launchpad address is stored unless it was read from a getter on the live contract, and the entry records which getter. Our own deployed addresses are held to it too: the launcher checks the bytecode at each one before it will send anything, and refuses if the address is empty or holds something it does not recognise. A figure and an address fail the same way — quietly, by looking settled — which is why neither is allowed to be typed.

The fourth needed a second rule, because it was not an address. We had documentation for a collectFees function the creator calls to collect their fees, and we had written the plan, the launcher and this site's income figure against it. It does not exist. Not on the factory, the locker, the hook or the escrow — the name is in none of their published interfaces and its four-byte selector is in none of their deployed code. An address at least fails loudly when you call it; a function that was never there is simply absent, and everything built on it reads as finished work.

So a function we intend to call must be shown to exist in a verified interface, and exercised against the real contracts on a fork, before anything is built on it. Doing that is how the real mechanism was found: Pons sweeps a pool's fees into an escrow, the escrow credits the creator, and the creator calls claim() on it themselves. The fly is paid, by its own signature — just not by the function we had written down. The figure on /launches now counts what that escrow actually paid out.

The cause of the third was that the gate printed both readouts as two tables with identical headers, so they could be read as one. They are now labelled with which is the gate and which is reporting only, and no figure reaches this page without passing through the generated file — a key that is not in it fails the build rather than rendering blank.

And it does not generalise to photographs. 19 real CC-licensed images — 5 fly, 5 looming, 5 other, 4 uniform:

channelfly photoseverything elseseparable
LC11[-0.00070, +0.00070][-0.00616, +0.00734]no
LC10a[+0.00000, +0.00008][-0.00038, +0.00038]no
LC10b[-0.00014, +0.00038][-0.00003, +0.00053]no
LC18[-0.00011, +0.00022][-0.00009, +0.00012]no
small-object mean[-0.00002, +0.00019][-0.00055, +0.00108]no
total drive[+0.01633, +0.03725][+0.00148, +0.10101]no
Every fly range sits inside its non-fly range. No channel separates.
source · docs/fixtures_drive.json · separation

The circuit has small-object tuning for a clean disc on a uniform field. A photograph of a fly is not that: it is a fly plus a background, and the drive reflects the whole image. So the small-object threshold is None, SEEN is unconditional, and no cutoff was chosen. The honest statement is that he responds to your photograph — not that he recognised what is in it.

Stability, for completeness. With no input the network is quiet: 3,819 spikes over the whole dark run, and a tail of 4.9 spikes/ms. A driven stimulus never puts more than 0.0085% of cells into simultaneous activity. Gate G3: PASS.

source · docs/gates_final.json · g3
08

Determinism

Same bytes in, same fingerprint out, forever, for a given model version.

There is no randomness anywhere in the presentation: no jitter, no noise, no dropout, no sampling. There is no plasticity — nothing in the model changes as a result of having seen an image, which is deliberate, because plasticity would mean the tenth photograph got a different answer from the first.

The image is hashed with SHA-256 in your browser, before upload. That hash is the cache key: an image already judged returns its stored verdict rather than being re-run, and an image already launched can never be launched a second time. It is also the salt Pons receives, so the same photograph always produces the same token address.

Anything that could change an answer bumps MODEL_VERSION, and the version in force is written into the token metadata at launch. That list is: the connectome export and its filters, the neurotransmitter sign rule, the retina lattice or acceptance function, any of the three gains, the time constants, the presentation protocol, the readout window, the noise floor, and the fingerprint's canonical form.

MODEL_VERSION = "snapfly-malecns1.0-lif-0.3.0"
source · brain/engine/params.py

The float32 sparse reductions inside the engine are not bitwise reproducible across devices — that is the entire reason control A in section 6 is not zero. The fingerprint is rounded to six decimal places before hashing, orders of magnitude above that noise, so the hash is stable even though the raw sum is not.

He is judged on a CPU, and that is a determinism decision rather than a hardware one. On CUDA the same photograph does not produce the same numbers twice: index_add_ accumulates spike arrivals with atomics, and cuSPARSE's sparse matrix–vector product is non-deterministic for the three largest matrices in the model. We measured the spread at up to 1 × 10⁻⁸ mV. That is small, but it is not small enough against the rounding above: it would flip a fingerprint channel in roughly 3.3% of re-judges, and a flipped channel is a different ticker for the same photograph. PyTorch's determinism flag fixes the first cause, not the second, and raises no error about it. A CPU is bit-identical across processes, so he looks at photographs three times more slowly than the hardware allows.

09

The ticker

The name of the token is a function of the drive vector. Nobody chooses it.

canonical = JSON of {type: round(drive, 6)}, keys sorted fp_hash = sha256(canonical + MODEL_VERSION) value = int(fp_hash, 16) letters = base-26 of value, A=0 ... Z=25 ticker = first 5 letters, widening to 6 then 7 on collision or reserved word
source · brain/verdict.py · canonical_vector, fp_hash, choose_ticker

Worked example — a fixture photograph of a Drosophila, side view:

stepvalue
fixturefly_01
total drive0.03725 mV
peak channelLC35b, +0.01429 mV
model versionsnapfly-malecns1.0-lif-0.3.0
fp_hashc13efc0d0f573c757c9e248549a04ed437012ddece33096d65ce690a6225ef42
base-26 candidatesEXMTE, EXMTEG, EXMTEGN
tickerEXMTE
The drive vector is what the brain produced; the hash and ticker are derived from it and the model version, so both move when the version does. Under the version that produced this run the hash was 7ce31b3388bf5bea… and the ticker was four letters.
source · brain/export/build_figures.py · recomputed from docs/fixtures_drive.json rows[fly_01].lc_drive under the current MODEL_VERSION

The ticker was four letters until model version snapfly-malecns1.0-lif-0.3.0. If you computed a four-letter ticker for an image before that, this is the explanation rather than a contradiction: the length changed, so the ticker for every fingerprint changed with it. Nothing about the model or the drive vector moved — but fp_hash is sha256(drive + MODEL_VERSION), so a version bump changes the hash, and the hash is the ticker.

The reason is occupancy. Pons was measured at 22,909 launches a day, and four base-26 letters is only 456,976 strings:

namespacepossibilitiesafter 1 dayafter 7 daysafter 30 daysafter 1 year
4 letters456,9764.9%29.6%77.8%~100%
5 letters11,881,3760.2%1.3%5.6%50.5%
6 letters308,915,7760.0%0.1%0.2%2.7%
Probability that a uniformly random ticker of that length is already in use, assuming uniform occupancy. Two caveats keep this from being more certain than it looks: ERC-20 symbols are not unique on chain, so a collision costs distinctiveness rather than causing a failure; and real symbols cluster on pronounceable words while ours are uniform over the space, so this overstates the pressure on a hash-derived string.
source · measured on Robinhood Chain 4663, 2026-09-12: 522 Pons launches in a 33-minute window = 22,909/day

Two things keep that table from meaning more than it does. Symbols are not unique on chain, so a collision costs distinctiveness — someone can launch a lookalike — rather than causing a failure; our launch would still succeed. And real symbols cluster on pronounceable words (MOSS, TREE, TABS) while ours are uniform over the whole space, so uniform-occupancy arithmetic overstates the pressure on a hash-derived string. Five letters is cheap insurance either way; it is not a precise calculation.

On a collision with a ticker already launched, or with one of the 44 reserved strings — chain tickers, major assets, and words such as SEEN and NULL that would read as a system message — the candidate widens to five letters and then six.

10

The launch

The fly pays for the launch out of his own wallet, and the fees come back to it.

Tokens launch on Robinhood Chain (chainId 4663) through the Pons V2 launchpad. A launch runs a bonding curve and then graduates into a permanently locked Uniswap v4 position held by the Pons locker.

Uploading costs nothing and needs no wallet. You are not asked to connect anything, sign anything or pay anything. He looks at the photograph, and if he responds you may ask him to launch it.

The fly pays the launch fee 0.0005 ETH, from his own wallet — and is the creator fee recipient on every token he launches. Pons keeps 30% of creator fees; the rest arrives at the fly wallet and pays for further launches.

source · read 2026-09-12 from memeHook.currentFeePolicy() on chain 4663, decoded with the hook's own verified ABI and cross-checked against protocolFeeShareBps(); see contracts/PONS_V2_NOTES.md

Nothing is distributed to anyone who uploads a photograph. There is no share, no split, no claim and no payout. If you give a wallet address when you ask him to launch, it is recorded as attribution and nothing else — it is not paid, and no mechanism exists by which it could be. We state this flatly rather than leaving it unsaid.

Your name, not ours, and not a claim about you. A launch is credited to a display name you choose, recorded as submitted by. It is not “launcher”: we sign the transaction and we pay the fee, so we launched it. The name goes into the token's on-chain metadata, where it is permanent and cannot be edited or removed by anyone, including us — which is why names are checked before a launch rather than moderated after one.

We take nothing from trades. Pons lets a launcher set creatorTaxBps, an extra tax on every trade of up to 1,000 basis points, paid entirely to the creator. Ours is 0, by choice and not by default: a trade tax would make the fly an extractive party in every transaction on a token he launched.

A guard on the terms. Immediately before sending, the launcher reads previewLaunchEconomics and passes the digest back as expectedEconomics. If Pons changes the launch config between our quote and our send, the launch reverts rather than executing on terms nobody read. Passing zero would waive that check silently, so the launcher refuses to construct one.

There was a splitter here, and there is not any more. An earlier version of this project paid uploaders half the creator fees through an immutable, non-custodial contract — one CREATE2 clone per specimen, no owner, no pause, no withdrawal function, with the address computable before the uploader paid anything. When uploads became walletless there was no uploader wallet to pay, so there was nothing to split, and it was removed rather than left in the repository for someone to assume was live. It is in the git history, and the two contracts deployed to the testnet are abandoned and hold nothing. Nothing was ever deployed to mainnet. This paragraph exists because the tests and the commit log still mention it, and an unexplained absence is worse than a recorded one.

11

What we do not do

The list of things that are not in the box, stated so you do not have to take it on faith.

  • No LLM. No language model reads your image, names your token, or writes anything about it.
  • No classifier. Nothing is trained to recognise a fly, or anything else. There is no training set.
  • No fitted parameters. Every constant in section 4 is either taken from a published measurement, derived analytically from another constant, or set against a stability limit that was measured. None was swept against an outcome we wanted.
  • No plasticity. It would break determinism.
  • The fly never trades. His wallet can call exactly two things: launch a token, and claim creator fees. No swap path exists anywhere in the codebase.
  • No EXIF is uploaded. The photo is re-encoded to JPEG in your browser before anything leaves the device, which strips metadata as a side effect.
  • Geotags are opt-in. GPS is read from the EXIF before the strip and returned separately, so coordinates only leave your device if you tick the box.
  • No colour, no second eye. R7 and R8 are unused; the left optic lobe has no ground-truth column map in this release.
  • No hand-typed figures. Every number on this page comes from brain/export/build_figures.py. A figure that cannot be sourced from the file it generates fails the build.
source · web/lib/image.ts; brain/verdict.py; web/components/Figure.tsx
12

Credits and licence

None of this is our data. Almost none of it is our maths.

  • Connectome. MaleCNS v1.0 — HHMI Janelia Research Campus (FlyEM), the Cambridge Drosophila Connectomics Group at the MRC Laboratory of Molecular Biology, and Google Research. Released under CC-BY 4.0. Used here as the bulk flat-connectome release, exported 2026-09-11.
  • Spiking model. Shiu, Sterne et al., Nature 2024.
  • Graded formulation. Lappalainen et al., Nature 2024 (flyvis). The equations only. None of its connectome and none of its trained weights are used here. Their model obtains its direction selectivity by optimising free parameters against a motion task; this one has no training step, which is why section 7 reads the way it does.
  • Time constants. Behnia et al., Nature 2014, 512:427. Arenz et al., Current Biology 2017, 27:929. Strother et al., Neuron 2017, 94:168. Serbe et al., Neuron 2016, 89:829. Meier & Borst 2019.
  • Adaptation. Laughlin 1981; Matulis et al. 2020.
  • Retina geometry. NeLy-EPFL flyvis, for the acceptance-angle maths. The lattice itself is the dataset's own 892 columns.
  • Prior art. FlyWire — Dorkenwald et al., Schlegel et al., Nature 2024.
  • Launchpad. Pons V2 on Robinhood Chain.
  • Fixture photographs. Wikimedia Commons contributors, individually credited in fixtures/CREDITS.json.
13

The API

Everything the site knows about a specimen, as JSON, without asking us.

Three read-only endpoints, no key, no sign-up, CORS open to any origin. They return the same rows the pages render, so anything you can see on /launches or a specimen page you can also fetch. Responses are cached at the edge for a few seconds; these rows change when the fly judges something, not per request.

GET /api/specimens every specimen, newest first ?limit= 50, max 200 ?offset= 0 ?verdict= SEEN | UNSEEN ?state= judged | launching | launched GET /api/specimen/:ticker one specimen, by ticker GET /api/stats counts, caps and limits

A specimen carries its number, its ticker, the verdict, the full differential drive vector in millivolts, the fingerprint hash the ticker was derived from, the photograph, and the token addresses once it has launched. The drive vector is the actual readout, not a summary of it, so the fingerprint hash in section 8 can be recomputed from what this returns.

{ "specimen": "#001", "ticker": "EXMTE", "verdict": "SEEN", "state": "launched", "image_sha256": "2f257e0a...", "fingerprint_sha256": "c13efc0d...", "drive_mv": { "LC11": 0.00071, "LC35b": 0.01429, ... }, "total_drive_mv": 0.03725, "peak_type": "LC35b", "token_address": "0x...", "submitted_by": "a passerby", "location": { "lat": 51.5, "lng": -0.1, "precision_deg": 0.1 }, "created_at": "..." }

What the API will not give you. A specimen hidden by moderation is absent from the listing and a 404 by ticker — there is no parameter that reveals one, and a 404 rather than a 403 so the API cannot be used to confirm that a particular ticker was moderated. Geotags are rounded to a tenth of a degree, the same as the map, so the API cannot recover a position the map will not show. A submitted-by name that has been hidden is null. The attribution address is not returned at all: it is never paid (section 10), and publishing an address beside a token invites exactly the reading that column name exists to prevent.

Limits. Reading is not rate limited. Uploading and launching are, per IP, because we pay the GPU bill and the launch fee out of the fly's own wallet — the upload fee that used to bound this is gone, and these are what replaced it. The live values, and the daily caps, are in /api/stats rather than only here, so a client can read them instead of discovering them by being refused.

judge 3 per hour per IP upload a photograph launch 1 per hour per IP ask him to launch one daily set in admin reported by /api/stats

The IP behind those counters is hashed with a server-side secret and never stored. We need to count requests from an address, not know the address; a table of plain IPs is a log of who looked at what, and we have no use for one.

These three endpoints are meant to be depended on. Everything else under /api — the upload path, the job poller, the admin routes — is internal plumbing, may change without notice, and is not covered by anything said here.