# agent-exec > Accountless, pay-per-use code execution for software agents: submit source code, get stdout, stderr and exit code back from an isolated, network-less sandbox. No account, no API key -- possession of valid payment is sufficient. ## Paid endpoints (HTTP 402, USDC on base via x402 -- no account, no API key, no signup) - POST https://agent-exec.45.67.221.128.sslip.io/execute -- run source code to completion in the sandbox. Runtimes: python3, node. From $0.0010 at default limits; larger limits cost more -- call POST /quote first for the exact price of a given limits object. - POST https://agent-exec.45.67.221.128.sslip.io/listings/promote -- buy paid placement for a directory listing you own, priced by time: day $0.0667, week $0.3333, month $1.0000. - POST https://agent-exec.45.67.221.128.sslip.io/storage -- Accountless, pay-per-use durable object storage for software agents: pay once for a byte cap and a retention window, then read your bytes back for free with a wallet signature. No account, no API key, no public URL -- possession of the paying wallet is sufficient. Retention can be extended before it lapses via a paid POST /storage/{id}/renew, keeping the same id. Plans: small $0.0011 (65536 bytes, 86400s), medium $0.0015 (262144 bytes, 604800s), large $0.0125 (1048576 bytes, 2592000s). - POST https://agent-exec.45.67.221.128.sslip.io/claim -- atomically win a caller-named key for your own paying wallet's fleet, or learn who already holds it. Accountless compare-and-set: pays once, either wins the key (`outcome: "acquired"`) or is told who holds it and until when (`outcome: "held"`, not an error). Plans: hour $0.0023 (3600s), day $0.0168 (86400s), week $0.0470 (604800s). See "Coordinating with /claim" below. - POST https://agent-exec.45.67.221.128.sslip.io/attest -- pay to have us fetch a URL from OUR vantage point and return a signed, timestamped attestation of what we saw (hash, status, headers) -- never the body. $0.0080 flat. Retrieve it back for free at GET /attest/{id} to cite it to a third party who never paid us. See "Verifying an attestation" below. - POST https://agent-exec.45.67.221.128.sslip.io/handoff -- pay to deposit a typed briefing for your successor process: what is DONE (every entry must carry evidence of how it was verified), what is NOT done, what was IN FLIGHT at the cut, and what to pick up FIRST. $0.0033 flat, retained 3 days. Returns an id; the successor reads it back free at GET /handoff/{id} with no identity, header or payment of any kind, so neither side needs an account with us. The "done" section structurally refuses an unverified claim. - POST https://agent-exec.45.67.221.128.sslip.io/deadman -- pay to register an intent you are about to act on irreversibly, with a lease (1-604800s) and a verification probe (a GET request plus a predicate: expectedBodyContains and/or expectedStatus). $0.0037 flat. Confirm before the lease expires at POST /deadman/{id}/confirm (free); otherwise the probe runs on your behalf and the verdict -- confirmed-by-probe, refuted-by-probe, or unresolved (a first-class honest outcome, never guessed as refuted) -- is published with the probe's raw response at GET /deadman/{id}, free, forever, so you can re-run the probe yourself and check our answer. - POST https://agent-exec.45.67.221.128.sslip.io/drift -- pay to re-fetch a venue endpoint we already snapshot and diff it against our archived baseline: field_added, field_removed, weights_rebased, enum_extended, nulled_fleet_wide. Substitute the venue as a path segment: POST /drift/{venue}, e.g. /drift/percall -- the venue names one of the endpoints we already snapshot. $0.0061 flat. Returns BOTH the archived and the fresh raw body so you can verify the diff yourself rather than trusting ours. GET /drift/{venue} is free and reports whether a baseline exists, without spending the live re-fetch a POST costs. ## Free endpoints (no payment required) - POST https://agent-exec.45.67.221.128.sslip.io/quote -- price a limits object before paying. - GET https://agent-exec.45.67.221.128.sslip.io/listings -- search the directory by tag, text, tier and liveness (?alive=true). See "Liveness" below: every row carries its own probe result. - POST https://agent-exec.45.67.221.128.sslip.io/listings -- create or update a listing you own, proved by a wallet signature over the request body. - DELETE https://agent-exec.45.67.221.128.sslip.io/listings -- permanently delete a listing you own: a hard delete, not a tombstone, freeing the row against your owner cap and the directory's global cap. Proved by a wallet signature over its OWN domain-separated string `agent-exec:listing:delete:v1\n`, not the raw body -- a captured update signature cannot double as a delete. - GET https://agent-exec.45.67.221.128.sslip.io/listings/{id}/uptime -- free and unauthenticated: this listing's observed uptime, derived from the probe history this directory already accumulates. Every count ships beside the ratio so you can recompute it or apply a stricter rule than ours. Grouped by the url probed at the time, so a repointed listing does not inherit the previous host's record. - POST https://agent-exec.45.67.221.128.sslip.io/storage/{id}/renew?plan= -- PAID. Extend an object's retention, keeping its id. Same plans and same prices as POST /storage; the window is added to what the object has left, not substituted for it. `id` must be a UUID minted by POST /storage -- a free-tier id is refused, free, with STORAGE_FREE_OBJECT_NOT_ELIGIBLE; renew a free object by re-signing POST /storage/free instead. - POST https://agent-exec.45.67.221.128.sslip.io/storage/{id}/replace?plan= -- PAID. Give an object NEW CONTENTS, keeping its id. This is the one that makes stored state usable as MEMORY rather than as a write-once archive: each run overwrites the slot the last run parked. Same plans and same prices as POST /storage. `id` must be a UUID minted by POST /storage -- a free-tier id is refused, free, with STORAGE_FREE_OBJECT_NOT_ELIGIBLE; re-sign POST /storage/free instead to overwrite a free object. - POST https://agent-exec.45.67.221.128.sslip.io/storage/free -- free: store ONE object for your own wallet, up to 4096 bytes, for 86400 seconds -- renewable by re-signing, which overwrites this same object in place. A sibling of POST /storage, not a discount on it -- no plan field, since size and retention are fixed. Proved by an X-Signature header over its OWN domain, distinct from both the read and delete domains below. See "Reading storage back" below. - GET https://agent-exec.45.67.221.128.sslip.io/storage/usage -- this wallet's current object count and total stored bytes, proved by an X-Signature header, never a payment. - GET https://agent-exec.45.67.221.128.sslip.io/storage/{id} -- read back an object you paid to store, proved by an X-Signature header, never a payment. - GET https://agent-exec.45.67.221.128.sslip.io/storage/list -- page this wallet's currently-unexpired stored objects (id, size, createdAt, expiresAt, label), newest first, proved by an X-Signature header, never a payment. See "Reading storage back" below for paging. - DELETE https://agent-exec.45.67.221.128.sslip.io/storage/{id} -- erase an object you paid to store before it expires, freeing its bytes against your wallet cap. Proved by an X-Signature header, never a payment -- but over its OWN signed domain, distinct from a read: a read signature is refused here. Buys nothing back -- no refund, no retention credit transferred. - POST https://agent-exec.45.67.221.128.sslip.io/claim/free -- free: atomically win a caller-named key for 300 seconds, or learn who already holds it -- no payment, unlimited distinct keys per wallet, renewable by re-acquiring. A sibling of POST /claim, not a discount on it. Proved by an X-Signature header over its OWN domain, distinct from both the read and release domains below. See "Coordinating with /claim" below. - GET https://agent-exec.45.67.221.128.sslip.io/claim/{key} -- free: who currently holds this key, scoped to your own wallet. `claim: null` (never 404) means nobody holds it. Proved by an X-Signature header, never a payment. See "Coordinating with /claim" below. - DELETE https://agent-exec.45.67.221.128.sslip.io/claim/{key} -- free: release a key you hold before it expires. Proved by an X-Signature header, over its OWN signed domain distinct from a read -- a read signature is refused here. See "Coordinating with /claim" below. - POST https://agent-exec.45.67.221.128.sslip.io/claim/{key}/renew?plan=&holder= -- PAID. Extend a hold you already have (a heartbeat), keeping your place in line. Same plans and same prices as POST /claim; the window is added to what the key has left, not substituted for it. Identity is the paying wallet, but `holder` (unsigned) must still name the key's CURRENT holder -- a re-acquire deliberately does not extend, so this explicit, separately-paid call is the only way. See "Coordinating with /claim" below. - GET https://agent-exec.45.67.221.128.sslip.io/attest/{id} -- free and unauthenticated: read a previously-created attestation back by id, to cite it to a third party. Not wallet-scoped and needs no signature -- see "Verifying an attestation" below. - GET https://agent-exec.45.67.221.128.sslip.io/handoff/{id} -- free and unauthenticated: read a briefing back by id. Deliberately not wallet-scoped and needs no signature -- the successor is by construction a different process from the depositor and may hold no wallet at all. Possession of the id is the whole authorisation, so treat it as a secret. - POST https://agent-exec.45.67.221.128.sslip.io/deadman/{id}/confirm -- free: the live caller closes the loop before the lease expires. Deliberately not wallet-scoped and needs no signature -- possession of the id is the whole authorisation. Refused once the entry has left pending, never a silent no-op. - GET https://agent-exec.45.67.221.128.sslip.io/deadman/{id} -- free and unauthenticated: read an entry back by id, including its verdict once resolved. If the lease has lapsed and nobody resolved it yet, the probe runs right here before responding. - GET https://agent-exec.45.67.221.128.sslip.io/drift/{venue} -- free: whether we hold a baseline snapshot for this venue at all, and a hash/byte-count of it, without spending the live re-fetch POST /drift/{venue} costs. - GET https://agent-exec.45.67.221.128.sslip.io/health -- liveness check. - GET https://agent-exec.45.67.221.128.sslip.io/openapi.json -- full OpenAPI 3.1 description of every endpoint above. - GET https://agent-exec.45.67.221.128.sslip.io/.well-known/x402.json -- x402 Bazaar discovery manifest. - POST https://agent-exec.45.67.221.128.sslip.io/mcp -- MCP (Model Context Protocol) transport, streamable-http, JSON-RPC 2.0. Free and read-only: two tools, service_card (what /execute costs and where to pay) and search_listings (same data as GET /listings). MCP cannot carry an x402 402, so nothing here is purchasable -- this endpoint advertises agent-exec inside MCP-native tooling, it does not sell for it. Buying still happens over plain HTTP, at the URLs those tools return, never through this MCP connection. ## Paying a 402 challenge Every priced route above answers an unpaid request with HTTP 402: `{ x402Version, error, accepts: [PaymentRequirements] }`. Pick an entry from `accepts[]` and pay it by sending an `X-PAYMENT` request header on the SAME request, retried: base64 of the UTF-8 JSON object `{"x402Version":1,"scheme":"exact","network":"","payload":{"signature":"0x","authorization":{"from":"0x","to":"0x","value":"= maxAmountRequired>","validAfter":"","validBefore":"","nonce":"0x<32 random bytes>"}}}`. (A v2-shaped payload on a `PAYMENT-SIGNATURE` header is also accepted -- see GET /openapi.json's x-payment-header.v2 -- but X-PAYMENT is understood by every x402 client and needs no v2 support.) `payload.signature` is an EIP-712 signature over a `TransferWithAuthorization` message (EIP-3009), not a personal-sign string over the JSON. Domain: `name`/`version` from `accepts[].extra`, `chainId` for `accepts[].network` (8453 for "base", 84532 for "base-sepolia"), `verifyingContract` = `accepts[].asset`. Message fields: `from` (payer), `to` = `accepts[].payTo`, `value` (atomic units, >= `accepts[].maxAmountRequired`), `validAfter` (unix seconds, 0 is valid), `validBefore` (unix seconds; must be <= now + `accepts[].maxTimeoutSeconds` + 60s clock-skew slack -- 300s + 60s on every route above, checked locally before any facilitator call), `nonce` (0x-prefixed 32 random bytes, caller-chosen, single-use). ## Liveness (what the directory offers that a free catalog does not) Every row carries the outcome and timestamp of its own last liveness probe (lastProbeCode, lastSeenAlive, dead). We re-probe every listing on a 6-hour interval; a listing that fails 3 consecutive probes is marked dead, ranks below every live listing regardless of what its owner paid, and is eventually evicted -- after 6 hours if it never once answered a probe, 30 days otherwise (a listing under a live promotion keeps its place until that promotion lapses). We do not hide failures: we publish them per row. Pass ?alive=true to GET /listings to get back only the rows that have actually passed a probe -- dead=false AND lastSeenAlive set, which is not the same test as dead=false alone. This is the differentiator, so it is stated precisely rather than generously: an unfiltered GET /listings is RANKED by liveness, not filtered by it, and a listing that has never answered a probe reads dead=false until enough consecutive failures accumulate. Ask for ?alive=true if what you need is "only rows that have passed a probe" -- that filter is applied in SQL before pagination, so walking the cursor never leaks one. A dead row is never silently deleted without warning: every row (not just yours) carries consecutiveFailures, deadSince, and -- once dead -- evictsAt (the unix ms it will be permanently removed if nothing changes) and remedy (a one-line fix: re-POST the same listing id with a working url, which clears dead, consecutiveFailures and the eviction clock in a single write). There are no accounts and no contact field, so this response is the only channel you have to learn a row of yours is about to be deleted. Use `?owner=0x` to see only your own rows, rather than paging the whole directory. A dead row may also carry `ownerVerifiedAlternate: { url, verifiedVia, signedByOwner }` -- present only when a candidate replacement url's OWN x402 discovery document names the SAME owner as this row (proof by an address the candidate's operator actually controls, never a name or description match). This is surfaced ADDITIVELY and is NEVER substituted into `url` above -- see "Attribution" for the byte-identical-url promise that field keeps regardless. POST https://agent-exec.45.67.221.128.sslip.io/listings never refuses a listing for the host it names -- a submitter is warned, not turned away. If the url's host matches a known ephemeral-tunnel vendor (e.g. *.trycloudflare.com, *.lhr.life, *.ngrok*, *.loca.lt, *.serveo.net), the response still creates/updates the row (200/201) and adds a `warnings` array naming the host class and that the listing will start failing once the tunnel rotates -- these rotate within hours to a day by design, and this directory has no way to keep serving a url that no longer resolves. ## Listing your service Publish a url you control and that survives a restart -- your own domain, or a platform url that is stable by contract. Quick tunnels (*.trycloudflare.com, *.lhr.life, *.ngrok*, *.loca.lt, *.serveo.net) get a NEW HOSTNAME every time they restart, by their own vendor's design, not by anything this directory does -- a listing on one of these WILL eventually fail its liveness probe and be evicted (see "Liveness" above for the exact numbers). The fix costs nothing and is not a new registration: re-POST the SAME listing `id` with the new url. That is a free update, and it clears `dead`, `consecutiveFailures` and the eviction clock in the same write. There is no account to lose either way -- the signing wallet is the identity, so a rotated tunnel only ever means one field changed. Even a row that already MISSED that window and was evicted is not gone for good: re-POST the SAME id, signed by the SAME wallet that owned it, and if a matching tombstone is still on file (kept up to 90 days) it is restored under its ORIGINAL id and original createdAt -- age is not lost to an outage. Any field you omit (an `id` present makes every field optional, same as an ordinary update) comes back from what was tombstoned, so this is usually a one-field request: just the new `url`. This still counts against your normal listing caps, since it re-occupies a live slot. ## Your service is listed here and you never asked (curated rows) Some rows in this directory were listed BY US, ABOUT SOMEONE ELSE. If you run an agent-first, accountless, open-source service, we may have found it in a public registry, checked it against a published six-point rubric, probed it live and listed it for free so that you can come and take the row over. Such a row is served with `provenance: "curated"`, `claimable: true`, `operator: false` and a `claim` string, and it is NEVER published as ours. If you are reading this because you saw our probe's `Referer: https://agent-exec.45.67.221.128.sslip.io/listings` in your access log, this section is why. - POST https://agent-exec.45.67.221.128.sslip.io/listings/claim -- free. Body `{"id": "", "action": "claim" | "remove", "newOwner": ""}`. No account, no signup, no email. TWO credentials, and `claim` on your own row tells you which one applies: if we recorded a `payTo` for you at listing time, sign the message below; if your service publishes no x402 card at all, prove control of your own origin instead. Card on file: two DIFFERENT signed messages, one per action, sent as the `X-Signature` header: personal_sign(`agent-exec:listing:claim:v1\n`) to claim, personal_sign(`agent-exec:listing:remove:v1\n`) to remove. Never the raw request body, and never interchangeable -- the signature you send us is public the moment you send it, so one shared message would let anyone who saw you claim your row replay it to DELETE that row instead. No card on file: send NO `X-Signature` header, and instead serve that exact same text verbatim (no signing -- just the plaintext) as the body of `/.well-known/agent-exec-listing-claim.txt` at your own origin -- proof by control of the origin rather than a wallet. A `claim` this way also needs `newOwner` in the body (the wallet address the row should transfer to), since there is no signature to derive an owner from; `remove` does not need `newOwner`. On `claim` the row transfers to your wallet (the recovered signer, or `newOwner` on the no-card path) and becomes an ordinary owner-signed row: it keeps its id, its original createdAt and its entire probe history, so the uptime record we accumulated on your behalf comes with it. From then on it is yours to change with POST /listings and to delete with DELETE /listings, like any other. On `remove` the row is deleted immediately and unconditionally. No argument, no "are you sure", no retention period, and no difference in how we treat a row we listed last week from one we have probed for a year. A directory that is hard to leave is a directory nobody trusts to join. A row you POSTed yourself is not claimable here and returns LISTING_NOT_CLAIMABLE -- it is already yours. A well-formed signature from any wallet other than the recorded payTo returns LISTING_NOT_OWNED, and names the address that would have worked (it is public: your own card publishes it). On the no-card path, a well-known file that does not answer or does not match returns LISTING_WELL_KNOWN_CLAIM_UNVERIFIED. ## Attribution (optional, on you, not on us) Every url a listing carries in GET https://agent-exec.45.67.221.128.sslip.io/listings is served byte-identical to what its owner signed -- we never rewrite it, not even to append a tracking parameter, because the url is part of a signed body and a stray query parameter on what is usually an x402 POST endpoint with a fixed request schema can produce a real 400 for a real caller. So we cannot mark the request you make to a listed service on your behalf. If you found that service through this directory and want its operator to be able to tell, that is on you to add when you call it: send a `Referer: https://agent-exec.45.67.221.128.sslip.io/listings` header, or append `?utm_source=agent-exec` if you have checked the endpoint tolerates an extra query parameter. Unenforced and invisible to us either way -- we do not see the request you send after reading this response. We hold ourselves to the same standard the other direction: every liveness probe this service sends to a listed url (see "Liveness" above) carries both a self-identifying User-Agent and a `Referer: https://agent-exec.45.67.221.128.sslip.io/listings`, so any operator we probe can look up who is knocking and why, the same way we are asking you to let operators do for us. ## Identity No accounts, ever. In an x402 call the paying wallet is already signature-proven, so it doubles as identity with no extra credential and nothing to register: pay from a wallet and that wallet is who you are to this service. ## Rate limits Every endpoint above is capped per source IP, per 60s window. Over the cap gets HTTP 429, code LISTING_RATE_LIMITED, with a Retry-After header (seconds) and details.retryAfterMs -- never a silent drop. POST https://agent-exec.45.67.221.128.sslip.io/listings/promote has its OWN, far more generous budget, separate from GET/POST/DELETE https://agent-exec.45.67.221.128.sslip.io/listings -- reading the free directory to exhaustion never blocks you from paying us. See /openapi.json's 429 response on each route for the exact numbers. ## Storing for free POST /storage/free stores ONE object per wallet, up to 4096 bytes, for 86400 seconds -- no payment, and it is a sibling of POST /storage, not a discount on it: there is no plan field, since the size and retention are fixed rather than chosen. Calling it again renews the SAME object in place (same id, fresh content, a fresh 86400s window) rather than creating a second one, so "renewable by re-signing" is the whole free-tier lifecycle. There is no payment on this path to prove identity with, so send ?nonce=&signedAt= as query parameters and X-Signature as the header, with the body being the raw bytes to store (same as POST /storage, no JSON envelope): sign the domain-separated string `agent-exec:storage:free-write:v1 free-write ` (fields joined by a literal newline; ``/`` are the exact values sent as this call's query parameters) -- note the `free-write` domain, neither a read nor a delete signature is accepted. This is its OWN domain -- neither the read signature nor the delete signature below works here, and this one does not work for either of those. On success (201) you get { id, expiresAt, plan: "free", label } -- the id is `free:`, not a UUID, and it is exactly what GET /storage/{id}, GET /storage/usage and GET /storage/list already read, since a free object lives in the same store as a paid one. This is deliberate: GET /storage/usage and GET /storage/list are free to call but can only ever answer with real data once SOMETHING has been stored under your wallet -- POST /storage/free is the only way to make that true without paying first. ## Reading storage back POST /storage costs money; reading it back (GET /storage/{id} and GET /storage/usage) is free but requires proving the wallet that paid, via an X-Signature header -- there is no payment on the read path to reuse as proof. Send ?nonce=&signedAt= as query parameters, and X-Signature as the header: to read object , sign the domain-separated string `agent-exec:storage:read:v1 ` (fields joined by a literal newline; ``/`` are the exact values sent as this call's query parameters) -- NOT the raw request body, NOT JSON; for GET /storage/usage, is the literal string "usage", and for GET /storage/list it is the literal string "list" (neither is a real object id, and a proof minted for one is not valid for the other). GET /storage/list returns this wallet's currently-unexpired objects newest-first -- id, size, createdAt, expiresAt and label, never the bytes -- so an agent that lost the id POST /storage handed back can still find what it paid to store. `label` is the optional name you chose at write time (POST /storage?plan=...&label=), stored verbatim and echoed on the 201: it is what lets a later run tell its planner state from its scratch buffer WITHOUT fetching either, which a bare list of UUIDs cannot. At most 128 characters, non-empty, no control characters, and refused free with STORAGE_LABEL_INVALID before the paywall rather than silently truncated. It is opaque to us, it is NOT unique, and there is NO lookup-by-label -- the way to a specific object is always list, then GET /storage/{id}. GET /storage/list is PAGED at 1000 rows per call: the response carries nextCursor, and while it is not null you reach the next page by repeating the call with ?after=. The cursor is opaque -- echo it back verbatim, never parse or construct one -- and each page is a separate signed read needing its own fresh nonce. Because there is no lookup-by-label, following nextCursor is the ONLY way to reach an object past the newest 1000, so a caller holding more than that must page to see all of what it paid to store. objectCount is the whole-wallet total on every page rather than what remains, truncated is always exactly (nextCursor !== null), and an ?after= value we did not issue is refused with STORAGE_CURSOR_INVALID rather than silently served as the first page -- so a paging loop fails loudly instead of repeating page one forever. A rejected read (STORAGE_SIGNATURE_INVALID, STORAGE_SIGNATURE_MISSING, STORAGE_READ_REQUEST_INVALID) carries details.expected with this same message spelled out for the exact call that failed. ## Deleting storage early POST /storage/{id}/renew?plan= is how stored state OUTLIVES the window it was sold. Retention here is finite and paid-for and nothing extends it as a side effect -- reading an object never moves its expiry -- so without renewal your only option would be to fetch the bytes back and store them again, which re-pays to hold bytes we already have AND hands you a different id, invalidating every reference you already wrote down. Renewal keeps the id. Identity is the PAYING WALLET and nothing else (no X-Signature): the payment already proves who you are, and paying does not let you renew an object that is not yours -- another wallet's id answers 404 exactly as a nonexistent one does. Five things are refused BEFORE the payment settles, so none of them can cost you anything: an id naming a free-tier object (400 STORAGE_FREE_OBJECT_NOT_ELIGIBLE -- a free object's whole lifecycle is re-signing POST /storage/free, never a paid renewal), an id that is not yours (404), an object that has already expired (410 STORAGE_EXPIRED -- expired objects are never resurrected, store the bytes again instead), a plan whose byte allowance does not cover the object's actual size (400 STORAGE_RENEWAL_PLAN_TOO_SMALL -- renew on a plan big enough to hold it), and a renewal that would leave the object holding more than 5184000s of unexpired future (409 STORAGE_RENEWAL_WINDOW_EXCEEDED). That last one is the only refusal in this product that heals on its own: an object's TOTAL life may exceed that ceiling across many renewals, but its REMAINING prepaid window may not, so the identical call succeeds nearer the expiry. Renewing early is safe and never wasteful -- the new window is ADDED to whatever was left. On success you get { id, expiresAt, previousExpiresAt, plan } with the same id you sent. DELETE /storage/{id} erases an object before it expires, freeing its bytes against your wallet cap -- the only way to get below that cap without waiting for your own objects to age out. It is free, but it is NOT proved the same way as a read: reusing a read signature here is refused, because delete signs its own domain-separated message so a captured read proof can never double as a destruction order for the object it read. Same query parameters as a read (?nonce=, ?signedAt=) and the same X-Signature header, but to delete object , sign the domain-separated string `agent-exec:storage:delete:v1 ` (fields joined by a literal newline; ``/`` are the exact values sent as this call's query parameters) -- note the `delete` domain, a read signature is not accepted. A successful delete returns { id, deleted: true, freedBytes } -- freedBytes is not a refund or a credit; it only tells you how much room you just made. Deleting buys nothing back: no refund, and no retention credit moved to another object. ## Coordinating with /claim POST /claim is an accountless compare-and-set: pay once to atomically win a caller-named key, or learn who already holds it. It coordinates ONE PAYING WALLET's own fleet racing itself -- e.g. several workers on one wallet about to duplicate an irreversible action (a deploy, a post, a purchase) -- not mutually distrusting strangers over a shared namespace: keys are scoped to the paying wallet, invisible to and unaffected by every other wallet's keys. Send { key, holder, plan } -- key is 1..256 UTF-8 bytes naming what you want to hold (no '/' character -- it addresses a URL path segment on the read/release routes below), holder is 1..256 UTF-8 bytes identifying which of your own processes is claiming it, and plan picks the hold duration from the price table above. The response's outcome tells you what happened: "acquired" (201) means you won it; "held" (200, NOT an error) means somebody already holds it, and held.mine says whether that somebody is YOU -- the signal that stops a crash-resume or a duplicated retry from doing the irreversible action twice. Re-acquiring a key you already hold returns held+mine:true and does NOT extend the hold; acquire again with a longer plan for that. A wallet that already holds 10000 unexpired keys is refused free, before payment, with CLAIM_WALLET_KEY_LIMIT -- release keys you no longer need rather than retrying. POST /claim/free is the same compare-and-set with NO payment: it acquires for exactly 300 seconds, unlimited distinct keys per wallet, renewable by re-acquiring -- 12x shorter than the cheapest paid plan (hour, 3600s), so a hold across a deploy, a batch, or a day is on the paid ladder from the first minute. There is no `plan` field and no payment to prove identity with, so send { key, holder, nonce, signedAt } and an X-Signature header instead: to acquire key , sign the domain-separated string `agent-exec:claim:free-acquire:v1 5: : ` (fields joined by a literal newline; the key and nonce are each prefixed with their UTF-8 byte length and a colon; ``/`` are the exact values sent as this call's query parameters) -- NOT the raw request body, NOT JSON -- and note the `free-acquire` domain, distinct from both `read` and `release`; neither of those signatures is accepted here. This is its OWN domain -- neither the read signature below nor the release signature works here, and this one does not work for either of those, so a proof captured from routinely polling or releasing a lock can never be replayed into a free acquire of it. Same response shape as the paid acquire (outcome/held/mine), with `plan: "free"` and no `payment` field. GET /claim/{key} is free and tells you who holds a key in YOUR OWN wallet's namespace right now, or null if nobody does -- checking your own lock must not cost again, or a fleet polling its own hold is charged for not doing work twice. Identity is an X-Signature header, not a payment: send ?nonce=&signedAt= as query parameters and X-Signature as the header; to inspect key , sign the domain-separated string `agent-exec:claim:read:v1 5: : ` (fields joined by a literal newline; the key and nonce are each prefixed with their UTF-8 byte length and a colon; ``/`` are the exact values sent as this call's query parameters) -- NOT the raw request body, NOT JSON -- and note the `read` domain, which cannot release the key. DELETE /claim/{key} is free and releases a key you hold before it expires, so the next racer does not wait out your hold. It is NOT proved the same way as a read: reusing a read signature here is refused, because release signs its own domain-separated message so a captured inspect proof can never double as a lock-breaking order for the key it inspected. Same query style as a read, PLUS ?holder= (unsigned, checked against the key's CURRENT holder -- the check that stops one member of your own fleet from breaking a sibling's lock, which is the exact situation this product is sold for) -- to release key , sign the domain-separated string `agent-exec:claim:release:v1 5: : ` (fields joined by a literal newline; the key and nonce are each prefixed with their UTF-8 byte length and a colon; ``/`` are the exact values sent as this call's query parameters) -- NOT the raw request body, NOT JSON -- and note the `release` domain, a read signature is not accepted. A successful release returns { key, released: true }. POST /claim/{key}/renew?plan=&holder= is the HEARTBEAT: pay again to extend a hold you already have, without losing your place in line. A re-acquire of the same key deliberately does NOT extend the hold (see above) -- a retry loop must not be able to hold a key indefinitely by accident -- so this is a separate, explicitly-requested, separately-paid call. Identity is the paying wallet (no X-Signature, like POST /claim itself), but `holder` is a separate, unsigned query parameter checked against the key's CURRENT holder, same as DELETE's -- one member of your own fleet cannot extend (or indefinitely renew) a sibling's hold. The window bought is ADDED to the key's current expiry, never substituted for it, so renewing early forfeits nothing. Refused BEFORE payment settles whenever it can be predicted: a key that is not yours or has already expired answers 404 (CLAIM_NOT_FOUND -- never silently re-acquired; somebody else may already legitimately hold it), the wrong holder answers 403 (CLAIM_NOT_YOURS), and extending past 2592000s of remaining window answers 409 (CLAIM_RENEWAL_WINDOW_EXCEEDED) -- that last one heals on its own closer to expiry. On success you get { key, plan, expiresAt, previousExpiresAt, payment }. ## Verifying an attestation POST /attest sends { url } (1..2048 UTF-8 bytes, absolute http:// or https://) and pays to have us fetch it from OUR vantage point; the response never includes the fetched body, only its hash, byte count, status and headers. A fetch that fails to connect, times out, or hits a redirect refused for SSRF safety is still signed and still billed -- "we tried, from our vantage, and here is exactly what happened" is the product, not a guaranteed 200; fetch_ok/fetch_code tell you which happened. GET /attest/{attestation_id} reads it back for free, with no signature and no wallet scoping -- the whole point is that a THIRD PARTY who never paid us can fetch it and check it themselves. To verify: recover the signer of `signature` over the message `"agent-exec attestation v2"` followed by, newline-joined in this order: attestation_id, url, final_url_after_redirects, observed_at, http_status, body_sha256, body_bytes, body_truncated, content_type, fetch_ok, fetch_code, redirects -- attestation_id/url/final_url_after_redirects/observed_at/body_sha256/content_type/fetch_code each prefixed with their own UTF-8 byte length and a colon, http_status/body_bytes/body_truncated/fetch_ok/redirects interpolated as their bare String() form (booleans as "true"/"false") (any EIP-191 personal_sign recovery works -- viem's verifyMessage({address, message, signature}), ethers' verifyMessage, or equivalent) -- and check the recovered address equals `signer_address` in the response, which is also published at GET /health as attestSignerAddress if you lost track of which address to check against. An attestation created before this field set shipped was signed under the domain "agent-exec attestation v1" over only attestation_id/url/final_url_after_redirects/observed_at/http_status/body_sha256/body_bytes/content_type (no body_truncated/fetch_ok/fetch_code/redirects) -- it is NOT re-signed, so if v2 verification fails, retry once against the v1 domain and field set before concluding the attestation is invalid.