Skip to content

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.

episode

One teleoperation or autonomous episode on a named platform that met the acceptance bar.

intervention

One live human takeover of a running policy, from stop to resume, with the failure class labelled.

correction

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.

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.

readEvery GET: providers, credits, orders
orderEvery POST: create an order, respond, issue seeds, deliver, decide, extend. The older name rent is accepted as order.
keysThe 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

FormatJSON 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 idEvery response sets X-Latent-Request-Id and repeats it as request_id in the body. Quote it when you write to us.
Rate limitsPer IP: 240 reads per minute, 60 writes per minute. Over the limit answers 429 rate_limited.
IdempotencyOrder creation requires idempotency_key. Keys are scoped to your account; a retry with the same key returns the same order with created: false.
HashesEvery 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.
PaginationNone 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.

GET/api/v1/providerspublic
Query: unit (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.
Request
curl -s "https://latentenergy.com/api/v1/providers?unit=intervention&platform=Almond%20Axol"
Response
{
  "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…"
}
acceptance_rateAccepted units divided by accepted plus rejected units, 0 to 1. Voided units count for neither side. Null until a unit was decided.
response_time_secMedian seconds between a request and the provider's accept or decline.
units_completedAccepted units across all orders.
rating_avgWeighted 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_msThe input-to-actuation latency the provider lists. A manifest's p99 above twice this value is a warning on the unit.
price_from_unitsCheapest active listing, in credits.
related_to_latentTrue 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.

units_decidedThe sample size n: units accepted, rejected or reversed, counted by decision time.
accepted_by_buyer, accepted_auto, accepted_by_adminAccepted 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_rateAccepted units an admin reversed inside the clawback window.
top_reject_familiesRejects 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_auditUnits where a qualified Latent audit disagreed with the decision. They change the score, never the credits.
short_closes, short_unitsOrders 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_lostDisputed orders where an admin rejected or reversed a unit. Those units already count as rejected or reversed.
buyers, repeat_buyersBuyers with an accepted unit, and those with accepted units on orders placed in 2 or more different weeks.
lead_time_sec, lead_time_ordersMedian 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.

The formula
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 it

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

The rating with rater weights
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.

GET/api/v1/creditsscope: read
The balance of the key's account. available_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.
Request
curl -s https://latentenergy.com/api/v1/credits \
  -H "Authorization: Bearer $LATENT_API_KEY"
Response
{
  "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.

POST/api/v1/ordersscope: order
Body: provider_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.

rubricUp 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.
formatsUp to 80 characters. Default: the task's formats.
rightsNon-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_streamsUp 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_s1 to 86,400. A unit shorter than this is refused (check duration). Unset: no minimum.
objectsUp to 30 names of up to 60 characters. Each seed carries them in a server-shuffled order.
instructionWhat 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_secThe 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.

Create an order with terms
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
    }
  }'
Response
{
  "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…"
}
When the balance is short
HTTP/1.1 402 Payment Required

{ "ok": false, "error": "insufficient_credits", "missing_units": 6000, "request_id": "d2a7…" }
GET/api/v1/ordersscope: read
Orders for the key's account. Default is the buyer side; ?role=provider lists the orders received by the account's provider profile. Newest first, up to 100.
List orders as a provider
curl -s "https://latentenergy.com/api/v1/orders?role=provider" \
  -H "Authorization: Bearer $LATENT_API_KEY"
GET/api/v1/orders/:idscope: read
The order with its terms, its units in sequence order, the provider's slug and name, and role: what the key's account is to this order (buyer or provider). Anyone else gets 403.
Request
curl -s https://latentenergy.com/api/v1/orders/$ORDER_ID \
  -H "Authorization: Bearer $LATENT_API_KEY"
