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 get | what 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 block | never observed: the frame was not a view of the street. The block says why, and whether retrying helps |
counts: null with counts_at_camera | counted, 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.