Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Machines that ask emem where they are

Every call on this page was run against https://emem.dev before it was written down, and scripts/example_check.py runs them again on every CI pass. If one stops working the build fails, not the reader.

A robot already knows its own coordinates. What it does not know is what is true at those coordinates: the slope it is about to grade, the wind it is about to spray into, whether the street ahead is clear, how the crop has moved since the last pass. That is a memory problem, and it is the one emem is for.

Two properties matter more to a machine than to a person. Every answer carries a signed receipt, so a second machine can check the first one’s claim without trusting it. And every place has one address, so two machines that resolve “this field” separately arrive at the same cell64 rather than two descriptions that only a human can tell apart.

What emem does not do

It does not drive anything, and it holds no control loop. It answers questions about places and signs the answers. Latency is a fetch, not a tick: a warm recall is milliseconds, a cold one that has to reach an upstream can be seconds. Nothing here belongs inside a safety loop, and a machine that cannot proceed without an answer should carry its own fallback.

It is also not everywhere. The corpus holds thousands of places, not billions. Ask for a cell it has never seen and it will fetch what it can and tell you what it could not.


A street robot, and an autonomous vehicle

Both have the same question at a junction: what is in front of me right now, and is that a measurement or a guess?

curl -s -X POST https://emem.dev/v1/perception/at \
  -H 'content-type: application/json' \
  -d '{"cell":"defi.zb64a.cEjo.zfa47","question":"what is on this street?","stages":["detect"],"detect_from_clip":true}'

That cell is London. detect_from_clip is the part that matters, and it is not the default. Without it the count comes from the camera’s current frame: source: live_frame, tamper_evidence: none, and those bytes are gone the moment they are read, so nobody, including you, can recompute the number afterwards. With it, the count is taken from the retained clip the receipt commits to: source: retained_clip, tamper_evidence: recomputable_from_source, and the response carries the S3 key, the frame index and the hash at camera.clip.clip_sha256. Anyone holding the clip can rerun the detector and get your number. A machine that may have to defend a decision later wants the second one, and it does not cost anything to prefer it: measured across four London cameras, 0.8 s, 0.85 s, 0.88 s and 3.1 s, against 2.9 s for the live-frame form.

Those numbers are worth stating because they were not always true. The same call took 34 to 165 seconds until the inference behind it was serialised, which is why the door allows 120 s (EMEM_PERCEPTION_TIMEOUT_S) rather than the 40 s every other route gets, and returns a typed 504 past that. The ceiling stays where it is: it is sized for the bad day, not the good one, and a shared GPU has bad days.

The response names the camera it used, how far away it was, and why that camera was chosen, because a count from a camera 300 m away is a different claim from a count at the junction you asked about. It also carries camera_requested and camera_substituted: if you name a camera and it is not there, you are told you were given another one rather than quietly handed someone else’s street.

counts never contains a zero

This is the part most likely to be coded against wrongly. There is no zero to watch for, because publishing one would assert an empty street when the truth was that nothing was seen. Four states, and an agent has to tell them apart:

what you getwhat it means
counts: {"car": 11, ...}looked, and found these
counts: {}looked, and found none. This is the empty-street case
counts: null with an unobservable blocknever observed: the frame was not a view of the street. The block says why, and whether retrying helps
counts: null with counts_at_cameracounted, but at a camera too far away to attribute to the point you asked about

An agent waiting for {"car": 0} waits forever.

Is 45 people a lot?

A count on its own cannot answer that, and most machines asking this question need the answer rather than the count. The response carries temporal_context beside counts, which compares what was just seen against what this same camera has seen before:

An excerpt, from the London call above on 2026-09-29:

{
  "temporal_context": {
    "algorithm": "percentile_band@1",
    "basis": "hour_of_day",
    "comparison_to_prefer": "hour_of_day",
    "counted": {"people": 3, "vehicles": 4},
    "counted_higher_than": "47 of 133 previous readings at this camera at this hour of day, over the last 30 days",
    "percentile": 0.387,
    "percentile_ci": [0.309, 0.472],
    "bounded_claim": {"at_confidence": "95%", "says": "below this camera's median"},
    "people": {
      "count": 3, "percentile": 0.549, "percentile_ci": [0.464, 0.631],
      "verdict": "about usual",
      "usual_range": {"p25": 2.0, "median": 3.0, "p75": 5.0},
      "n": 133, "basis": "hour_of_day"
    },
    "provenance_class": "deterministic_index"
  }
}

Read basis before you read the verdict. hour_of_day compares this hour with the same hour on other days, which is the comparison you want. The response also carries an all_hours comparison under comparisons, with a much larger sample, and comparison_to_prefer names hour_of_day or nothing, never all_hours: traffic is strongly diurnal, so a genuinely quiet 3 a.m. reading looks low against an all-hours baseline for a reason that has nothing to do with the street being empty.

n is how many past readings the comparison rests on, and percentile_ci is the interval that sample supports. When the interval crosses a tier boundary the verdict is supported only in direction (verdict_supported_at), and bounded_claim states the strongest thing the whole interval agrees on. counted_higher_than is the exact rank and needs no interval at all. The baseline holds only readings from the same detector (detector_fn_id), because a count from a different model is a different measurement of the same street.

Two limits are stated in the response rather than left for you to discover. not_a_claim_about says this is one camera against its own past and not this street against its neighbours, because two cameras 250 m apart differ by about as much as two 5 km apart. And the band is a percentile of stored history, not a mean and a standard deviation: on a count, which cannot go below zero, a two-sigma low threshold is often a negative number and so can never be crossed, which would leave “unusually quiet” unreachable while every response still read normal. The percentile has no such blind end. emem_explain_algorithm on anomaly_zscore@1 carries that trap in full, if you are choosing between them.