Response
{
  "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…"
}
statusdelivered, accepted, rejected or voided
check_statuspass when every manifest check passed, warn when at least one warned. A failing manifest never becomes a unit.
manifest_sha256sha256 of the canonical manifest JSON as delivered
seed_idThe seed the manifest named; the seed is now used
evidence_urlThe manifest's storage_uri, else the evidence_url sent with the delivery, else null
decide_bydelivered_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_oncetrue after the buyer's one extension
decided_bybuyer, admin, auto (deadline passed) or provider_cancel (voided by a provider cancel)
reject_code, reject_reasonThe closed-list code and the free text of a rejection; the reason of a reversal
voided_at, access_revoked_atSet 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 full view: seeds, manifests, checks and events
# 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.

POST/api/v1/orders/:id/respondscope: order
Provider keys. Body: decision accept or decline. Only while the order is requested. Decline releases the whole hold to the buyer.
Accept an order
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.

POST/api/v1/orders/:id/seedsscope: order
Provider keys. Body: count, 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.
Issue four 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 }'
Response
{
  "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…"
}
GET/api/v1/orders/:id/seedsscope: read
Provider keys. Every seed issued on the order, plus open (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).
List the seeds on an order
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_idGoes into the manifest as seed_id
nonce32 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_idThe first 8 characters of the order id, a dash, and the running number of the episode on this order
instructionThe order's terms.instruction. The order note is never printed.
objectsThe terms' object list in a random order drawn by the server. Lay the scene out in that order.
layoutAlways {} in phase 1
issued_at, expires_atexpires_at is issued_at plus 259,200 (72 hours). The episode's start_timestamp must fall between them.
used_by_unitnull 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.

POST/api/v1/orders/:id/deliverscope: order
Provider keys. Body: manifest (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 }.
A manifest for an intervention unit
{
  "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 }
  ]
}
schema_versionstringExactly latent-manifest/1
seed_idstringA seed issued on this order to this provider, unused and not expired
noncestringThe seed's 32 hex characters
formatstringOne of lerobot_v3, rlds, mcap, other
storage_uristring, optionallatent://<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_idstringUp to 120 characters, required. Use the seed's episode_id.
episode.episode_indexinteger0 or more
episode.operator_idstringUp to 120 characters, required. Your id for the human operator.
episode.robot_idstringUp to 120 characters, required
episode.station_idstringUp to 120 characters, required
episode.start_timestampnumberUnix seconds (fractions allowed), between the seed's issued_at and expires_at
episode.duration_snumberMore than 0, at most 86,400; at least terms.min_active_s when set
episode.camera_countinteger1 to 16
episode.is_eval_episodebooleantrue when a policy was running. Required true for an intervention.
episode.checkpoint_pathstringUp to 300 characters. Must be empty unless is_eval_episode is true.
episode.policy_versionstringUp to 120 characters
episode.successboolean or nullYour claim about the episode's outcome. Stored as success_claim; it moves no credits.
interventionobjectRequired 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
correctionobjectRequired 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.
latencyobject, optionalp50_ms and p99_ms, non-negative integers with p99 at or above p50. Input-to-actuation of the teleop link during the episode.
filesarrayAt 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).

Deliver a unit
# 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
Response
{
  "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…"
}
Refused delivery
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.

schemaschema_version is latent-manifest/1fail
formatformat is one of the four valuesfail
storagestorage_uri, when given, is an http or https URL or latent://<upload id>fail
seedseed_id present and nonce 32 hex; the seed exists, belongs to this order and provider, is unused, is not expired, and its nonce matchesfail
seed_timestart_timestamp is at or after the seed's issued_at and at or before its expires_atfail
pi_columnsThe nine intake columns are present with the right types; checkpoint_path is empty unless is_eval_episodefail
filesAt least one file; every path present and unique; every sha256 64 hex and unique; every bytes a positive integerfail
duplicateNo sha256 is already stored under another unit, from any provider on any orderfail
intervention_spanIntervention orders: 0 <= takeover_start_s < takeover_end_s <= duration_s, is_eval_episode is true, failure_class is in the listfail
correction_refCorrection orders: correction.failed_log_ref and correction.policy_version are presentfail
durationduration_s is at least terms.min_active_s (only when the order sets it)fail
streamsEvery entry of terms.required_streams is the prefix of at least one file path (only when the order sets them)fail
latency_hardIntervention orders with latency given: p50_ms is below 300fail
latencyLatency given and malformed: fail. p99_ms above twice the provider's listed latency_ms: warn. Otherwise pass.warn fail
storage_missingNo 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
custodystorage_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_signatureA 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_identityDeliveries 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.

