API for apps
Order teleoperation units for your robots from a program. One API key, JSON in and out, credits held per order and charged per accepted unit. Every delivered unit carries a manifest against a seed Latent issued, every decision carries a reason code and a deadline, and every order keeps a hash-chained record. New to the site? Start with the buyer guide or the provider guide.
On this page
Overview
The v1 API is the same marketplace the site runs, for programs instead of people.
Base URL https://latentenergy.com. Every route lives under /api/v1. A buyer app lists providers, checks its balance, places orders with terms and decides delivered units. A provider app lists its orders, accepts them, draws seeds and delivers units with manifests. Both sides use the same key type; what a key can do depends on which account made it.
One teleoperation or autonomous episode on a named platform that met the acceptance bar.
One live human takeover of a running policy, from stop to resume, with the failure class labelled.
One offline relabel or re-demonstration of a logged failure the buyer supplies.
Prices are per accepted unit, in credits. 1 credit is 1 US cent. Fields that end in _units are integer credits. Timestamps are unix seconds. A unit is accepted by the buyer, or on the buyer's behalf when its deadline passes.
| What runs here | What does not run here yet |
|---|---|
| Issues a seed per episode before recording; requires a manifest per unit; records every file hash and refuses duplicates across the whole market; checks the manifest against the order's terms; auto-accepts at the deadline; needs a reason code to reject; lets an admin reverse an accept inside the clawback window; writes a hash-chained event per transition; takes reviews only from charged orders. Holds the files providers upload into Latent custody, hashes them from the stored bytes and serves the buyer's downloads. Opens custody files to check them: decoding, frame loss, the recording code in the task text. Reads the seed card in the first seconds of custody video, compares the video with the other units in custody for near-duplicates, and runs the heuristic physical checks. | Score units with a model. Anchor the chain heads to an outside timestamp. Sign C2PA exports with a claim certificate. Nothing in this column is claimed on this site until it runs here; checks on the files flag and never move credits. |
Authentication
Bearer API keys. Sessions are refused on these routes.
Create a key in Settings. It starts with vc_ and is shown once. Send it in the Authorization header as a bearer token, or in X-Api-Key. A browser session is refused with 401 api_key_required, so a site cookie never drives these routes by accident.
| Scope | Allows |
|---|---|
read | Every GET: providers, credits, orders |
order | Every POST: create an order, respond, issue seeds, deliver, decide, extend. The older name rent is accepted as order. |
keys | The Cell keys box on the key form: register, list and revoke a cell's signing key under /api/v1/cell-keys. A key registered this way stays pending until it is activated in the provider console. API keys themselves are made only in Settings. |
A key acts as the account that made it. Buyer routes (create, decide, extend) need the key of the buyer; provider routes (respond, seeds, deliver) need the key of the account that owns the provider profile. A key without the scope gets 403 forbidden_scope; a key from the wrong side of an order gets 403 forbidden. Up to 20 active keys per account by default. The key that delivers a unit is recorded on it as delivered_by_key.
Conventions
| Rule | Detail |
|---|---|
| Format | JSON request bodies with Content-Type: application/json; JSON responses |
| Success | { "ok": true, ... } with 200, or 201 when an order was created |
| Failure | { "ok": false, "error": "<code>", ...extra } with the HTTP status for that code |
| Request id | Every response sets X-Latent-Request-Id and repeats it as request_id in the body. Quote it when you write to us. |
| Rate limits | Per IP: 240 reads per minute, 60 writes per minute. Over the limit answers 429 rate_limited. |
| Idempotency | Order creation requires idempotency_key. Keys are scoped to your account; a retry with the same key returns the same order with created: false. |
| Hashes | Every sha256 is 64 lowercase hex characters. terms_sha256, manifest_sha256 and event hashes are computed over canonical JSON: keys sorted, no whitespace, undefined fields dropped. |
| Pagination | None yet. Lists return up to 60 providers (limit up to 200) and the latest 100 orders. |
Providers
The public directory: listed providers with prices and stats.
/api/v1/providerspublicunit (episode, intervention, correction), platform (one of the platform labels exactly as listed, for example Almond Axol, Seeed reBot B601 or Maker Arm Station (MakerMods)), country (ISO two letters), verified=1, sort (score, the default; rating, price, response, newest), q (text in name, bio or location), limit (default 60, max 200). No key needed. Every matching provider, up to 1,000, is ranked before the list is cut to limit.curl -s "https://latentenergy.com/api/v1/providers?unit=intervention&platform=Almond%20Axol"{
"ok": true,
"providers": [
{
"provider_id": "7f1c…",
"slug": "latent-axol-cell",
"name": "Latent Axol cell",
"kind": "cell_owner",
"location": "Providence, RI",
"country": "US",
"platforms": ["Almond Axol"],
"rigs": ["Quest 3 or similar headset"],
"latency_ms": 70,
"verified": true,
"color": "#2F6BFF",
"listings": [
{
"listing_id": "a91e…",
"unit_type": "intervention",
"platform": "Almond Axol",
"price_units": 2500,
"min_qty": 4,
"max_qty_per_week": 200,
"active": true
}
],
"stats": {
"units_completed": 312,
"units_decided": 330,
"acceptance_rate": 0.945,
"response_time_sec": 1840,
"orders_completed": 41,
"rating_avg": 4.8,
"rating_count": 37,
"price_from_units": 2500
},
"related_to_latent": true,
"track_record": {
"window_days": 365,
"since": 1730000000,
"min_units": 20,
"min_buyers": 3,
"score": { "value": 91, "sort_value": 91.18, "units": 212, "n": 61.5, "buyers": 9, "good_units": 196, "bad_units": 15, "neutral_units": 1 },
"rows": [
{
"platform": "Almond Axol",
"platform_id": "almond-axol",
"unit_type": "intervention",
"units_decided": 208,
"withheld": false,
"accepted_by_buyer": 171,
"accepted_auto": 25,
"accepted_by_admin": 0,
"rejected": 10,
"reversed": 2,
"accept_rate": 0.942,
"buyer_accept_rate": 0.822,
"auto_accept_rate": 0.12,
"reversal_rate": 0.01,
"top_reject_families": [{ "family": "intervention_timing", "count": 7 }, { "family": "quality", "count": 3 }],
"rejects_overturned_by_audit": 1,
"accepts_failed_by_audit": 0,
"short_closes": 1,
"short_units": 4,
"disputes_lost": 0,
"buyers": 9,
"repeat_buyers": 6,
"lead_time_orders": 31,
"lead_time_sec": 15300,
"enough": true
}
]
}
}
],
"request_id": "0b4d…"
}| Stat | Meaning |
|---|---|
acceptance_rate | Accepted units divided by accepted plus rejected units, 0 to 1. Voided units count for neither side. Null until a unit was decided. |
response_time_sec | Median seconds between a request and the provider's accept or decline. |
units_completed | Accepted units across all orders. |
rating_avg | Weighted mean of published reviews from the last 12 months, 1 to 5. Null until 3 reviews from 2 or more buyers exist. Each review is weighted by its order's charged credits, no buyer holds more than 20 percent of that weight (an equal share when fewer than five buyers have reviewed), and each weight is then multiplied by the author's rater weight (below). |
latency_ms | The input-to-actuation latency the provider lists. A manifest's p99 above twice this value is a warning on the unit. |
price_from_units | Cheapest active listing, in credits. |
related_to_latent | True for a provider owned by Latent or by a person who runs Latent, whose account is linked to one of Latent's accounts (a shared card, or a link an admin made), or whose account is on one of Latent's mail domains. Orders between it and Latent's accounts, accounts linked to them, and accounts on Latent's mail domains count in none of these numbers. |
Every number here leaves out practice orders and orders whose buyer is the provider's own account or linked to it; a buyer on the provider's own company mail domain is linked when the order is placed. The stats above cover all time; the track record, the score and the rating cover the last 12 months, and are recomputed at most once a minute.
Verified track record
track_record.rows has one row per platform label and unit type with a unit decided or a short close in the last 12 months, largest sample first. Rates are null below min_units (20) decided units, where the site says "too few to judge": at 20 units a 90 percent accept rate has a 95 percent interval of about 70 to 97 percent, which already separates a provider who delivers from one who does not; at 10 it is about 60 to 98 percent. A row whose units were decided by fewer than min_buyers (3) buyers has withheld: true: it publishes only units_decided, the short closes and the lead time, and every other count and rate is null, so no row is one buyer's decision history.
| Field | Meaning |
|---|---|
units_decided | The sample size n: units accepted, rejected or reversed, counted by decision time. |
accepted_by_buyer, accepted_auto, accepted_by_admin | Accepted units by who decided: the buyer, the deadline (auto-accept), or a Latent admin in a dispute. accept_rate, buyer_accept_rate and auto_accept_rate divide them by n. |
reversed, reversal_rate | Accepted units an admin reversed inside the clawback window. |
top_reject_families | Rejects grouped by reason code: spec (not_as_specified, wrong_platform), completeness (missing_streams, truncated), originality (duplicate), quality (quality_below_bar), intervention_timing (takeover_not_needed, takeover_late), other. |
rejects_overturned_by_audit, accepts_failed_by_audit | Units where a qualified Latent audit disagreed with the decision. They change the score, never the credits. |
short_closes, short_units | Orders the provider accepted that closed with units not accepted, rejected or reversed, and those units: the provider's cancel, the buyer's cancel after 7 days without a delivery, or an admin's dispute close unless the admin put the shortfall on the buyer. A provider cancel after the buyer rejected units on the order waits for an audit of those rejects. A cancel before accepting is a decline and does not count. |
disputes_lost | Disputed orders where an admin rejected or reversed a unit. Those units already count as rejected or reversed. |
buyers, repeat_buyers | Buyers with an accepted unit, and those with accepted units on orders placed in 2 or more different weeks. |
lead_time_sec, lead_time_orders | Median seconds from the provider accepting an order to its first delivered unit, over lead_time_orders orders. Null below 3. |
Verified score
track_record.score is one number in the spirit of Upwork's Job Success Score, computed from verified outcomes instead of opinions: each buyer's credit-weighted good outcomes minus bad over all outcomes, with no buyer counting for more than 7 units, shrunk toward the score of an 80 percent accept rate so a short record cannot sit at either end. The directory sorts on it by default, with stars as the tie-break. A provider with no record sits at 80; a record below an 80 percent accept rate ranks under a new provider, and one above it ranks over. A record from fewer than 3 buyers is shrunk harder and its value is not shown: one friendly account cannot carry a provider past a record from three buyers, and one hostile account cannot sink one. good_units, bad_units and neutral_units are null below 3 buyers.
for each buyer b and each of b's outcome units in the last 12 months,
w = the unit price of its order, in credits:
good accepted by the buyer or by Latent, not reversed 1 good
accepted at the deadline (no buyer decision) 1/2 good, 1/2 neutral,
half a unit
bad rejected by Latent in a dispute, reversed, or
accepted but failed by a qualified audit 1 bad
rejected by the buyer r_b bad, 1 - r_b neutral
a short close: min(units left, max(1, delivered)) 1 bad each
neutral rejected, but a qualified audit accepted it (not on a related provider's order)
r_b the buyer's rater weight (below; 1 on a related provider's order)
short close: an order the provider accepted that closed with units not accepted,
rejected or reversed: the provider's cancel, the buyer's cancel after 7 days without
a delivery, or an admin's dispute close that did not put the shortfall on the buyer.
A provider cancel after the buyer rejected units on the order counts once a qualified
audit upholds one of those rejects, and never if one is overturned.
units_b = buyer b's units (deadline accepts at half)
rate_b = (sum w good - sum w bad) / (sum w good + sum w bad + sum w neutral)
e_b = min(units_b, 7) no buyer counts for more than 7 units
n = sum e_b
rate = sum(e_b * rate_b) / n -1 to 1
prior = 10 + 10 * max(0, 3 - buyers) units at 0.6 (80% accepted)
score = 50 * (1 + (n * rate + prior * 0.6) / (n + prior)) 0 to 100
value = round(score) when n >= 20 and buyers >= 3, else null
sort_value = score with 2 decimals, always set; the directory sorts on itRater weight
A buyer whose rejects Latent's audits overturn counts for less in the rating. A reject is audited when a qualified auditor gave a verdict on the unit, and overturned when one accepted it; audits of a related provider's units never count against a buyer. With no audited reject the weight is 1; one overturned reject makes it 2/3, three of three make it 0.4.
credit weight = max(1, order's charged credits - reversed credits)
capped weight = credit weight, scaled down so no buyer holds more than
max(20 percent, 1 / buyers) of the total
rater weight = (rejects upheld + 2) / (rejects audited + 2)
rating = sum(capped weight * rater weight * overall) / sum(capped weight * rater weight)Order by provider_slug plus unit_type to take the provider's cheapest active listing for that unit, or by listing_id to pin one listing. Listings carry min_qty; an order below it is refused.
Credits
Prepaid balance in credits. Buying happens on the site; the API reads it.
/api/v1/creditsscope: readavailable_units can fund new orders, reserved_units is held on open orders, spent_units has been charged for accepted units. A reversal moves credits from spent back to available.curl -s https://latentenergy.com/api/v1/credits \
-H "Authorization: Bearer $LATENT_API_KEY"{
"ok": true,
"balance": { "available_units": 48000, "reserved_units": 10000, "spent_units": 42000 },
"request_id": "6c2a…"
}Buy credits by card on Billing, or ask for a grant against an invoice. Credits are prepaid and are not withdrawable; see the terms.
Orders and terms
An order is a funded request for a quantity of one unit type from one provider, with its acceptance terms frozen at creation.
/api/v1/ordersscope: orderprovider_slug or listing_id (one is required), unit_type, quantity (1 to 10,000), note (a private note to the provider, up to 2,000 characters, never printed), idempotency_key (required, up to 120 characters), optional task_id (one of your own tasks) and terms, with terms.instruction required unless the order is for a task. Holds quantity times unit price from the buyer's available credits. Answers 201 when created, 200 when the idempotency key matched an earlier order, 400 missing_instruction when there is neither an instruction nor a task, 402 when the balance is short.Terms
Terms are the contract the manifest checks and the reject codes refer to. They are normalized by the server, stored on the order, hashed into terms_sha256 and never change afterwards. Every field but instruction is optional, and a task fills the gaps, the instruction included.
| Field | Rule |
|---|---|
rubric | Up to 20 steps of { label, max }. Labels up to 400 characters; the server assigns ids s1, s2, … in order. max is a share in (0, 1]; steps without one share the remainder evenly. From a task with no rubric given, the task's acceptance bar becomes step s1 with max 1. |
formats | Up to 80 characters. Default: the task's formats. |
rights | Non-exclusive, Undecided or empty. Capitals do not matter. Default: the task's rights. Non-exclusive is stored in this spelling; empty or Undecided stores no rights. All three give the buyer the non-exclusive licence the terms define. Any other value, exclusivity or a buyout included, answers 400 rights_not_offered with offered. The whole value is read, and one longer than 400 characters is refused. |
required_streams | Up to 12 path prefixes of up to 80 characters. Every delivered unit must have at least one file whose path starts with each prefix (check streams). |
min_active_s | 1 to 86,400. A unit shorter than this is refused (check duration). Unset: no minimum. |
objects | Up to 30 names of up to 60 characters. Each seed carries them in a server-shuffled order. |
instruction | What to record, up to 600 characters. Printed on every recording card and saved as each take's task label. Required unless the order is for a task, whose description is then the default. The note is never used in its place. |
decide_within_sec | The buyer's decision window per unit, 86,400 to 2,592,000 (1 to 30 days). Default 604,800 (7 days). Stored on the order, not inside terms. |
The order also carries clawback_sec (1,209,600, 14 days, fixed), the window in which an admin can reverse an accepted unit and a provider can dispute a completed order, and review_closes_at, set to 14 days after the order completes, or is cancelled with a charged unit. Time the order then spends in a dispute is added back to it.
curl -s -X POST https://latentenergy.com/api/v1/orders \
-H "Authorization: Bearer $LATENT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"provider_slug": "latent-axol-cell",
"unit_type": "intervention",
"quantity": 4,
"note": "Cup handover failures from policy v12. One log per unit.",
"idempotency_key": "policy-v12-batch-001",
"terms": {
"rubric": [
{ "label": "The takeover starts within 2 s of the policy stalling", "max": 0.5 },
{ "label": "The cup ends upright on the tray", "max": 0.5 }
],
"formats": "LeRobot v3 + metadata CSV",
"rights": "Non-exclusive",
"required_streams": ["overview", "wrist"],
"min_active_s": 20,
"objects": ["paper cup", "tray", "lid"],
"instruction": "Hand the cup to the customer side of the counter.",
"decide_within_sec": 604800
}
}'{
"ok": true,
"created": true,
"order": {
"order_id": "c3d9…",
"status": "requested",
"unit_type": "intervention",
"platform": "Almond Axol",
"unit_price_units": 2500,
"quantity": 4,
"hold_units": 10000,
"charged_units": 0,
"released_units": 0,
"provider_units": 0,
"fee_units": 0,
"terms": {
"rubric": [
{ "id": "s1", "label": "The takeover starts within 2 s of the policy stalling", "max": 0.5 },
{ "id": "s2", "label": "The cup ends upright on the tray", "max": 0.5 }
],
"formats": "LeRobot v3 + metadata CSV",
"rights": "Non-exclusive",
"required_streams": ["overview", "wrist"],
"min_active_s": 20,
"objects": ["paper cup", "tray", "lid"],
"instruction": "Hand the cup to the customer side of the counter."
},
"terms_sha256": "5e2b…",
"decide_within_sec": 604800,
"clawback_sec": 1209600,
"review_closes_at": null,
"requested_at": 1761052800,
"responded_at": null,
"completed_at": null
},
"request_id": "9e1f…"
}HTTP/1.1 402 Payment Required
{ "ok": false, "error": "insufficient_credits", "missing_units": 6000, "request_id": "d2a7…" }/api/v1/ordersscope: read?role=provider lists the orders received by the account's provider profile. Newest first, up to 100.curl -s "https://latentenergy.com/api/v1/orders?role=provider" \
-H "Authorization: Bearer $LATENT_API_KEY"/api/v1/orders/:idscope: readrole: what the key's account is to this order (buyer or provider). Anyone else gets 403.curl -s https://latentenergy.com/api/v1/orders/$ORDER_ID \
-H "Authorization: Bearer $LATENT_API_KEY"{
"ok": true,
"role": "buyer",
"provider": { "provider_id": "7f1c…", "slug": "latent-axol-cell", "name": "Latent Axol cell" },
"order": { "order_id": "c3d9…", "status": "in_progress", "quantity": 4, "charged_units": 2500, "…": "…" },
"units": [
{
"unit_id": "f08a…", "seq": 1, "status": "accepted",
"evidence_url": "https://files.example.com/c3d9/ep-1/",
"manifest_sha256": "a7c1…", "seed_id": "0f6e…", "check_status": "warn",
"decide_by": 1761667200, "extended_once": false,
"reject_code": null, "reject_reason": null, "decided_by": "buyer",
"delivered_at": 1761062400, "decided_at": 1761066000, "voided_at": null, "access_revoked_at": null
},
{
"unit_id": "2b7d…", "seq": 2, "status": "delivered",
"evidence_url": "https://files.example.com/c3d9/ep-2/",
"manifest_sha256": "c90e…", "seed_id": "41aa…", "check_status": "warn",
"decide_by": 1761670800, "extended_once": false,
"reject_code": null, "reject_reason": null, "decided_by": null,
"delivered_at": 1761066000, "decided_at": null, "voided_at": null, "access_revoked_at": null
}
],
"request_id": "41b0…"
}| Unit field | Meaning |
|---|---|
status | delivered, accepted, rejected or voided |
check_status | pass when every manifest check passed, warn when at least one warned. A failing manifest never becomes a unit. |
manifest_sha256 | sha256 of the canonical manifest JSON as delivered |
seed_id | The seed the manifest named; the seed is now used |
evidence_url | The manifest's storage_uri, else the evidence_url sent with the delivery, else null |
decide_by | delivered_at plus the order's decide_within_sec (72 h more when the robot was connected less than a day before); the unit is auto-accepted after it unless auto-accept waits (see the cron tick). null after its robot is revoked as compromised, so it is never auto-accepted |
extended_once | true after the buyer's one extension |
decided_by | buyer, admin, auto (deadline passed) or provider_cancel (voided by a provider cancel) |
reject_code, reject_reason | The closed-list code and the free text of a rejection; the reason of a reversal |
voided_at, access_revoked_at | Set on reversal and provider cancel; access_revoked_at is also set on reject. For a unit in Latent custody it ends the buyer's file links; for files the provider hosts it records the decision only. |
# The site route accepts the same bearer key and returns the full view:
# seeds, manifests with file hashes, check rows and the event chain.
curl -s https://latentenergy.com/api/market/orders/$ORDER_ID \
-H "Authorization: Bearer $LATENT_API_KEY"The site route returns view with order, units, seeds, manifests (one per unit, with its files and their hashes), checks (every check row of every unit), events (the chain, oldest first), messages, reviews and can, the actions open to the caller.
/api/v1/orders/:id/respondscope: orderdecision accept or decline. Only while the order is requested. Decline releases the whole hold to the buyer.curl -s -X POST https://latentenergy.com/api/v1/orders/$ORDER_ID/respond \
-H "Authorization: Bearer $PROVIDER_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "decision": "accept" }'Seeds
Latent issues the seed before the episode is recorded. It proves the recording is not older than the order and binds it to one order.
/api/v1/orders/:id/seedsscope: ordercount, 1 to 200, default 1. Only while the order is accepted or in_progress. Issues at most quantity minus delivered units minus open seeds (unused and unexpired); when that room is zero the call answers 409 no_seed_room with the three numbers. Answers 201 with seeds and a note on how to use them. Site path: /api/market/orders/:id/seeds.curl -s -X POST https://latentenergy.com/api/v1/orders/$ORDER_ID/seeds \
-H "Authorization: Bearer $PROVIDER_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "count": 4 }'{
"ok": true,
"seeds": [
{
"seed_id": "0f6e1c2a-…",
"order_id": "c3d9…",
"provider_id": "7f1c…",
"nonce": "3b9d2f6e8a1c4d7e9f0a1b2c3d4e5f60",
"episode_id": "c3d9a1b2-1",
"instruction": "Hand the cup to the customer side of the counter.",
"objects": ["tray", "paper cup", "lid"],
"layout": {},
"issued_at": 1761052900,
"expires_at": 1761312100,
"used_by_unit": null
},
{ "seed_id": "41aa…", "nonce": "…", "episode_id": "c3d9a1b2-2", "…": "…" }
],
"note": "Each seed is bound to this order and expires 72 hours after issue. Put seed_id and nonce in the unit's manifest; …",
"request_id": "b2c4…"
}/api/v1/orders/:id/seedsscope: readopen (unused and unexpired), delivered (units so far), room (how many more seeds the order can take) and now (server time, to compare with expires_at).curl -s https://latentenergy.com/api/v1/orders/$ORDER_ID/seeds \
-H "Authorization: Bearer $PROVIDER_API_KEY"
# { "ok": true, "seeds": [ … ], "open": 3, "delivered": 1, "room": 0, "now": 1761062400, "request_id": "…" }| Seed field | Meaning |
|---|---|
seed_id | Goes into the manifest as seed_id |
nonce | 32 lowercase hex characters (128 random bits). Goes into the manifest as nonce. Show its card in frame at the start of the episode: for files in Latent custody the checker reads it in the first seconds of the video; for files the provider hosts the server only matches the manifest's nonce to the seed. |
episode_id | The first 8 characters of the order id, a dash, and the running number of the episode on this order |
instruction | The order's terms.instruction. The order note is never printed. |
objects | The terms' object list in a random order drawn by the server. Lay the scene out in that order. |
layout | Always {} in phase 1 |
issued_at, expires_at | expires_at is issued_at plus 259,200 (72 hours). The episode's start_timestamp must fall between them. |
used_by_unit | null until a delivery consumes the seed; then the unit id. A seed is single use. |
Draw seeds in batches you will record within 72 hours. An expired seed no longer counts as open, so you can draw a replacement. Every batch writes a seeds_issued event on the order.
Manifest
A delivery is a manifest: who recorded what, on which robot, when, how long, with which files. Deliver requires it.
/api/v1/orders/:id/deliverscope: ordermanifest (required, below), note (up to 2,000 characters), evidence_url (http or https, used only when the manifest has no storage_uri). Allowed while the order is accepted or in_progress; after the last unit the order reads delivered. The manifest is parsed and checked; one failing check answers 400 manifest_invalid with the check list and nothing is written. Otherwise the unit is created with its decide_by, the manifest, its files and its check rows are stored, the seed is marked used, and a unit_delivered event is written. Response: { ok, order, unit, checks }.{
"schema_version": "latent-manifest/1",
"seed_id": "0f6e1c2a-…",
"nonce": "3b9d2f6e8a1c4d7e9f0a1b2c3d4e5f60",
"format": "lerobot_v3",
"storage_uri": "https://files.example.com/c3d9/ep-1/",
"episode": {
"episode_id": "c3d9a1b2-1",
"episode_index": 0,
"operator_id": "op-17",
"robot_id": "axol-03",
"station_id": "pvd-cell-1",
"start_timestamp": 1761061200.0,
"duration_s": 42.5,
"camera_count": 2,
"is_eval_episode": true,
"checkpoint_path": "policy-v12/ckpt-8800",
"policy_version": "v12",
"success": true
},
"intervention": { "takeover_start_s": 12.0, "takeover_end_s": 36.0, "failure_class": "grasp_miss" },
"latency": { "p50_ms": 61, "p99_ms": 140 },
"files": [
{ "path": "overview.mp4", "sha256": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08", "bytes": 48213990 },
{ "path": "wrist.mp4", "sha256": "60303ae22b998861bce3b28f33eec1be758a213c86c93c076dbe9f558c11c752", "bytes": 39120044 },
{ "path": "episode_000000.parquet", "sha256": "fd61a03af4f77d870fc21e05e7e80678095c92d808cfb3b5c279ee04c74aca13", "bytes": 1204410 }
]
}| Field | Type | Rule |
|---|---|---|
schema_version | string | Exactly latent-manifest/1 |
seed_id | string | A seed issued on this order to this provider, unused and not expired |
nonce | string | The seed's 32 hex characters |
format | string | One of lerobot_v3, rlds, mcap, other |
storage_uri | string, optional | latent://<upload id> for files uploaded into Latent custody, or an http or https URL, up to 1,000 characters, where the buyer downloads files the provider hosts. Becomes the unit's evidence_url. Missing: the storage_missing and custody checks warn, and the delivery still goes through. |
episode.episode_id | string | Up to 120 characters, required. Use the seed's episode_id. |
episode.episode_index | integer | 0 or more |
episode.operator_id | string | Up to 120 characters, required. Your id for the human operator. |
episode.robot_id | string | Up to 120 characters, required |
episode.station_id | string | Up to 120 characters, required |
episode.start_timestamp | number | Unix seconds (fractions allowed), between the seed's issued_at and expires_at |
episode.duration_s | number | More than 0, at most 86,400; at least terms.min_active_s when set |
episode.camera_count | integer | 1 to 16 |
episode.is_eval_episode | boolean | true when a policy was running. Required true for an intervention. |
episode.checkpoint_path | string | Up to 300 characters. Must be empty unless is_eval_episode is true. |
episode.policy_version | string | Up to 120 characters |
episode.success | boolean or null | Your claim about the episode's outcome. Stored as success_claim; it moves no credits. |
intervention | object | Required on intervention orders: takeover_start_s, takeover_end_s (seconds from the episode start) and failure_class, one of grasp_miss, misalignment, slip, perception_error, out_of_spec, other |
correction | object | Required on correction orders: failed_log_ref (up to 300 characters) and policy_version (up to 120). This policy_version is the one stored on the unit. |
latency | object, optional | p50_ms and p99_ms, non-negative integers with p99 at or above p50. Input-to-actuation of the teleop link during the episode. |
files | array | At least one. Each { path, sha256, bytes }: path up to 300 characters and unique in the manifest; sha256 64 hex, unique in the manifest and new to the whole market; bytes a positive integer. The first 500 entries are read. |
The nine Physical Intelligence intake columns (episode_id, episode_index, operator_id, robot_id, station_id, start_timestamp, duration_s, camera_count, is_eval_episode plus checkpoint_path) are checked for presence and type, so a buyer that ingests that format gets the columns filled. Fields are stored as given, and these checks never open the files. Files in Latent custody are opened by the checker after delivery (Checks on the files, below).
# unit-1.json holds { "manifest": { ...the manifest above... }, "note": "Takeover at 12 s, cup slipped on the lid." }
curl -s -X POST https://latentenergy.com/api/v1/orders/$ORDER_ID/deliver \
-H "Authorization: Bearer $PROVIDER_API_KEY" \
-H "Content-Type: application/json" \
--data-binary @unit-1.json{
"ok": true,
"order": { "order_id": "c3d9…", "status": "in_progress", "…": "…" },
"unit": {
"unit_id": "f08a…", "seq": 1, "status": "delivered",
"evidence_url": "https://files.example.com/c3d9/ep-1/",
"manifest_sha256": "a7c1…", "seed_id": "0f6e1c2a-…", "check_status": "warn",
"decide_by": 1761667200, "extended_once": false, "delivered_at": 1761062400
},
"checks": [
{ "check_code": "seed", "level": "pass", "message": "seed matches this order and is unused", "value": null, "threshold": null, "checker_version": "manifest/1.0" },
{ "check_code": "seed_time", "level": "pass", "message": "episode starts inside the seed window", "value": 8300, "threshold": 259200, "checker_version": "manifest/1.0" },
{ "check_code": "pi_columns", "level": "pass", "message": "the nine intake columns are present", "value": null, "threshold": null, "checker_version": "manifest/1.0" },
{ "check_code": "duplicate", "level": "pass", "message": "3 file hashes are new to the market", "value": 3, "threshold": null, "checker_version": "manifest/1.0" },
{ "check_code": "intervention_span", "level": "pass", "message": "takeover of 24.0 s", "value": 24, "threshold": 42.5, "checker_version": "manifest/1.0" },
{ "check_code": "duration", "level": "pass", "message": "meets the order's minimum duration", "value": 42.5, "threshold": 20, "checker_version": "manifest/1.0" },
{ "check_code": "streams", "level": "pass", "message": "every required stream is present", "value": null, "threshold": null, "checker_version": "manifest/1.0" },
{ "check_code": "latency", "level": "pass", "message": "latency within the listed range", "value": 140, "threshold": 140, "checker_version": "manifest/1.0" },
{ "check_code": "custody", "level": "warn", "message": "files are hosted by the provider, not by Latent; hashes are recorded but not yet verified against bytes", "value": null, "threshold": null, "checker_version": "manifest/1.0" }
],
"request_id": "7c33…"
}HTTP/1.1 400 Bad Request
{
"ok": false,
"error": "manifest_invalid",
"checks": [
{ "check_code": "seed", "level": "fail", "message": "seed already used by a delivered unit", "value": null, "threshold": null, "checker_version": "manifest/1.0" },
{ "check_code": "duplicate", "level": "fail", "message": "overview.mp4 was already delivered as part of another unit", "value": null, "threshold": null, "checker_version": "manifest/1.0" }
],
"request_id": "8d10…"
}Checks
Every manifest is run through this list. A single fail refuses the delivery; warnings are stored on the unit and shown to the buyer.
| check_code | Rule | Level |
|---|---|---|
schema | schema_version is latent-manifest/1 | fail |
format | format is one of the four values | fail |
storage | storage_uri, when given, is an http or https URL or latent://<upload id> | fail |
seed | seed_id present and nonce 32 hex; the seed exists, belongs to this order and provider, is unused, is not expired, and its nonce matches | fail |
seed_time | start_timestamp is at or after the seed's issued_at and at or before its expires_at | fail |
pi_columns | The nine intake columns are present with the right types; checkpoint_path is empty unless is_eval_episode | fail |
files | At least one file; every path present and unique; every sha256 64 hex and unique; every bytes a positive integer | fail |
duplicate | No sha256 is already stored under another unit, from any provider on any order | fail |
intervention_span | Intervention orders: 0 <= takeover_start_s < takeover_end_s <= duration_s, is_eval_episode is true, failure_class is in the list | fail |
correction_ref | Correction orders: correction.failed_log_ref and correction.policy_version are present | fail |
duration | duration_s is at least terms.min_active_s (only when the order sets it) | fail |
streams | Every entry of terms.required_streams is the prefix of at least one file path (only when the order sets them) | fail |
latency_hard | Intervention orders with latency given: p50_ms is below 300 | fail |
latency | Latency given and malformed: fail. p99_ms above twice the provider's listed latency_ms: warn. Otherwise pass. | warn fail |
storage_missing | No storage_uri: the buyer has no way to download the files. The delivery is not refused, but the unit is likely to be rejected. | warn |
custody | storage_uri is a latent:// upload whose files match the manifest and are verified: pass. A provider-hosted URL: warn. A latent:// upload that does not match, or a hosted URL on an order that requires custody: fail. | warn fail |
cell_signature | A signature by an active cell key over the manifest's hash: pass. Unsigned: warn, or fail when the order requires a signature or the delivery comes from a robot's credential. A bad, revoked or pending key: fail. | warn fail |
cell_identity | Deliveries by a robot: the manifest's robot_id is the robot's (fail otherwise) and its station_id should be (warn). | warn fail |
Each stored check row carries check_code, level, value and threshold (the measured number and the limit, when there is one), a message and checker_version (manifest/1.0). Checks only read the manifest, the seed, the terms and the hash index; they do not fetch files. These delivery checks refuse a delivery that breaks the rules; the checks on the files and the audits below run after delivery, where enabled. Nothing here moves credits by itself.
Uploads and files
Latent custody: the provider uploads the files, Latent computes every hash from the stored bytes and serves the buyer's downloads.
Latent storage is on in this deployment.
GET /api/v1/storage says whether custody is on, the size limits and whether uploads go straight to the storage host. Every path must pass the manifest path rules: letters, digits and . _ - = /, at most 300 characters and 16 levels, no empty, . or .. segment, no name starting with a dash, no two paths that differ only in case, and no file named like a folder. A refused path answers 400 bad_path with its rule.
# 1. Hash the files, then open an upload for one seed
curl -s -X POST https://latentenergy.com/api/v1/uploads \
-H "Authorization: Bearer $PROVIDER_API_KEY" -H "Content-Type: application/json" \
-d '{"order_id":"$ORDER_ID","seed_id":"$SEED_ID","episode_index":0,
"files":[{"path":"videos/observation.images.overhead_left/chunk-000/file-000.mp4","sha256":"<64 hex>","bytes":91234567}]}'
# -> { "ok": true, "upload": { "upload_id": "upl_…", "storage_uri": "latent://upl_…" }, "slots": [ { "path": …, "method": "PUT", "url": …, "headers": { … } } ] }
# 2. PUT each file to its slot with the slot's headers, verbatim
curl -s -X PUT "$SLOT_URL" -H "content-type: application/octet-stream" --data-binary @file-000.mp4
# 3. Complete; "verified" at once when Latent hashed the bytes, "verifying" while a custody worker re-hashes them
curl -s -X POST https://latentenergy.com/api/v1/uploads/$UPLOAD_ID/complete -H "Authorization: Bearer $PROVIDER_API_KEY" -H "Content-Type: application/json" -d '{}'
# 4. Deliver with storage_uri set to the upload's latent:// valueSlots are short-lived. Before retrying a failed or expired PUT, fetch GET /api/v1/uploads/[id] for fresh slots: it lists slots only for files still pending or failed, so an upload resumes where it stopped. Opening the same file set again for the same seed answers 200 with the same upload. A file whose bytes are already in custody under another unit is refused (409 upload_duplicate); too many such refusals in an hour answer 429 duplicate_probe_limit.
The buyer, the provider and Latent download a custody unit with GET /api/v1/orders/[id]/units/[seq]/files: links that expire, with each file's sha256 to check after downloading. The buyer's access ends when the buyer rejects the unit. Files are kept 30 days after a reject and 365 days after an accept, then purged.
Signatures and cell keys
A delivery can be signed by a key registered for the cell that recorded it. Each kind of key proves something different.
| Key | What a valid signature proves | What it does not prove |
|---|---|---|
| Ed25519 key file on the cell (a robot's key, or latent key register) | Whoever holds the key file approved this manifest; no person has to be present | That the frames are real: root on the recorder can sign anything |
| Security key with an attestation verified to a pinned vendor root | A hardware key of an allow-listed model approved it, touched by a person | That the frames are real |
| Security key without a verified attestation | The registered credential signed it | Which device it is, or that a person touched it |
Register an Ed25519 key with a challenge: POST /api/v1/cell-keys/challenge, sign latent-cell-key-register/1\n<provider_id>\n<challenge>\n, then POST /api/v1/cell-keys. A key registered through an API key stays pending until it is activated in the provider console, signed in, so a leaked API key alone cannot add a trusted key. Security keys are added in the console. Each key may carry a cell profile: which video stream is the overview camera, which the wrist, which two form a stereo pair. Physical checks need it.
# The manifest's hash as the server computes it (no challenge is spent)
curl -s -X POST https://latentenergy.com/api/v1/orders/$ORDER_ID/manifest/preview \
-H "Authorization: Bearer $PROVIDER_API_KEY" -H "Content-Type: application/json" -d @unit.json
# -> { "manifest_sha256": "d8a4…", "canonical": "{…}", "dropped_keys": [], "checks": [ … ], "status": "pass" }
# Sign the UTF-8 bytes "latent-manifest-sig/1\n<manifest_sha256>\n" with the cell key, then deliver with
# "signature": { "kind": "ed25519", "key_id": "ck_…", "manifest_sha256": "d8a4…", "signature": "<base64, 64 bytes>" }The robot's agent may also put the same signature inside the manifest as cell_signature; when both are sent they must be identical. A delivery made with a robot's credential must be signed by that robot's active key. An order may require a signature, a hardware key or physical checks; a delivery that does not meet it is refused.
Connect a robot
An Axol robot connects to a provider account with one line and one click, then delivers its takes by itself.
In the provider console, Robots, press Connect a robot and paste the line it shows on the robot over SSH. The robot asks for approval in the console, with its hostname, cameras and the fingerprint of its key; one click approves it. A robot without the line shows an 8-letter code to enter at /pair. Pairing follows the OAuth device authorization grant: the robot proves possession of its key at every step, and its credential is created only after the provider approved and the robot signed a fresh poll.
A robot's credential is a vc_ API key bound to its cell. Every request made with it carries the header Latent-Cell-Proof, signed by the robot's key over the method, the path, the time, a fresh nonce, the body hash and the credential's id, so the bearer string alone is worthless. It works only on the provider side of a few routes (orders, seeds, uploads, preview, deliver and /api/v1/cells/*); everywhere else it answers 403 cell_key_forbidden. Every v1 answer carries Latent-Server-Time.
Limits per robot: 2 seeds per call, 8 open seeds per order, 240 deliveries an hour, only the orders whose robot list names it (an empty list means every robot of the provider). Units from a robot connected less than a day before are given 72 more hours for the buyer. Revoking a robot as compromised stops auto-accept on every undecided unit it delivered. See /robot for the install, updates and rollback.
Checks on the files
Where Latent's checker runs, units in custody get file and physical checks after delivery. They flag; they never accept, reject, charge or refund.
Results appear on the order (results in GET /api/v1/orders/[id]) with a status per unit: pending while checks run, unchecked when they could not run (for example no checker online, or a codec Latent's checker cannot decode), pass, warn, fail or error. Rows whose threshold is borrowed or proposed and not yet calibrated on Latent's reference recordings say so (calibrated: false).
Only facts about the bytes pause auto-accept: a broken dataset, frame loss above PI's published 20 percent, a video that cannot be decoded or uses a codec the order excludes, a task text naming another recording code, or another seed's card decoded in full. A paused unit can still be accepted or rejected; Latent reviews it, and clearing the pause gives the buyer a fresh window. Heuristic checks (stereo depth, joint log against video, IMU, torque) never pause anything.
The seed card is a QR code of the seed's nonce, printed from the order page. Read in the first seconds of the video it proves the recording was made after the seed was issued, unless the video was edited. The code in the task text ([latent:<nonce>]) is only a label the provider writes. Some checks are shown only to the buyer, as a fixed sentence without values: this keeps thresholds out of providers' routine view, but it is not a secret from a provider who also buys. When the buyer and provider accounts are linked, those rows are not shown at all.
Latent staff also audit a sample of custody units against the order's terms. An audit verdict is a record; a qualified auditor's reject may pause the unit's auto-accept for a Latent review, and it never moves credits by itself.
Receipts, anchors, C2PA
Evidence both sides can keep and check without trusting Latent's database.
Receipt
GET /api/v1/orders/[id]/receipt returns the order's event record, recomputed, each anchor holding it with the Merkle proof of the order's leaf, the RFC 3161 timestamp tokens, and the OpenSSL steps to check them.
Anchors
Anchoring is not turned on in this deployment yet. GET /api/market/anchors lists the roots, /api/market/anchors/[id]/leaves the leaf hashes (no order ids), and /api/market/anchors/[id]/tokens/[n] each timestamp token. Latent cannot backdate an anchored root; a rewrite after anchoring is visible to anyone who kept an earlier receipt. See /transparency.
C2PA export
GET /api/v1/orders/[id]/units/[seq]/c2pa returns a C2PA-compatible manifest definition for a custody unit. It is unsigned until Latent holds a claim-signing certificate, asserts only what Latent checked, and labels the provider's claims as provider-declared.
Decisions and deadlines
The buyer accepts or rejects each unit before its decide_by. A reject names a reason code from a closed list.
/api/v1/orders/:id/units/:seq/decidescope: orderdecision accept or reject; on reject reject_code (required, below) and rubric_step (optional, a step id such as s1, up to 40 characters) and reason (optional, up to 1,000 characters). The step is stored inside the unit's reject reason as rubric step s1: reason. Accept charges one unit price from the hold, credits the provider's earnings minus the 15 percent fee and records decided_by: "buyer". Reject charges nothing and sets access_revoked_at. When every unit of the quantity is decided the order completes, the rest of the hold is released and the review window opens. Allowed while the order is in_progress or delivered, and while it is disputed when the provider is a related provider (see Reversal, cancel, dispute); otherwise 409 bad_state.| reject_code | Use it when |
|---|---|
not_as_specified | The unit does not do what the terms or the instruction asked |
wrong_platform | Recorded on a robot other than the listed platform |
missing_streams | A camera or sensor stream the order needs is absent or unusable, though the manifest named a file for it |
truncated | The recording stops before the task ends, or the files are shorter than duration_s says |
duplicate | The content repeats an earlier unit even though the bytes differ |
quality_below_bar | A rubric step is not met; name it in reason |
takeover_not_needed | Interventions: the policy had not failed when the operator took over |
takeover_late | Interventions: the operator took over after the failure had already played out |
other | Anything else; explain in reason |
The list in code is REJECT_CODES: not_as_specified, wrong_platform, missing_streams, truncated, duplicate, quality_below_bar, takeover_not_needed, takeover_late, other. A reject without a valid code answers 400 reject_code_required with the allowed list.
curl -s -X POST https://latentenergy.com/api/v1/orders/$ORDER_ID/units/1/decide \
-H "Authorization: Bearer $LATENT_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "decision": "accept" }'curl -s -X POST https://latentenergy.com/api/v1/orders/$ORDER_ID/units/2/decide \
-H "Authorization: Bearer $LATENT_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "decision": "reject", "reject_code": "takeover_late", "reason": "Policy stalled at 9.4 s, takeover starts at 12.0 s; rubric step s1." }'HTTP/1.1 400 Bad Request
{ "ok": false, "error": "reject_code_required", "allowed": ["not_as_specified", "wrong_platform", "missing_streams", "truncated", "duplicate", "quality_below_bar", "takeover_not_needed", "takeover_late", "other"], "request_id": "1a9c…" }/api/v1/orders/:id/units/:seq/extendscope: orderdecide_by forward by the order's decide_within_sec and sets extended_once. A second call answers 409 already_extended; a decided unit answers 409 unit_already_decided; a unit with no deadline (its robot was revoked as compromised, so only the buyer accepts it) answers 409 no_deadline. Writes a decision_extended event. Site path: /api/market/orders/:id/units/:seq/extend.curl -s -X POST https://latentenergy.com/api/v1/orders/$ORDER_ID/units/2/extend \
-H "Authorization: Bearer $LATENT_API_KEY"{
"ok": true,
"order": { "order_id": "c3d9…", "status": "in_progress", "…": "…" },
"unit": { "unit_id": "2b7d…", "seq": 2, "status": "delivered", "decide_by": 1762275600, "extended_once": true, "…": "…" },
"request_id": "5f72…"
}Auto-accept and the cron tick
A unit nobody decides by its deadline is accepted on the buyer's behalf. A scheduler on the host drives it.
/api/market/cron/tickX-Cron-TokenX-Cron-Token equal to the server's MARKET_CRON_TOKEN. Not an API-key route. One pass: accepts up to 100 delivered units whose decide_by has passed on orders that are in_progress or delivered, oldest deadline first, one transaction per unit, and publishes reviews that have waited 14 days for the other side once the order's review window has closed (never on a disputed order), with a reviews_published event. Idempotent; schedule it every 5 minutes. No token set on the server answers 403 cron_not_configured; a wrong header answers 403 cron_token_invalid. GET on the same path answers { ok, configured } and runs nothing. Skipped: units whose auto-accept is paused, units of a frozen buyer, units with no deadline, and units whose checks are still running, for up to 48 hours past the deadline.# Scheduled on the host, every 5 minutes. The token is MARKET_CRON_TOKEN.
curl -s -X POST https://latentenergy.com/api/market/cron/tick \
-H "X-Cron-Token: $MARKET_CRON_TOKEN"{ "ok": true, "auto_accepted": 2, "reviews_published": 0, "ran_at": 1761580000 }An auto-accepted unit is charged exactly as a buyer accept: one unit price from the hold, the provider's earning minus the fee, a unit_accepted event with decided_by: "auto" and the actor system. The order completes when it was the last undecided unit. Disputed orders are skipped. Review windows need no tick: a review after review_closes_at is refused by timestamp. The tick only publishes the lone reviews.
Reversal, cancel, dispute
What can undo an accept, and what happens to undecided units when an order ends early.
/api/market/admin/orders/:id/units/:seq/reverseadminX-Admin-Token header or an admin session. Body: reason. Allowed on an accepted unit within clawback_sec (14 days) of its decided_at. The charge moves from the buyer's spent back to available (ledger kind unit_reversal), the provider's earning is reversed (earnings kind adjust, negative), the order's charged, provider and fee totals go down, and the unit becomes voided with decided_by: "admin", the reason in reject_reason, and voided_at and access_revoked_at set. A unit_reversed event is written. The order's status does not change. Errors: 400 reason_required, 409 unit_not_accepted, 409 clawback_window_closed.curl -s -X POST https://latentenergy.com/api/market/admin/orders/$ORDER_ID/units/1/reverse \
-H "X-Admin-Token: $MARKET_ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "reason": "joint log does not match the overview video" }'{
"ok": true,
"order": { "order_id": "c3d9…", "charged_units": 5000, "provider_units": 4250, "fee_units": 750, "…": "…" },
"unit": { "seq": 1, "status": "voided", "decided_by": "admin", "voided_at": 1761580000, "access_revoked_at": 1761580000, "reject_reason": "joint log does not match the overview video", "…": "…" }
}| Action | Who | Effect on undecided units | Credits |
|---|---|---|---|
| Cancel | Buyer | Not allowed while any delivered unit is undecided (409 decide_units_first). On an order the provider accepted and then delivered nothing on for 7 days, the cancel is a short close on the provider's track record | Remaining hold released |
| Cancel | Provider | Become voided with decided_by provider_cancel, counted as neither accepted nor rejected. After the provider accepted the order, the cancel is a short close on its track record; when the buyer had rejected units on the order, only once a qualified audit upholds one of those rejects (they are queued for audit) | Remaining hold released |
| Dispute | Buyer or provider | Auto-accept pauses. An admin decides each one (accept, or reject with an optional code); the order completes on the last decision, or an admin closes the dispute when units were never delivered. That close is a short close on the provider's track record unless the admin puts the shortfall on the buyer (shortfall in the dispute_closed event) | Each admin accept charges at once; the rest of the hold is released when the order closes |
| Dispute after completion | Provider, within clawback_sec of completed_at | None; every delivered unit is already decided. Latent reviews the record, but the platform cannot re-decide a unit: a rejected unit stays rejected, and any remedy is agreed by hand. An admin then closes the dispute | Nothing moves; the hold was released when the order completed |
| Dispute on an order from a related provider | Buyer only; the provider gets 403 latent_provider_cannot_dispute | The buyer still decides each one; an admin can only reject (403 latent_unit_buyer_decides on an accept) | A buyer accept charges the unit from the hold; a reject moves nothing, and the hold for a rejected unit is released when the order closes |
A related provider is one owned by Latent or by a person who runs Latent, such as the robot cell owned by Latent's founder. On its orders the buyer's decision stands: only the buyer accepts a unit, or the deadline does for a buyer who stays silent.
Cancel, dispute, messages and reviews are done on the order page on the site for now; their routes under /api/market accept the same bearer key but are not part of the v1 surface.
Event chain
Every transition on an order is an append-only event whose hash covers the one before it. Both sides can recompute it.
{
"event_id": "9d3a…",
"order_id": "c3d9…",
"unit_id": "f08a…",
"actor_account_id": "email:7f1c…",
"actor_role": "provider",
"kind": "unit_delivered",
"payload": { "seq": 1, "manifest_sha256": "a7c1…", "seed_id": "0f6e1c2a-…", "check_status": "warn", "files": 3, "decide_by": 1761667200 },
"prev_hash": "4c0e…",
"hash": "b81f…",
"created_at": 1761062400
}| kind | Actor | Payload |
|---|---|---|
order_created | buyer | quantity, unit_type, unit_price_units, hold_units, terms_sha256 |
order_accepted, order_declined | provider | none |
order_expired | system | respond_sec, released_units; the provider did not answer within the respond window, and the hold is released as on a decline |
seeds_issued | provider | count, seed_ids |
unit_delivered | provider | seq, manifest_sha256, seed_id, check_status, files, decide_by |
unit_accepted | buyer, admin or system | seq, decided_by, charged_units |
unit_rejected | buyer, admin or system | seq, decided_by, reject_code, reason |
decision_extended | buyer | seq, decide_by |
unit_reversed | admin | seq, units, reason |
order_completed | system | charged_units, released_units |
order_cancelled | buyer or provider | voided_units, released_units |
dispute_opened | buyer or provider | reason, previous_status |
dispute_closed | admin | status, released_units, review_closes_at when the order has a review window (dispute time added back), and undelivered and shortfall (provider or buyer) when units were never delivered |
review_posted | buyer or provider | subject_type, review_id, commitment: the review's seal, never a rating or its text. Events written before seals existed carry subject_type, overall, published instead and stay as written |
reviews_published | system | reason (both_reviewed or window_closed), reviews: review_id, subject_type and salt of each review published |
The hash rule: body is the canonical JSON of { order_id, unit_id, actor, role, kind, payload, at } (actor is actor_account_id, role is actor_role, at is created_at), and hash = sha256(prev_hash + "|" + body) with an empty string in place of the first event's null prev_hash. Fetch view.events from the site route and recompute; a mismatch at any event means the record was altered after the fact.
// verify-chain.mjs: node verify-chain.mjs $ORDER_ID
import { createHash } from "node:crypto";
const canonical = (v) =>
Array.isArray(v) ? `[${v.map(canonical).join(",")}]`
: v && typeof v === "object"
? `{${Object.keys(v).sort().filter((k) => v[k] !== undefined).map((k) => `${JSON.stringify(k)}:${canonical(v[k])}`).join(",")}}`
: JSON.stringify(v);
const res = await fetch(`https://latentenergy.com/api/market/orders/${process.argv[2]}`, {
headers: { Authorization: `Bearer ${process.env.LATENT_API_KEY}` },
});
const { view } = await res.json();
let prev = null;
for (const ev of view.events) {
const body = canonical({ order_id: ev.order_id, unit_id: ev.unit_id, actor: ev.actor_account_id, role: ev.actor_role, kind: ev.kind, payload: ev.payload, at: ev.created_at });
const hash = createHash("sha256").update(`${prev || ""}|${body}`).digest("hex");
if (ev.prev_hash !== prev || ev.hash !== hash) throw new Error(`chain broken at ${ev.event_id}`);
prev = ev.hash;
}
console.log(`${view.events.length} events, chain intact, head ${prev}`);Reviews are sealed in the chain. When a review is posted, review_posted carries commitment = sha256(canonical({ review_id, order_id, subject_type, overall, quality, communication, timeliness, body, salt })), where salt is 32 random hex characters the server keeps until publication and shows to nobody, and body is the text as stored and returned: trimmed, at most 3000 characters, an unpaired surrogate stored as U+FFFD and U+0000 removed. When reviews publish, reviews_published carries each one's salt, so either side can check that the published review is the one sealed when it was posted, and the record held no rating before. The order page runs this check and marks each published review that matches its seal.
// verify-seals.mjs: node verify-seals.mjs $ORDER_ID (after verify-chain.mjs, same canonical)
import { createHash } from "node:crypto";
const canonical = (v) =>
Array.isArray(v) ? `[${v.map(canonical).join(",")}]`
: v && typeof v === "object"
? `{${Object.keys(v).sort().filter((k) => v[k] !== undefined).map((k) => `${JSON.stringify(k)}:${canonical(v[k])}`).join(",")}}`
: JSON.stringify(v);
const res = await fetch(`https://latentenergy.com/api/market/orders/${process.argv[2]}`, {
headers: { Authorization: `Bearer ${process.env.LATENT_API_KEY}` },
});
const { view } = await res.json();
const commitment = new Map(view.events.filter((e) => e.kind === "review_posted" && e.payload.commitment).map((e) => [e.payload.review_id, e.payload.commitment]));
const salt = new Map(view.events.filter((e) => e.kind === "reviews_published").flatMap((e) => e.payload.reviews).filter((r) => r.salt).map((r) => [r.review_id, r.salt]));
for (const r of view.reviews.filter((x) => x.published_at != null)) {
if (!commitment.has(r.review_id)) { console.log(`${r.review_id}: posted before seals`); continue; }
const { review_id, order_id, subject_type, overall, quality, communication, timeliness, body } = r;
const sealed = canonical({ review_id, order_id, subject_type, overall, quality, communication, timeliness, body, salt: salt.get(review_id) });
const ok = createHash("sha256").update(sealed).digest("hex") === commitment.get(review_id);
console.log(`${review_id}: ${ok ? "matches its seal" : "does not match its seal"}`);
}The order page on the site shows the same chain. Anchoring the chain heads to an outside timestamp is built but not turned on in this deployment.
Reviews
Written on the site after an order ends with a charged unit. The rules that decide what the API's stats show.
| Rule | Detail |
|---|---|
| Who can review | Each side once, on a completed order, or a cancelled one, with at least one charged unit (409 order_not_completed, or 409 no_charged_units otherwise). A review of a cancelled order has order_cancelled true in the order view and the public provider JSON, and the site labels it. The flag follows the order's status, so a review written on a completed order is labelled too if a later dispute ends the order cancelled |
| Window | Until review_closes_at, 14 days after the order completed or was cancelled; time spent in a dispute is added back when the dispute closes (409 review_window_closed after it, 409 order_not_completed while disputed) |
| Publication | When both sides have written one, or 14 days after the first once the window has closed and the order is not disputed; the cron tick publishes the lone ones. Each publication writes reviews_published |
| Before publication | The view returns a review to its author only, admins included; review_posted holds its seal, not its ratings or text |
| Rating shown | Only with 3 or more published reviews from 2 or more buyer accounts in the last 12 months; weighted by the order's charged credits with no buyer above 20 percent of the weight, or an equal share when fewer than five buyers have reviewed, then by the author's rater weight |
| Review list | The profile and GET /api/market/providers/<slug> list the reviews the rating counts: published in the last 12 months, on orders not between linked accounts and, for a related provider, not between it and Latent. reviews_not_listed gives how many are left out (older, linked, latent_own), each once |
| Acceptance rate | Accepted over accepted plus rejected; voided units are excluded |
| Response time | Median of responded_at minus requested_at over responded orders; a buyer cancel before any response does not count |
Lifecycle
What each status means and what moves credits.
| Order status | Meaning | Credits |
|---|---|---|
requested | Placed by the buyer, waiting for the provider | Hold taken: quantity x unit price |
accepted | Provider accepted; seeds can be issued, nothing delivered yet | Hold kept |
declined | Provider declined, or did not answer within the respond window (3 days by default, event order_expired) | Hold released |
in_progress | At least one unit delivered, fewer than the quantity | Charged per accepted unit |
delivered | Every unit delivered, some still undecided | Charged per accepted unit |
completed | Every unit decided; review window open for 14 days | Remaining hold released |
cancelled | Cancelled by a side, under the cancel rules; with a charged unit, review window open for 14 days | Remaining hold released |
disputed | A side opened a dispute | Auto-accept paused; an admin accept charges the unit at once. On an order from a related provider the buyer still decides and an admin can only reject |
| Unit status | Meaning |
|---|---|
delivered | Manifest checked and stored; waiting for the buyer until decide_by |
accepted | Accepted by the buyer, an admin, or automatically at the deadline; one unit price charged, provider paid |
rejected | Rejected with a reason code; nothing charged |
voided | Reversed by an admin inside the clawback window, or undecided when the provider cancelled; counts for neither side |
Worked example
Four interventions at 2,500 credits each, three accepted, one rejected with a code.
# 1. Buyer: check the balance covers the hold (4 x 2500 = 10000 credits)
curl -s https://latentenergy.com/api/v1/credits -H "Authorization: Bearer $LATENT_API_KEY"
# 2. Buyer: place the order with its terms (the hold is taken now, the terms are frozen and hashed)
curl -s -X POST https://latentenergy.com/api/v1/orders \
-H "Authorization: Bearer $LATENT_API_KEY" -H "Content-Type: application/json" \
-d '{"provider_slug":"latent-axol-cell","unit_type":"intervention","quantity":4,"note":"Policy v12 handovers","idempotency_key":"policy-v12-batch-001","terms":{"required_streams":["overview","wrist"],"min_active_s":20,"instruction":"Hand the cup to the customer side of the counter."}}'
# 3. Provider: accept
curl -s -X POST https://latentenergy.com/api/v1/orders/$ORDER_ID/respond \
-H "Authorization: Bearer $PROVIDER_API_KEY" -H "Content-Type: application/json" \
-d '{"decision":"accept"}'
# 4. Provider: get four seeds before recording anything (each is valid 72 hours, single use)
curl -s -X POST https://latentenergy.com/api/v1/orders/$ORDER_ID/seeds \
-H "Authorization: Bearer $PROVIDER_API_KEY" -H "Content-Type: application/json" \
-d '{"count":4}' > seeds.json
# 5. Provider: record each episode after its seed was issued, then deliver with a manifest, one call per unit.
# unit-$i.json = { "manifest": { ...seed_id, nonce, episode, files with sha256... }, "note": "..." }
for i in 1 2 3 4; do
curl -s -X POST https://latentenergy.com/api/v1/orders/$ORDER_ID/deliver \
-H "Authorization: Bearer $PROVIDER_API_KEY" -H "Content-Type: application/json" \
--data-binary @unit-$i.json
done
# 6. Buyer: accept three, reject one with a reason code, before each unit's decide_by
for i in 1 2 3; do
curl -s -X POST https://latentenergy.com/api/v1/orders/$ORDER_ID/units/$i/decide \
-H "Authorization: Bearer $LATENT_API_KEY" -H "Content-Type: application/json" \
-d '{"decision":"accept"}'
done
curl -s -X POST https://latentenergy.com/api/v1/orders/$ORDER_ID/units/4/decide \
-H "Authorization: Bearer $LATENT_API_KEY" -H "Content-Type: application/json" \
-d '{"decision":"reject","reject_code":"takeover_late","reason":"Takeover starts 2.6 s after the stall; rubric step s1."}'
# 7. Either side: the order is completed; 7500 credits charged, 2500 released; the review window is open for 14 days
curl -s https://latentenergy.com/api/v1/orders/$ORDER_ID -H "Authorization: Bearer $LATENT_API_KEY"| After step 7 | Credits |
|---|---|
| Held at request | 10,000 |
| Charged (3 accepted x 2,500) | 7,500 |
| Released at close (1 rejected x 2,500) | 2,500 |
| Provider earnings (3 x 2,125) | 6,375 |
| Platform fee (3 x 375) | 1,125 |
| If the buyer had decided nothing by day 7 | The same 10,000 charged by the tick, 4 x 2,500, decided_by auto |
| If an admin reverses unit 1 within 14 days | 2,500 back to available, provider earnings minus 2,125, fee minus 375 |
Webhooks
None yet.
Poll GET /api/v1/orders/:id for changes. Every 30 to 60 seconds is enough; the order's status and each unit's status, decide_by and decided_at tell you what happened. Webhooks with signed payloads are planned; they will be announced on this page before they ship.
Errors
The code is in the body; the status is the HTTP status.
{ "ok": false, "error": "below_min_qty", "min_qty": 4, "request_id": "e5a1…" }| Status | Code | When |
|---|---|---|
| 400 | bad_json | The body is not a JSON object |
| 400 | idempotency_key_required | POST /orders without an idempotency key |
| 400 | provider_or_listing_required | POST /orders with neither provider_slug nor listing_id |
| 400 | bad_unit_type | unit_type is not episode, intervention or correction |
| 400 | bad_quantity | quantity is not an integer from 1 to 10,000 |
| 400 | below_min_qty (min_qty) | quantity is under the listing's minimum |
| 400 | rights_not_offered (offered) | The order's rights, or its task's, are not Non-exclusive, Undecided or empty |
| 400 | missing_instruction | POST /orders with no terms.instruction and no task_id: say what to record |
| 400 | bad_count (min, max) | seeds count is not an integer from 1 to 200 |
| 400 | reason_required | Reverse called without a reason |
| 400 | manifest_invalid (checks) | A manifest check failed; the unit was not created |
| 400 | bad_decision (allowed) | decision is not one of the allowed values |
| 400 | reject_code_required (allowed) | A reject without a code from the list |
| 400 | bad_rubric_step | rubric_step is not 1 to 40 letters, digits, dots, dashes or underscores |
| 400 | bad_evidence_url | evidence_url is not an http or https URL |
| 400 | bad_order_id, bad_seq | Malformed id or sequence number in the path |
| 401 | api_key_required | No bearer key, or a session cookie instead of a key |
| 401 | unauthorized | The key is revoked or unknown |
| 402 | insufficient_credits (missing_units) | Available credits do not cover the hold |
| 403 | forbidden_scope (scope) | The key lacks the scope the route needs |
| 403 | forbidden | The key's account is not a party to this order |
| 403 | admin_required | An admin route without the admin token or an admin session |
| 403 | latent_provider_cannot_dispute, latent_unit_buyer_decides | A provider related to Latent opening a dispute, or an admin accepting its unit: the buyer's decision stands |
| 403 | cron_not_configured, cron_token_invalid | The cron tick without MARKET_CRON_TOKEN set, or with a wrong header |
| 404 | provider_not_found, listing_not_found | Unknown slug or listing, or the listing is inactive |
| 404 | no_listing_for_unit | The provider has no active listing for that unit type |
| 404 | order_not_found, unit_not_found, task_not_found | No such order, unit, or task of yours |
| 409 | provider_not_listed | The provider exists but is not listed |
| 409 | own_provider | A buyer cannot order from its own provider profile |
| 409 | bad_state (status) | The action is not allowed in the order's current status |
| 409 | no_seed_room (quantity, delivered, open) | Every remaining unit already has an open seed |
| 409 | all_units_delivered | Deliver called after the last unit |
| 409 | unit_already_decided (status) | Decide or extend called on a decided unit |
| 409 | already_extended | Extend called twice on the same unit |
| 409 | no_deadline | Extend called on a unit with no deadline (its robot was revoked as compromised); the buyer decides it |
| 409 | decide_units_first (undecided) | Buyer cancel with undecided units |
| 409 | unit_not_accepted (status) | Reverse called on a unit that is not accepted |
| 409 | clawback_window_closed (clawback_sec) | Reverse called more than clawback_sec after the accept |
| 409 | order_not_completed, no_charged_units, review_window_closed | A review on an order that is neither completed nor cancelled with a charge (a disputed one included), with nothing charged, or after its window |
| 429 | rate_limited | Over the per-IP limit; wait a minute |
| 500 | internal_error | Our fault; send the request id |