The arithmetic is deterministic_index, not a model output, and recompute carries the query that reproduces it from the signed observations. The observations are signed; this arithmetic over them is not, and adds nothing you could not redo yourself.

A laser leveller

A leveller grades a field to a target slope. Before it starts, the slope that is already there decides how much soil has to move and in which direction.

curl -s -X POST https://emem.dev/v1/terrain \
  -H 'content-type: application/json' \
  -d '{"cell":"defi.zb4e3.vopU.fEyO"}'

Back comes centre_elevation_m, a slope block, ruggedness, and topo_position, each with the cell dimensions they were computed over and the fact ids they were computed from. The honest_note says which neighbourhood was sampled and at what step, because a slope is meaningless without the baseline it was measured across. The default is a 3x3 Cop-DEM neighbourhood at about 29 m pitch (step_cells: 3, matching the 30 m source); 3x3 at 90 m is a different number, and a machine that mixes them will grade to the wrong plane. The DEM carries no bathymetry, so an ocean cell comes back inconclusive rather than flat.

A sprayer

Wind decides whether a pass happens at all. Drift is the whole argument.

curl -s -X POST https://emem.dev/v1/weather \
  -H 'content-type: application/json' \
  -d '{"cell":"defi.zb4e3.vopU.fEyO"}'

The route returns four bands: weather.wind_speed_10m, temperature, precipitation and relative humidity, from an Open-Meteo / MET Norway forecast blend. It takes no band filter, so read the one you need out of bands. The blend is labelled model_output, not a measurement. Every reading carries an age_s. A wind speed is only a decision input while it is fresh, and “the newest we hold” is not the same claim as “current” – the response distinguishes them so a sprayer does not act on a number from yesterday afternoon.

Soil is the other half of the same decision, and the same cell answers on the soil route. At this cell, on 2026-09-29, the SoilGrids bands came back as signed Absences (kind: absence, value: null), which is an answer: the source was asked and had nothing for this point. A read that timed out instead appears in materialize_notes with retryable: true, and a retry a few seconds later usually answers warm.

curl -s -X POST https://emem.dev/v1/soil \
  -H 'content-type: application/json' \
  -d '{"cell":"defi.zb4e3.vopU.fEyO"}'

A harvester

Readiness is a shape over time, not a reading. One NDVI value cannot tell you whether a crop is climbing or senescing; two months of them can.

curl -s -X POST https://emem.dev/v1/trajectory \
  -H 'content-type: application/json' \
  -d '{"cell":"defi.zb493.xuqA.zcb5f","band":"indices.ndvi","window":[20325,20690]}'

The window is a pair of Unix epoch-day slots. Each point in series carries its own fact_cid, so the curve is not a picture: every point on it dereferences back to the signed observation it came from, and a second machine can re-check any of them.

Observations are sparse and unevenly spaced, because a satellite writes when it passes and not on a schedule you chose. Do not assume consecutive slots. Recall the band first if you need to know which slots exist:

curl -s -X POST https://emem.dev/v1/recall \
  -H 'content-type: application/json' \
  -d '{"cell":"defi.zb493.xuqA.zcb5f","bands":["indices.ndvi"]}'

An industrial robot, indoors

emem does not see a factory floor, and no camera in this corpus is pointed at one. What it offers a machine that works indoors is the other half: identity and attestation.

curl -s https://emem.dev/v1/device_platforms

registry.platforms is the whitelist of hardware a key may enrol from, with the root of trust and evidence format each platform presents. That is what makes a write from a robot different from a write from a laptop claiming to be one, and it is why a fleet operator can later tell which arm produced which record.

What ships: an enrolled device writes through POST /v1/attest_traced, which takes the attestation together with the emem.os_trace.v1 execution trace that produced it, checks the trace against the device’s substrate profile, binds each fact to an output the trace emitted, and returns an emem:trace: token on admit. POST /v1/trace_verify runs the same checks statelessly on any trace and returns every failed check. GET /v1/devices lists devices whose operators chose to be listed.

What does not ship yet: there is no public enrolment route, so a device is enrolled by its node’s operator. Every platform in the registry is candidate and every trust anchor is provisional, so hardware-attested enrolment refuses everything until a vendor anchor is published; an operator-asserted enrolment is recorded as exactly that.

A satellite

A satellite is not a consumer here. It is an attester: the thing whose passes become the observations everything else reads.

curl -s -X POST https://emem.dev/v1/recall \
  -H 'content-type: application/json' \
  -d '{"cell":"defi.zb493.xuqA.zcb5f","bands":["indices.ndvi"],"deterministic":true}'

deterministic: true is the filter that matters for a machine that has to defend a number later. It keeps only facts recomputable from the cited raw source, so anything a model produced is excluded rather than blended in. What comes back can be recomputed by anyone who fetches the same scene.

Checking an answer you were given

Any machine can verify another’s citation without asking emem to vouch for it:

curl -s -X POST https://emem.dev/v1/verify_receipt \
  -H 'content-type: application/json' \
  -d '{"receipt": <the receipt object you were handed>}'

That call is a convenience. The receipt verifies offline against the key in it (BLAKE3 over the preimage, then ed25519), so a second robot can check the first robot’s claim with arithmetic instead of trust; https://emem.dev/verify does the same in a browser with no call back to us.

To see who else writes here:

curl -s https://emem.dev/v1/agents

That is every namespace that has written a note, discovered from the store rather than approved. key_status: proven_by_signature means a note from that namespace carries a caller signature; trust is always caller_decides.