Upload, complete, deliver
# 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:// value

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

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 presentThat the frames are real: root on the recorder can sign anything
Security key with an attestation verified to a pinned vendor rootA hardware key of an allow-listed model approved it, touched by a personThat the frames are real
Security key without a verified attestationThe registered credential signed itWhich 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.

Preview, sign, deliver
# 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.

POST/api/v1/orders/:id/units/:seq/decidescope: order
Buyer keys. Body: decision 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.
not_as_specifiedThe unit does not do what the terms or the instruction asked
wrong_platformRecorded on a robot other than the listed platform
missing_streamsA camera or sensor stream the order needs is absent or unusable, though the manifest named a file for it
truncatedThe recording stops before the task ends, or the files are shorter than duration_s says
duplicateThe content repeats an earlier unit even though the bytes differ
quality_below_barA rubric step is not met; name it in reason
takeover_not_neededInterventions: the policy had not failed when the operator took over
takeover_lateInterventions: the operator took over after the failure had already played out
otherAnything 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.

Accept unit 1
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" }'
Reject unit 2 with a code
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." }'
Reject without a code
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…" }
POST/api/v1/orders/:id/units/:seq/extendscope: order
Buyer keys, once per unit. Moves decide_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.
Extend the deadline of unit 2
curl -s -X POST https://latentenergy.com/api/v1/orders/$ORDER_ID/units/2/extend \
  -H "Authorization: Bearer $LATENT_API_KEY"
Response
{
  "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.

POST/api/market/cron/tickX-Cron-Token
Header X-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.
The tick
# 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"
Response
{ "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.

POST/api/market/admin/orders/:id/units/:seq/reverseadmin
Admin only: an X-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.
Reverse an accepted unit
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" }'
Response
{
  "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", "…": "…" }
}
CancelBuyerNot 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 recordRemaining hold released
CancelProviderBecome 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
DisputeBuyer or providerAuto-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 completionProvider, within clawback_sec of completed_atNone; 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 disputeNothing moves; the hold was released when the order completed
Dispute on an order from a related providerBuyer only; the provider gets 403 latent_provider_cannot_disputeThe 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.

An event
{
  "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
}
order_createdbuyerquantity, unit_type, unit_price_units, hold_units, terms_sha256
order_accepted, order_declinedprovidernone
order_expiredsystemrespond_sec, released_units; the provider did not answer within the respond window, and the hold is released as on a decline
seeds_issuedprovidercount, seed_ids
unit_deliveredproviderseq, manifest_sha256, seed_id, check_status, files, decide_by
unit_acceptedbuyer, admin or systemseq, decided_by, charged_units
unit_rejectedbuyer, admin or systemseq, decided_by, reject_code, reason
decision_extendedbuyerseq, decide_by
unit_reversedadminseq, units, reason
order_completedsystemcharged_units, released_units
order_cancelledbuyer or providervoided_units, released_units
dispute_openedbuyer or providerreason, previous_status
dispute_closedadminstatus, 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_postedbuyer or providersubject_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_publishedsystemreason (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 the chain of an order
// 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.

Check published reviews against their seals
// 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.

Who can reviewEach 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
WindowUntil 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)
PublicationWhen 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 publicationThe view returns a review to its author only, admins included; review_posted holds its seal, not its ratings or text
Rating shownOnly 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 listThe 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 rateAccepted over accepted plus rejected; voided units are excluded
Response timeMedian 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.

requestedPlaced by the buyer, waiting for the providerHold taken: quantity x unit price
acceptedProvider accepted; seeds can be issued, nothing delivered yetHold kept
declinedProvider declined, or did not answer within the respond window (3 days by default, event order_expired)Hold released
in_progressAt least one unit delivered, fewer than the quantityCharged per accepted unit
deliveredEvery unit delivered, some still undecidedCharged per accepted unit
completedEvery unit decided; review window open for 14 daysRemaining hold released
cancelledCancelled by a side, under the cancel rules; with a charged unit, review window open for 14 daysRemaining hold released
disputedA side opened a disputeAuto-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
deliveredManifest checked and stored; waiting for the buyer until decide_by
acceptedAccepted by the buyer, an admin, or automatically at the deadline; one unit price charged, provider paid
rejectedRejected with a reason code; nothing charged
voidedReversed 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.

Both sides, end to end
# 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"
Held at request10,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 7The same 10,000 charged by the tick, 4 x 2,500, decided_by auto
If an admin reverses unit 1 within 14 days2,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.

Shape
{ "ok": false, "error": "below_min_qty", "min_qty": 4, "request_id": "e5a1…" }
400bad_jsonThe body is not a JSON object
400idempotency_key_requiredPOST /orders without an idempotency key
400provider_or_listing_requiredPOST /orders with neither provider_slug nor listing_id
400bad_unit_typeunit_type is not episode, intervention or correction
400bad_quantityquantity is not an integer from 1 to 10,000
400below_min_qty (min_qty)quantity is under the listing's minimum
400rights_not_offered (offered)The order's rights, or its task's, are not Non-exclusive, Undecided or empty
400missing_instructionPOST /orders with no terms.instruction and no task_id: say what to record
400bad_count (min, max)seeds count is not an integer from 1 to 200
400reason_requiredReverse called without a reason
400manifest_invalid (checks)A manifest check failed; the unit was not created
400bad_decision (allowed)decision is not one of the allowed values
400reject_code_required (allowed)A reject without a code from the list
400bad_rubric_steprubric_step is not 1 to 40 letters, digits, dots, dashes or underscores
400bad_evidence_urlevidence_url is not an http or https URL
400bad_order_id, bad_seqMalformed id or sequence number in the path
401api_key_requiredNo bearer key, or a session cookie instead of a key
401unauthorizedThe key is revoked or unknown
402insufficient_credits (missing_units)Available credits do not cover the hold
403forbidden_scope (scope)The key lacks the scope the route needs
403forbiddenThe key's account is not a party to this order
403admin_requiredAn admin route without the admin token or an admin session
403latent_provider_cannot_dispute, latent_unit_buyer_decidesA provider related to Latent opening a dispute, or an admin accepting its unit: the buyer's decision stands
403cron_not_configured, cron_token_invalidThe cron tick without MARKET_CRON_TOKEN set, or with a wrong header
404provider_not_found, listing_not_foundUnknown slug or listing, or the listing is inactive
404no_listing_for_unitThe provider has no active listing for that unit type
404order_not_found, unit_not_found, task_not_foundNo such order, unit, or task of yours
409provider_not_listedThe provider exists but is not listed
409own_providerA buyer cannot order from its own provider profile
409bad_state (status)The action is not allowed in the order's current status
409no_seed_room (quantity, delivered, open)Every remaining unit already has an open seed
409all_units_deliveredDeliver called after the last unit
409unit_already_decided (status)Decide or extend called on a decided unit
409already_extendedExtend called twice on the same unit
409no_deadlineExtend called on a unit with no deadline (its robot was revoked as compromised); the buyer decides it
409decide_units_first (undecided)Buyer cancel with undecided units
409unit_not_accepted (status)Reverse called on a unit that is not accepted
409clawback_window_closed (clawback_sec)Reverse called more than clawback_sec after the accept
409order_not_completed, no_charged_units, review_window_closedA review on an order that is neither completed nor cancelled with a charge (a disputed one included), with nothing charged, or after its window
429rate_limitedOver the per-IP limit; wait a minute
500internal_errorOur fault; send the request id