{"openapi":"3.1.0","info":{"title":"agent-exec -- pay-per-use code execution for agents","version":"0.1.0","description":"Accountless, pay-per-use code execution: submit source code over HTTP 402, get stdout, stderr and exit code back from an isolated, network-less sandbox. No account, no API key -- possession of valid payment is sufficient. Also hosts a free directory of x402 services (GET /listings) with per-row liveness data: 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.","x-guidance":"Call POST /execute to run source code: send {runtime, source} (runtime must be one of the ids in x-runtimes) and pay the 402 challenge to get back {stdout, stderr, exitCode}. Call POST /quote first if you want a price for non-default limits before paying -- it is free. GET /listings and GET /health are free and never require payment. POST /listings/promote is a second, unrelated paid resource (buying paid placement in the directory) with its own price table in x-payment-info.pricing.plans; it is not part of code execution. To pay any 402, see x-payment-header for the exact header, its base64 envelope, and the EIP-712 fields to sign -- do not guess the header name or trigger a malformed-payment error to learn it.","contact":{"email":"agentexec@448c.org"}},"servers":[{"url":"https://agent-exec.45.67.221.128.sslip.io"}],"paths":{"/execute":{"post":{"operationId":"execute","summary":"Run source code to completion in an isolated, network-less sandbox.","x-payment-info":{"protocols":[{"x402":{}}],"network":"base","payTo":"0xc8Fceb0F3611BE99c41586eA2B83F6A0911d2Ff0","price":{"mode":"dynamic","currency":"USD","min":"0.001000","max":"0.050000"},"pricing":{"model":"fixed-price-tiers","tiers":[{"name":"default","upToFraction":0,"priceUsd":0.001},{"name":"standard","upToFraction":0.25,"priceUsd":0.005},{"name":"elevated","upToFraction":0.5,"priceUsd":0.01},{"name":"high","upToFraction":0.75,"priceUsd":0.02},{"name":"max","upToFraction":1,"priceUsd":0.05}],"defaultQuote":{"limits":{"timeoutMs":5000,"maxMemoryBytes":2147483648,"maxOutputBytes":1048576,"workDirBytes":67108864},"tier":"default","priceUsd":0.001,"price":"$0.0010"}}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["runtime","source"],"properties":{"runtime":{"type":"string","enum":["python3","node"],"description":"Must be one of the allowlisted runtimes; anything else is rejected before payment."},"source":{"type":"string","description":"Program source, written to the runtime's entry file."},"stdin":{"type":"string","description":"Written to the payload's stdin, then stdin is closed."},"files":{"type":"object","additionalProperties":{"type":"string"},"description":"Extra files seeded alongside the entry file, as relative-path -> content."},"limits":{"type":"object","description":"Per-request resource budget. Every field is optional; an omitted field takes its documented default. A field outside [minimum, maximum] is rejected outright, never clamped.","properties":{"timeoutMs":{"type":"integer","minimum":100,"maximum":60000,"default":5000},"maxMemoryBytes":{"type":"integer","minimum":1207959552,"maximum":4294967296,"default":2147483648},"maxOutputBytes":{"type":"integer","minimum":1024,"maximum":8388608,"default":1048576},"workDirBytes":{"type":"integer","minimum":1048576,"maximum":268435456,"default":67108864}}}}}}}},"responses":{"200":{"description":"Execution completed, including the caller's own program exiting non-zero -- that is a successful service call, not a service error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExecuteResult"}}}},"400":{"description":"Malformed request, unsupported runtime, or an out-of-range limit. Free -- nothing was run."},"402":{"description":"Payment required. Body is an x402 challenge: `{ x402Version, error, accepts: [PaymentRequirements] }`. See x-payment-header for how to pay. A bare GET on this route also answers 402 with this same challenge, priced at the default tier -- never 405 -- so a generic crawler that never reads `outputSchema.input.method` still sees this route as alive and payable. A GET carrying `X-PAYMENT` still only ever sees the challenge; it is never charged."},"503":{"description":"The service is at capacity. Free -- nothing was run; retry."}}}},"/quote":{"get":{"operationId":"quoteDefault","summary":"Same free quote as POST, at the default tier -- a GET carries no body, so there is no `limits` to price against. Exists so a bare GET (what a generic crawler sends) reads as alive with real data rather than 405, without pretending this route is ever paid.","security":[],"responses":{"200":{"description":"Same shape as POST /quote's 200, priced at the default tier."}}},"post":{"operationId":"quote","summary":"Free, unauthenticated price quote for a given limits object. No payment required.","security":[],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"limits":{"type":"object","description":"Per-request resource budget. Every field is optional; an omitted field takes its documented default. A field outside [minimum, maximum] is rejected outright, never clamped.","properties":{"timeoutMs":{"type":"integer","minimum":100,"maximum":60000,"default":5000},"maxMemoryBytes":{"type":"integer","minimum":1207959552,"maximum":4294967296,"default":2147483648},"maxOutputBytes":{"type":"integer","minimum":1024,"maximum":8388608,"default":1048576},"workDirBytes":{"type":"integer","minimum":1048576,"maximum":268435456,"default":67108864}}}}}}}},"responses":{"200":{"description":"The normalized limits this price covers, the tier name, the price in USD, and this service's own self-description (serviceName, description, tags, payTo, network, and a `manifest` link to /.well-known/x402.json) -- the same fields that document carries, so a caller who only ever reaches /quote still learns who it is paying and for what."},"400":{"description":"REQUEST_MALFORMED -- the body is not valid JSON, is not a JSON object, or `limits` is not an object whose every field is a number. `details.field` names the offending field and `details.expected` carries the accepted shape. Free -- never priced."}}}},"/health":{"get":{"operationId":"health","summary":"Liveness check.","security":[],"responses":{"200":{"description":"The service is up. Body includes `facilitator` (\"reachable\"/\"unreachable\") and `facilitatorOperator` -- who settles payments for this service. It is always \"agent-exec (self-hosted; no third-party facilitator is in the settlement path)\": this deployment runs its own facilitator, so settlement never passes through a third party."},"503":{"description":"The process is up but its facilitator is unreachable, so no paid route can currently settle a payment. Same `facilitatorOperator` field as the 200 case."}}}},"/listings/{id}/uptime":{"get":{"operationId":"getListingUptime","summary":"Free, unauthenticated: this listing's observed uptime, derived from the probe history this directory already accumulates. Grouped by the url probed AT THE TIME, so a listing that was repointed does not inherit the previous host's record. Every count is published beside the ratio so you can apply a stricter rule without trusting ours -- which is the point of the endpoint, not a detail of it.","description":"The ratio is null, never 0 and never 1, below 5 samples (`minSamples` in the response, `insufficientSamples` on each row): too few observations is a different statement from \"down\", and publishing 0 for it is the exact defect this endpoint exists to not repeat. A 405 counts as UP and is reported separately as `refusedButReachable` -- a paid POST-only route answering a bare GET correctly is not an outage. At most 100 probes back the answer, roughly 25 days at the prober's interval; `observedFrom`/`observedTo` tell you the window you actually got.","security":[],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The listing id -- also published as `id` on every row GET /listings returns, and as returned by POST /listings."}],"responses":{"200":{"description":"One record per url this listing has been probed at, most recently observed first.","content":{"application/json":{"schema":{"type":"object","required":["listingId","currentUrl","minSamples","byUrl"],"properties":{"listingId":{"type":"string"},"currentUrl":{"type":"string","description":"The listing's url as it stands now -- not necessarily the url every row below was observed against."},"minSamples":{"type":"integer","description":"The sample floor below which `ratio` is withheld. Published so you need not read our source to compare our numbers with anyone else's."},"byUrl":{"type":"array","items":{"type":"object","required":["url","observedFrom","observedTo","samples","alive","confirmedOk","refusedButReachable","unreachable","ratio","insufficientSamples","codes"],"properties":{"url":{"type":"string","description":"The url as probed, not the listing's current url."},"observedFrom":{"type":"integer","description":"Unix ms of the oldest probe counted here."},"observedTo":{"type":"integer","description":"Unix ms of the newest probe counted here."},"samples":{"type":"integer"},"alive":{"type":"integer","description":"`confirmedOk + refusedButReachable`."},"confirmedOk":{"type":"integer"},"refusedButReachable":{"type":"integer","description":"Probes that got a 405: up, and declining that method."},"unreachable":{"type":"integer"},"ratio":{"type":["number","null"],"description":"`alive / samples`, or null when `samples` is below 5."},"insufficientSamples":{"type":"boolean","description":"True exactly when `ratio` is null for want of samples."},"codes":{"type":"object","additionalProperties":{"type":"integer"},"description":"Every probe outcome code seen, with its count, so you can apply your own rule."}}}}}}}}},"404":{"description":"LISTING_NOT_FOUND -- no listing with this id. A malformed id answers the same way: this route has no id grammar of its own to violate. Free -- nothing here is ever billable."},"429":{"description":"This source IP has exceeded its request rate limit (100 requests per 60s window). Code LISTING_RATE_LIMITED. Free -- nothing was written or charged. A `Retry-After` header (seconds) and `details.retryAfterMs` name exactly when to retry. This route shares GET /listings' free-catalogue read budget: browsing the directory and scoring its rows are the same caller doing the same thing."}}}},"/listings":{"get":{"operationId":"listListings","summary":"Free, unauthenticated search over the directory of x402 services. Live listings rank above dead ones; among equally-alive listings, paid ranks above free; within a tier, by liveness (most recently confirmed alive first) then recency (newest first).","description":"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.","security":[],"parameters":[{"name":"tag","in":"query","required":false,"schema":{"type":"string"},"description":"Filter to listings carrying this tag. Repeat the parameter to require more than one tag (AND, not OR)."},{"name":"q","in":"query","required":false,"schema":{"type":"string"},"description":"Case-insensitive substring match against name and description."},{"name":"tier","in":"query","required":false,"schema":{"type":"string","enum":["free","paid"]},"description":"Restrict to exactly this PLACEMENT tier (bought via POST /listings/promote) -- not whether the advertised service itself is free to use. See components.schemas.ListingCore.properties.tier."},{"name":"alive","in":"query","required":false,"schema":{"type":"boolean"},"description":"Restrict to listings that have actually passed a liveness probe: dead=false AND lastSeenAlive is set. dead=false alone is not enough -- a listing that has never been probed successfully (e.g. a URL that can never resolve) reads dead=false for up to several hours before 3 consecutive failures mark it dead, and would otherwise slip through. Every listing is re-probed on a 6-hour interval; alive=true means \"passed a probe within roughly the last cycle\", not \"is up right now\". alive=false is the complement: dead, or never successfully probed."},{"name":"owner","in":"query","required":false,"schema":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$"},"description":"Restrict to exactly this wallet's listings (case-insensitive). The data is already public -- this is a filter on it, not a private view, and needs no signature. The one way for an owner with no account and no support channel to see their own rows without paging the whole directory."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1},"description":"How many listings to return, at most. Defaults to 50 when omitted -- omitting this does not mean \"every listing\". Clamps silently to 200 rather than refusing a caller who asked for more."},{"name":"cursor","in":"query","required":false,"schema":{"type":"string"},"description":"Opaque continuation token from a previous response's `nextCursor`. Treat it as opaque -- the encoding is not documented and may change. It is shape-checked, not cryptographically signed: a value that fails to decode as `{asOf, offset}` is refused (400 LISTINGS_QUERY_INVALID), never silently treated as \"no cursor\", but this endpoint cannot tell a value it issued from a well-shaped one a caller constructed by hand."}],"responses":{"200":{"description":"At most `limit` matching listings, ranked as described above, plus a cursor for the next page.","content":{"application/json":{"schema":{"type":"object","required":["listings","nextCursor"],"properties":{"listings":{"type":"array","items":{"$ref":"#/components/schemas/Listing"}},"nextCursor":{"type":["string","null"],"description":"Pass as `?cursor=` to continue this search past this page, or null if there is no next page."}}}}}},"400":{"description":"LISTINGS_QUERY_INVALID -- an unrecognised `tier`/`alive`, a `tag`/`q`/`limit`/`cursor` given in a shape this endpoint does not accept (including an out-of-range `limit`, or a `cursor` that does not decode as a continuation token this endpoint could have issued), or an unknown query parameter. Free -- nothing here is ever billable."},"429":{"description":"This source IP has exceeded its request rate limit (100 requests per 60s window). Code LISTING_RATE_LIMITED. Free -- nothing was written or charged. A `Retry-After` header (seconds) and `details.retryAfterMs` name exactly when to retry. This route's ceiling is its OWN instance, sized for browsing/polling the free catalogue, and separate from POST/DELETE /listings and POST /listings/claim's shared, more conservative budget."}}},"post":{"operationId":"createOrUpdateListing","summary":"Free, accountless listing: advertise your own x402 service in this directory. Identity is the caller's wallet, proved by an EIP-191 signature over the exact raw request body rather than a supplied field -- so one wallet can never edit another's entry. Omit `id` to create; include it, owned by the signing wallet, to update. `id` is returned in this response AND on every row `GET /listings` returns -- so if you lose it, `GET /listings?owner=<your wallet address>` recovers it without re-signing anything; re-sending the SAME create-shaped body (still with `id` omitted) also still works, since if this wallet already holds a listing at this `url`, that row is updated and its existing `id` is returned again (200) instead of a new row being created (201). The body must include `nonce` and `signedAt`: `signedAt` is unix seconds and must be within 300 seconds of the server's clock, and each (wallet, nonce) pair may be used only once within that window -- both exist so a captured (body, signature) pair cannot be replayed to mint duplicate listings. 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.","security":[],"parameters":[{"name":"X-Signature","in":"header","required":true,"schema":{"type":"string"},"description":"An EIP-191 personal-sign signature over the exact raw request body, from the wallet that owns (or will own) this listing."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["url","name","description","tags","nonce","signedAt"],"properties":{"id":{"type":"string","description":"Omit to create a new listing; include to update one you own."},"url":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"tags":{"type":"array","items":{"type":"string"}},"nonce":{"type":"string","description":"Caller-chosen, unique per signature (e.g. a UUID). Reusing a (wallet, nonce) pair within the freshness window is refused with LISTING_SIGNATURE_REPLAYED."},"signedAt":{"type":"integer","description":"Unix seconds this request was signed. Must be within 300 seconds of the server's clock or the request is refused with LISTING_SIGNATURE_EXPIRED."},"curatedFrom":{"type":"string","description":"Curator-only, create-only: the registry or page this service was found in. Refused (LISTING_CURATION_FORBIDDEN) unless the signing wallet is this deployment's configured curator address, and refused (LISTING_INVALID) if `id` is present -- see docs/CURATION.md."},"curatedClaimAddress":{"type":"string","description":"Curator-only, create-only: the 0x address this service's OWN x402 card publishes as `payTo` -- the only address POST /listings/claim will later accept a signature from for this row. Same restrictions as curatedFrom."}}}}}},"responses":{"200":{"description":"Updated an existing row, OR reclaimed an evicted one. Body is `{ listing: Listing, warnings?: string[] }` -- same public view as the 201 case, never the raw stored row. Returned when: `id` was included and named a listing this wallet owns; `id` was omitted but this wallet already holds a listing at this exact `url` (how a caller that lost its `id` recovers it, same `url`/`name`/`description`/`tags` re-signed and re-sent with `id` omitted, rather than minting a duplicate row every retry); or `id` was included and named a listing this wallet owned that was since EVICTED -- a tombstone is kept up to 90 days, any field omitted from this request is filled in from it, and the restored row keeps its ORIGINAL `id` and `createdAt`. Same `warnings` shape, and same always-present `listing.promote` pointer, as the 201 case.","content":{"application/json":{"schema":{"type":"object","required":["listing"],"properties":{"listing":{"$ref":"#/components/schemas/Listing"},"warnings":{"type":"array","items":{"type":"string"}}}}}}},"201":{"description":"Created a new row. Body is `{ listing: Listing, warnings?: string[] }` -- the SAME public view GET /listings returns, never the raw stored row. Only returned when `id` was omitted AND this wallet held no existing listing at this `url` -- see the 200 case for the id-lost recovery path. `warnings` is present, non-empty, only when the listed `url`'s host matches a known-ephemeral tunnel vendor (e.g. `*.trycloudflare.com`, `*.lhr.life`) -- the listing is still created; the warning names the host class and that it will die when the tunnel rotates. `listing.promote` is always present: `{ url, method: \"POST\", plans }` naming POST /listings/promote and its live price table, the same numbers the real 402 challenge charges -- so this response alone answers \"how do I promote this\" with no other document read.","content":{"application/json":{"schema":{"type":"object","required":["listing"],"properties":{"listing":{"$ref":"#/components/schemas/Listing"},"warnings":{"type":"array","items":{"type":"string"}}}}}}},"400":{"description":"A body this endpoint cannot act on (LISTING_INVALID -- including curatedFrom/curatedClaimAddress sent alongside an `id`), no signature supplied (LISTING_SIGNATURE_MISSING), a signature that does not recover to a wallet (LISTING_SIGNATURE_INVALID), a stale `signedAt` (LISTING_SIGNATURE_EXPIRED), or a reused (wallet, nonce) pair (LISTING_SIGNATURE_REPLAYED). Free -- nothing here is ever billable."},"403":{"description":"LISTING_NOT_OWNED -- `id` names a listing owned by a different wallet than the one that signed this request. LISTING_CURATION_FORBIDDEN -- the body carried `curatedFrom`/`curatedClaimAddress` but the signing wallet is not this deployment's configured curator address. Three capacity refusals share this status rather than 429, because waiting does not fix any of them: LISTING_OWNER_CAP_EXCEEDED (this wallet is at its listing cap; promote an existing listing, which stops it counting), LISTING_GLOBAL_CAP_EXCEEDED (the directory itself is full -- no caller-side remedy) and LISTING_FREE_CAPACITY_RESERVED (the free tier's reserve is exhausted). A reclaim of an evicted, tombstoned listing (see the 200 case) is refused with these same capacity codes, on the same terms as a fresh create."},"404":{"description":"LISTING_NOT_FOUND -- `id` does not name any existing listing, AND no not-yet-restored tombstone for this (wallet, id) pair exists either (see the 200 case for when one does)."},"429":{"description":"This source IP has exceeded its request rate limit (20 requests per 60s window). Code LISTING_RATE_LIMITED. Free -- nothing was written or charged. A `Retry-After` header (seconds) and `details.retryAfterMs` name exactly when to retry."},"503":{"description":"LISTING_SIGNATURE_TRACKING_CAPACITY -- the replay guard that remembers (wallet, nonce) pairs is full, so this request cannot be checked for replay and is refused unread. Not your fault and not fixed by resending unchanged: the service, not this request, is out of room. Free."}}},"delete":{"operationId":"deleteListing","summary":"Free, accountless retraction: permanently deletes a listing you own -- a hard delete, not a tombstone, so it also frees the row against your owner cap and the directory's global cap. Identity is proved by an EIP-191 signature from the wallet that owns the listing, but -- unlike POST /listings -- NOT over the raw request body: a POST /listings update body and a DELETE /listings body overlap completely (both reduce to `{id, ...}`), so a captured update signature would otherwise double as a valid delete authorization for the same listing. Instead sign the domain-separated string `agent-exec:listing:delete:v1\\n<id>`.","security":[],"parameters":[{"name":"X-Signature","in":"header","required":true,"schema":{"type":"string"},"description":"An EIP-191 personal-sign signature over the string `agent-exec:listing:delete:v1\\n<id>` (id being the listing you are deleting) -- NOT the raw request body -- from the wallet that owns this listing."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["id"],"properties":{"id":{"type":"string","description":"The listing to delete. You must own it."}}}}}},"responses":{"200":{"description":"Deleted. Body is `{ listing: Listing }` -- the same public view GET /listings returns, as the row was immediately before deletion, never the raw stored row.","content":{"application/json":{"schema":{"type":"object","required":["listing"],"properties":{"listing":{"$ref":"#/components/schemas/Listing"}}}}}},"400":{"description":"A body this endpoint cannot act on (LISTING_INVALID), no signature supplied (LISTING_SIGNATURE_MISSING), or a signature that does not recover to a wallet (LISTING_SIGNATURE_INVALID). Free -- nothing here is ever billable."},"403":{"description":"LISTING_NOT_OWNED -- `id` names a listing owned by a different wallet than the one that signed this request. The row is not deleted."},"404":{"description":"LISTING_NOT_FOUND -- `id` does not name any existing listing."},"429":{"description":"This source IP has exceeded its request rate limit (20 requests per 60s window). Code LISTING_RATE_LIMITED. Free -- nothing was written or charged. A `Retry-After` header (seconds) and `details.retryAfterMs` name exactly when to retry."},"503":{"description":"LISTING_SIGNATURE_TRACKING_CAPACITY -- the replay guard that remembers (wallet, nonce) pairs is full, so this request cannot be checked for replay and is refused unread. Not your fault and not fixed by resending unchanged: the service, not this request, is out of room. Free."}}}},"/listings/claim":{"post":{"operationId":"claimListing","summary":"Take over, or take down, a CURATED row -- one agent-exec listed about your service without asking (`provenance: \"curated\"`, `claimable: true`). Free. TWO credentials, tried in this order: (1) if you send `X-Signature`, this is a signature from the wallet your own x402 card publishes as `payTo` -- two DIFFERENT domain-separated messages, one per action: personal_sign(`agent-exec:listing:claim:v1\\n<id>`) to claim, personal_sign(`agent-exec:listing:remove:v1\\n<id>`) to remove; never the raw body, and never interchangeable, since a signature is public the moment it is sent. (2) If your service publishes NO x402 card at all, send NO `X-Signature` header and instead serve that same domain-separated message, verbatim, as the plaintext body of `/.well-known/agent-exec-listing-claim.txt` on your own origin -- proof by control of the origin rather than a wallet. A well-known-fallback `claim` also requires `newOwner` (a wallet address) in the body, since there is no signature to derive an owner from; a well-known-fallback `remove` needs no `newOwner`. Check `claimable`/`claim` on the row (`GET /listings`) to see which credential IT expects -- a row with a recorded claim address always uses (1); one with none always uses (2).","tags":["directory"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["id","action"],"properties":{"id":{"type":"string","description":"The curated listing to claim or remove."},"action":{"type":"string","enum":["claim","remove"],"description":"Which of the two signed/served messages this is. Not defaulted: the two have opposite consequences."},"newOwner":{"type":"string","description":"Only meaningful for a well-known-fallback (no `X-Signature`) \"claim\": the wallet address this row should transfer to, since there is no signature to derive one from. Ignored on a signed request (the owner is always the recovered signer) and on any \"remove\"."}}}}}},"responses":{"200":{"description":"Body is `{ action, listing: Listing }` -- the SAME public view GET /listings returns, never the raw stored row. On `claim` the row is now owned by your wallet (the recovered signer, or `newOwner` on the well-known-fallback path) and keeps its id, its original createdAt and its entire probe history -- the uptime record we accumulated on your behalf comes with it. On `remove` the row is gone immediately (no retention, no argument), but the body still reports what the row WAS the instant before removal, through the same serializer -- there is no separate, narrower shape for a removal receipt.","content":{"application/json":{"schema":{"type":"object","required":["action","listing"],"properties":{"action":{"type":"string","enum":["claim","remove"]},"listing":{"$ref":"#/components/schemas/Listing"}}}}}},"400":{"description":"A body this endpoint cannot act on (LISTING_INVALID -- including an absent or unrecognised `action`, a malformed `newOwner`, or a well-known-fallback \"claim\" missing `newOwner`), no signature AND this row still needs one (LISTING_SIGNATURE_MISSING), or a signature that does not recover to a wallet (LISTING_SIGNATURE_INVALID). Free -- the well-known fetch (if any) never runs before these checks pass."},"403":{"description":"LISTING_NOT_CLAIMABLE -- `id` names a real listing, but not a curated one: it is already somebody's own row, so there is nothing here to take over (use POST/DELETE /listings, signed by its owner). LISTING_NOT_OWNED -- the row IS claimable by signature, but the signature recovered to a different wallet than the `payTo` this service publishes; the response names the address that would have worked, which is public on your own card. LISTING_WELL_KNOWN_CLAIM_UNVERIFIED -- the row IS claimable via the well-known fallback, but `/.well-known/agent-exec-listing-claim.txt` on its own origin either did not answer or did not contain the exact expected text. Nothing is changed in any case."},"404":{"description":"LISTING_NOT_FOUND -- `id` does not name any existing listing."},"429":{"description":"This source IP has exceeded its request rate limit (20 requests per 60s window). Code LISTING_RATE_LIMITED. Free -- nothing was written or charged. A `Retry-After` header (seconds) and `details.retryAfterMs` name exactly when to retry."}}}},"/listings/promote":{"post":{"operationId":"promoteListing","summary":"Buy paid placement for a listing you own: a promoted listing outranks free ones for as long as the promotion is live, priced by time, over this service's existing x402 paywall and receive wallet. Buying while a promotion is still live extends it rather than restarting it.","x-payment-info":{"protocols":[{"x402":{}}],"network":"base","payTo":"0xc8Fceb0F3611BE99c41586eA2B83F6A0911d2Ff0","price":{"mode":"dynamic","currency":"USD","min":"0.066700","max":"1.000000"},"pricing":{"model":"fixed-price-plans","plans":[{"duration":"day","name":"day","days":1,"priceUsd":0.0667,"price":"$0.0667"},{"duration":"week","name":"week","days":7,"priceUsd":0.3333,"price":"$0.3333"},{"duration":"month","name":"month","days":30,"priceUsd":1,"price":"$1.0000"}]}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["id","duration"],"properties":{"id":{"type":"string","description":"The listing to promote. You must pay from the wallet that owns it."},"duration":{"type":"string","enum":["day","week","month"],"description":"Which plan to buy -- see x-payment-info.pricing for the price of each."}}}}}},"responses":{"200":{"description":"Promoted. Body is `{ listing, promotion, payment }` -- the updated listing, the plan bought, and the settlement receipt.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PromotionResult"}}}},"400":{"description":"LISTING_INVALID -- a malformed body or an unrecognised `duration`. Free -- nothing was charged."},"402":{"description":"Payment required. Body is an x402 challenge: `{ x402Version, error, accepts: [PaymentRequirements] }`, priced for the single plan you asked for -- `accepts` always holds exactly one entry, never one per plan. See x-payment-header for how to pay. A bare GET on this route also answers 402 with this same challenge -- never 405."},"403":{"description":"LISTING_NOT_OWNED -- the payer is not the listing's owner. Free -- refused before settle, nothing was charged."},"404":{"description":"LISTING_NOT_FOUND -- `id` does not name any existing listing. Free -- never charge for what cannot be delivered."},"429":{"description":"This source IP has exceeded its request rate limit (100 requests per 60s window). Code LISTING_RATE_LIMITED. Free -- nothing was written or charged. A `Retry-After` header (seconds) and `details.retryAfterMs` name exactly when to retry. This route's ceiling is its OWN, separate from GET/POST/DELETE /listings' -- reading the free directory to exhaustion never counts against it."}}}},"/storage":{"post":{"operationId":"putStorageObject","summary":"Pay once to store up to a plan's byte cap for its retention window; the object is readable back for free, only by the wallet that paid, via GET /storage/{id}. The body IS the object -- no JSON envelope.","x-payment-info":{"protocols":[{"x402":{}}],"network":"base","payTo":"0xc8Fceb0F3611BE99c41586eA2B83F6A0911d2Ff0","price":{"mode":"dynamic","currency":"USD","min":"0.001100","max":"0.012500"},"pricing":{"model":"fixed-price-plans","plans":[{"name":"small","bytes":65536,"seconds":86400,"priceUsd":0.0011,"price":"$0.0011"},{"name":"medium","bytes":262144,"seconds":604800,"priceUsd":0.0015,"price":"$0.0015"},{"name":"large","bytes":1048576,"seconds":2592000,"priceUsd":0.0125,"price":"$0.0125"}]}},"parameters":[{"name":"plan","in":"query","required":true,"schema":{"type":"string","enum":["small","medium","large"]},"description":"Which plan to buy -- see x-payment-info.pricing for each plan's byte cap, retention window and price."},{"name":"label","in":"query","required":false,"schema":{"type":"string","minLength":1,"maxLength":128},"description":"Optional. A name of your own choosing for this object, stored verbatim and returned on every GET /storage/list row -- so a later run that no longer remembers the id can still tell which object is which without downloading them. Opaque to us: never parsed, indexed or deduplicated on. At most 128 characters, non-empty, and no control characters; a label breaking any of those is refused with STORAGE_LABEL_INVALID BEFORE the paywall, so it is never a charge, and is never silently truncated. Labels are NOT unique and NOT addressable -- two objects may share one, and lookup is list-then-get-by-id."}],"requestBody":{"required":true,"content":{"application/octet-stream":{"schema":{"type":"string","format":"binary","description":"The raw bytes to store. Must fit the named plan's byte cap."}}}},"responses":{"201":{"description":"Stored. Body is `{ id, expiresAt, plan, label, payment }` -- the id to read it back with, when it expires (unix ms), the plan bought, the label you gave it (or null), and the settlement receipt.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StoragePutResult"}}}},"400":{"description":"An unrecognised `plan` (STORAGE_PLAN_INVALID), a body too large for the named plan (STORAGE_BODY_TOO_LARGE_FOR_PLAN), an empty body, or a `label` that is empty, over its character cap or carries control characters (STORAGE_LABEL_INVALID). Free -- every one is checked before the payment settles, so nothing was charged."},"402":{"description":"Payment required. Body is an x402 challenge: `{ x402Version, error, accepts: [PaymentRequirements] }`, priced for the single plan you asked for -- `accepts` always holds exactly one entry, never one per plan. See x-payment-header for how to pay. A bare GET on this route also answers 402 with this same challenge -- never 405."},"429":{"description":"This source IP has exceeded its request rate limit (20 requests per 60s window). Code LISTING_RATE_LIMITED. Free -- nothing was written or charged. A `Retry-After` header (seconds) and `details.retryAfterMs` name exactly when to retry. This route's ceiling is its own instance, separate from every /listings route's."}}}},"/storage/{id}/renew":{"post":{"operationId":"renewStorageObject","summary":"Pay again to extend an object's retention, KEEPING ITS ID. The only way to keep stored state alive past the window it was sold: storing the same bytes again would mint a different id and invalidate every reference you already hold to the old one.","x-payment-info":{"protocols":[{"x402":{}}],"network":"base","payTo":"0xc8Fceb0F3611BE99c41586eA2B83F6A0911d2Ff0","price":{"mode":"dynamic","currency":"USD","min":"0.001100","max":"0.012500"},"pricing":{"model":"fixed-price-plans","plans":[{"name":"small","bytes":65536,"seconds":86400,"priceUsd":0.0011,"price":"$0.0011"},{"name":"medium","bytes":262144,"seconds":604800,"priceUsd":0.0015,"price":"$0.0015"},{"name":"large","bytes":1048576,"seconds":2592000,"priceUsd":0.0125,"price":"$0.0125"}]}},"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The object to renew. Must be one you stored: an id belonging to another wallet answers 404, exactly as a nonexistent one does. Must also be a UUID minted by POST /storage -- a free-tier id (POST /storage/free) is refused, free, with STORAGE_FREE_OBJECT_NOT_ELIGIBLE."},{"name":"plan","in":"query","required":true,"schema":{"type":"string","enum":["small","medium","large"]},"description":"Which plan to buy. Its retention window is ADDED to whatever the object has left, never substituted for it, so renewing early forfeits nothing. Its byte allowance must cover the object's actual size -- renewing a 1 MiB object on a 64 KiB plan is refused, free, with STORAGE_RENEWAL_PLAN_TOO_SMALL."}],"responses":{"200":{"description":"Renewed. Body is `{ id, expiresAt, previousExpiresAt, plan, payment }` -- the SAME id, its new expiry (unix ms), what the expiry was before, the plan bought, and the settlement receipt."},"400":{"description":"An unrecognised `plan` (STORAGE_PLAN_INVALID), STORAGE_RENEWAL_PLAN_TOO_SMALL -- the plan's byte allowance does not cover this object -- or `id` naming a free-tier object (STORAGE_FREE_OBJECT_NOT_ELIGIBLE): a free object's whole lifecycle is re-signing POST /storage/free, never a paid renewal. All free: checked before the payment settles, so nothing was charged."},"402":{"description":"Payment required. Body is an x402 challenge, priced from the same table POST /storage is priced from. A bare GET on this route also answers 402 with this same challenge -- never 405."},"404":{"description":"STORAGE_NOT_FOUND -- no such object under the paying wallet. A real id owned by someone else answers identically to one that never existed -- this route is not an existence oracle. Nothing was charged."},"405":{"description":"Any method other than POST or GET (e.g. PUT, DELETE) is refused with 405."},"409":{"description":"STORAGE_RENEWAL_WINDOW_EXCEEDED -- renewing now would leave the object holding more than 5184000s of unexpired future. An object's TOTAL life may exceed that across many renewals, but its remaining prepaid window may not. Unlike the other refusals this one heals with the clock: the identical request succeeds nearer the object's expiry. Nothing was charged."},"410":{"description":"STORAGE_EXPIRED -- the object has already expired and cannot be resurrected. Store it again; that mints a new id. Nothing was charged."},"429":{"description":"This source IP has exceeded its request rate limit (20 requests per 60s window). Code LISTING_RATE_LIMITED. Free -- nothing was written or charged. A `Retry-After` header (seconds) and `details.retryAfterMs` name exactly when to retry. This route's ceiling is ONE instance shared with every other /storage route, separate from every /listings route's -- so exhausting it on the free reads (GET /storage/list, /storage/usage, /storage/{id}) will refuse this PAID renewal until the window resets. Budget your reads accordingly."}}}},"/storage/{id}/replace":{"post":{"operationId":"replaceStorageObject","summary":"Pay to give an object NEW CONTENTS, KEEPING ITS ID. This is what makes stored state usable as agent MEMORY rather than as a write-once archive: each run overwrites the slot the last run parked, under a name you already wrote down. Storing the bytes again instead would mint a different id and dangle every reference you hold to the old one.","x-payment-info":{"protocols":[{"x402":{}}],"network":"base","payTo":"0xc8Fceb0F3611BE99c41586eA2B83F6A0911d2Ff0","price":{"mode":"dynamic","currency":"USD","min":"0.001100","max":"0.012500"},"pricing":{"model":"fixed-price-plans","plans":[{"name":"small","bytes":65536,"seconds":86400,"priceUsd":0.0011,"price":"$0.0011"},{"name":"medium","bytes":262144,"seconds":604800,"priceUsd":0.0015,"price":"$0.0015"},{"name":"large","bytes":1048576,"seconds":2592000,"priceUsd":0.0125,"price":"$0.0125"}]}},"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The object to overwrite. Must be one you stored: an id belonging to another wallet answers 404, exactly as a nonexistent one does. Must also be a UUID minted by POST /storage -- a free-tier id (POST /storage/free) is refused, free, with STORAGE_FREE_OBJECT_NOT_ELIGIBLE."},{"name":"plan","in":"query","required":true,"schema":{"type":"string","enum":["small","medium","large"]},"description":"Which plan to buy. Priced as a fresh store, because the content is entirely new. Its byte allowance must cover the NEW body, and its window is compared against what the object already has -- the longer of the two wins, so replacing early never shortens your retention."},{"name":"label","in":"query","required":false,"schema":{"type":"string"},"description":"Three-way. OMIT it to keep the object's current label (the usual case -- you are re-parking state under a name you already know). Send `?label=name` to rename. Send it empty (`?label=`) to clear it."}],"requestBody":{"required":true,"description":"The new contents, raw. Must be non-empty and within the chosen plan's byte allowance. An empty body is refused, free -- use the free DELETE /storage/{id} to remove an object rather than paying to blank it.","content":{"application/octet-stream":{"schema":{"type":"string","format":"binary"}}}},"responses":{"200":{"description":"Replaced. Body is `{ id, expiresAt, previousExpiresAt, size, previousSize, plan, label, payment }` -- the SAME id, its new expiry (unix ms), what the expiry was before, the new and previous sizes, the plan bought, the resulting label, and the settlement receipt. 200 and not 201: nothing was created."},"400":{"description":"An unrecognised `plan` (STORAGE_PLAN_INVALID), an empty body, a body larger than the named plan allows (STORAGE_BODY_TOO_LARGE_FOR_PLAN), an unusable `label` -- empty, over its character cap, or carrying control characters (STORAGE_LABEL_INVALID) -- or `id` naming a free-tier object (STORAGE_FREE_OBJECT_NOT_ELIGIBLE): a free object's whole lifecycle is re-signing POST /storage/free, never a paid replacement. All free: every one is checked before the payment settles, so nothing was charged."},"402":{"description":"Payment required. Body is an x402 challenge, priced from the same table POST /storage is priced from. A bare GET on this route also answers 402 with this same challenge -- never 405."},"403":{"description":"STORAGE_WALLET_CAP_EXCEEDED -- the new contents would push your wallet past its total byte cap. Counted as a SWAP (your total, minus this object's old size, plus the new one), so a same-size or smaller replacement never trips it. Checked before settlement; nothing was charged."},"404":{"description":"STORAGE_NOT_FOUND -- no such object under the paying wallet. A real id owned by someone else answers identically to one that never existed -- this route is not an existence oracle, and paying does not let you overwrite an object that is not yours. Nothing was charged."},"405":{"description":"Any method other than POST or GET (e.g. PUT, DELETE) is refused with 405."},"410":{"description":"STORAGE_EXPIRED -- the object has already expired and cannot be resurrected by overwriting it. Store the new bytes instead; that mints a new id. Nothing was charged."},"413":{"description":"STORAGE_OBJECT_TOO_LARGE -- the body exceeds the absolute per-object cap, whatever plan was named. Nothing was charged."},"429":{"description":"This source IP has exceeded its request rate limit (20 requests per 60s window). Code LISTING_RATE_LIMITED. Free -- nothing was written or charged. A `Retry-After` header (seconds) and `details.retryAfterMs` name exactly when to retry. This route's ceiling is ONE instance shared with every other /storage route, separate from every /listings route's -- so exhausting it on the free reads (GET /storage/list, /storage/usage, /storage/{id}) will refuse this PAID replacement until the window resets. Budget your reads accordingly."}}}},"/storage/usage":{"get":{"operationId":"getStorageUsage","summary":"Free: this wallet's current object count and total stored bytes. Identity is an EIP-191 signature in the X-Signature header, not a payment -- checking your own usage must not cost anything. sign the domain-separated string `agent-exec:storage:read:v1\nusage\n<nonce>\n<signedAt>` (fields joined by a literal newline; `<nonce>`/`<signedAt>` are the exact values sent as this call's query parameters) -- NOT the raw request body, NOT JSON (`usage` is the literal, fixed id this endpoint signs -- not a real object id).","security":[],"parameters":[{"name":"nonce","in":"query","required":true,"schema":{"type":"string"},"description":"Caller-chosen, unique per read. Must equal the `<nonce>` used in the signed message above."},{"name":"signedAt","in":"query","required":true,"schema":{"type":"integer"},"description":"Unix seconds this request was signed. Must equal the `<signedAt>` used in the signed message above, and be within 300 seconds of the server's clock."},{"name":"X-Signature","in":"header","required":true,"schema":{"type":"string"},"description":"An EIP-191 personal-sign signature (65-byte hex, 0x-prefixed) over the exact string described in this operation's summary, from the wallet whose usage is being checked."}],"responses":{"200":{"description":"Body is `{ objectCount, totalBytes }` for the signing wallet.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StorageUsageResult"}}}},"400":{"description":"Missing/malformed nonce or signedAt (STORAGE_READ_REQUEST_INVALID), or an invalid/expired/replayed signature (STORAGE_SIGNATURE_MISSING, STORAGE_SIGNATURE_INVALID, STORAGE_SIGNATURE_EXPIRED, STORAGE_SIGNATURE_REPLAYED). Free -- never billable. Every one of these carries `details.expected` naming the exact string to sign, so the refusal alone is enough to construct a correct retry."},"429":{"description":"This source IP has exceeded its request rate limit (20 requests per 60s window). Code LISTING_RATE_LIMITED. Free -- nothing was written or charged. A `Retry-After` header (seconds) and `details.retryAfterMs` name exactly when to retry. This route's ceiling is its own instance, separate from every /listings route's."},"503":{"description":"STORAGE_SIGNATURE_TRACKING_CAPACITY -- the replay guard that remembers (wallet, nonce) pairs is full, so this request cannot be checked for replay and is refused unread. Not your fault and not fixed by resending unchanged: the service, not this request, is out of room. Free."}}}},"/storage/list":{"get":{"operationId":"listStorageObjects","summary":"Free: this wallet's currently-unexpired objects, newest first -- id, size, createdAt, expiresAt and label for each, never the bytes. Use this to find an object again after losing the id POST /storage returned; `label` is whatever you passed as POST /storage's optional ?label=, or null if you passed none, and is what lets you tell your objects apart without fetching them. Fetch the bytes with a separate GET /storage/{id}. Identity is an EIP-191 signature in the X-Signature header, not a payment. sign the domain-separated string `agent-exec:storage:read:v1\nlist\n<nonce>\n<signedAt>` (fields joined by a literal newline; `<nonce>`/`<signedAt>` are the exact values sent as this call's query parameters) -- NOT the raw request body, NOT JSON (`list` is the literal, fixed id this endpoint signs -- not a real object id, and distinct from `usage`, so a proof minted for one is not valid for the other).","security":[],"parameters":[{"name":"nonce","in":"query","required":true,"schema":{"type":"string"},"description":"Caller-chosen, unique per read. Must equal the `<nonce>` used in the signed message above."},{"name":"signedAt","in":"query","required":true,"schema":{"type":"integer"},"description":"Unix seconds this request was signed. Must equal the `<signedAt>` used in the signed message above, and be within 300 seconds of the server's clock."},{"name":"X-Signature","in":"header","required":true,"schema":{"type":"string"},"description":"An EIP-191 personal-sign signature (65-byte hex, 0x-prefixed) over the exact string described in this operation's summary, from the wallet whose objects are being listed."},{"name":"after","in":"query","required":false,"schema":{"type":"string"},"description":"Optional pagination cursor. Omit it for the first page; then, for as long as the response's `nextCursor` is not null, repeat this call passing that value back verbatim as `after` to get the next page. This is the ONLY way to reach an object beyond the newest 1000: there is no lookup-by-label, so the route to any object is list-then-get-by-id. The token is opaque -- do not parse or construct one. A token this service did not issue is refused with STORAGE_CURSOR_INVALID rather than silently treated as the first page, so a paging loop fails loudly instead of repeating page one forever. The cursor is NOT part of the signed message (paging is not a privilege -- every page it reaches is already the signing wallet's own), but each page is a separate signed read and needs its own fresh nonce."}],"responses":{"200":{"description":"Body is `{ objects, objectCount, truncated, nextCursor }` for the signing wallet. `objects` holds at most 1000 entries, newest first; `objectCount` is the signing wallet's true unexpired count across ALL pages (it does not shrink as you page); `truncated` is true exactly when more rows follow this page; and `nextCursor` is the opaque token to pass as the next call's `after`, or null when this page is the last. Expired objects never appear.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StorageListResult"}}}},"400":{"description":"Missing/malformed nonce or signedAt (STORAGE_READ_REQUEST_INVALID), an `after` cursor this service did not issue (STORAGE_CURSOR_INVALID), or an invalid/expired/replayed signature (STORAGE_SIGNATURE_MISSING, STORAGE_SIGNATURE_INVALID, STORAGE_SIGNATURE_EXPIRED, STORAGE_SIGNATURE_REPLAYED). Free -- never billable. Every one of these carries `details.expected` naming the exact string to sign, so the refusal alone is enough to construct a correct retry."},"429":{"description":"This source IP has exceeded its request rate limit (20 requests per 60s window). Code LISTING_RATE_LIMITED. Free -- nothing was written or charged. A `Retry-After` header (seconds) and `details.retryAfterMs` name exactly when to retry. This route's ceiling is its own instance, separate from every /listings route's."},"503":{"description":"STORAGE_SIGNATURE_TRACKING_CAPACITY -- the replay guard that remembers (wallet, nonce) pairs is full, so this request cannot be checked for replay and is refused unread. Not your fault and not fixed by resending unchanged: the service, not this request, is out of room. Free."}}}},"/storage/free":{"post":{"operationId":"putStorageObjectFree","summary":"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 rather than creating a second one. A sibling of POST /storage, not a discount on it: no `plan` field, since size and retention are fixed, not chosen. This is what makes GET /storage/usage and GET /storage/list answerable for a wallet that has never paid. Identity is an EIP-191 signature in the X-Signature header, not a payment. sign the domain-separated string `agent-exec:storage:free-write:v1\nfree-write\n<nonce>\n<signedAt>` (fields joined by a literal newline; `<nonce>`/`<signedAt>` 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.","security":[],"parameters":[{"name":"nonce","in":"query","required":true,"schema":{"type":"string"},"description":"Caller-chosen, unique per request. Must equal the `<nonce>` used in the signed message above."},{"name":"signedAt","in":"query","required":true,"schema":{"type":"integer"},"description":"Unix seconds this request was signed. Must equal the `<signedAt>` used in the signed message above, and be within 300 seconds of the server's clock."},{"name":"X-Signature","in":"header","required":true,"schema":{"type":"string"},"description":"An EIP-191 personal-sign signature (65-byte hex, 0x-prefixed) over the exact string described in this operation's summary -- NOT the read or delete signature; neither is accepted here."},{"name":"label","in":"query","required":false,"schema":{"type":"string","minLength":1,"maxLength":128},"description":"Optional. Same rules as POST /storage's `label`: a name of your own choosing, stored verbatim and returned on GET /storage/list, refused free with STORAGE_LABEL_INVALID if empty, over the character cap, or carrying control characters."}],"requestBody":{"required":true,"content":{"application/octet-stream":{"schema":{"type":"string","format":"binary","description":"The raw bytes to store, up to 4096 bytes."}}}},"responses":{"201":{"description":"Stored (or renewed -- same response shape either way). Body is `{ id, expiresAt, plan, label }` -- `plan` is always `\"free\"`, and there is no `payment` field: nothing was charged.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StoragePutResult"}}}},"400":{"description":"An empty body, a `label` that is empty, over its character cap or carries control characters (STORAGE_FREE_REQUEST_INVALID / STORAGE_LABEL_INVALID), or an invalid/expired/replayed signature (STORAGE_SIGNATURE_MISSING, STORAGE_SIGNATURE_INVALID, STORAGE_SIGNATURE_EXPIRED, STORAGE_SIGNATURE_REPLAYED) -- a signature produced for GET /storage/{id} (the read domain) or DELETE /storage/{id} (the delete domain) is refused here. Free -- never billable. Every one of these carries `details.expected` naming the exact string to sign, so the refusal alone is enough to construct a correct retry."},"413":{"description":"STORAGE_OBJECT_TOO_LARGE -- the body exceeds the free tier's 4096-byte cap. Pay via POST /storage for more. Nothing was charged."},"429":{"description":"This source IP has exceeded its request rate limit (20 requests per 60s window). Code LISTING_RATE_LIMITED. Free -- nothing was written or charged. A `Retry-After` header (seconds) and `details.retryAfterMs` name exactly when to retry. This route's ceiling is ONE instance shared with every other /storage route -- this route pays for a signature recovery and a store call before it can refuse, so it needs the same ceiling a paid route would."},"503":{"description":"STORAGE_SIGNATURE_TRACKING_CAPACITY -- the replay guard that remembers (wallet, nonce) pairs is full. Retry shortly; nothing here is ever billable."}}}},"/mcp":{"post":{"operationId":"mcp","summary":"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.","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","description":"A JSON-RPC 2.0 request per the MCP streamable-http transport spec."}}}},"responses":{"200":{"description":"A JSON-RPC 2.0 response (or an SSE stream), per the MCP streamable-http transport spec."},"400":{"description":"Malformed JSON-RPC request -- the body was not valid JSON. A JSON-RPC 2.0 error object with `error.code` -32700. Free -- this endpoint never charges."},"406":{"description":"The request's Accept header did not include both application/json and text/event-stream, which the MCP streamable-http transport requires. This is the SDK's own refusal, not an agent-exec-specific one."},"413":{"description":"Body exceeds this endpoint's 256kb limit. A JSON-RPC 2.0 error object with `error.code` -32600. Free."}}}},"/storage/{id}":{"get":{"operationId":"getStorageObject","summary":"Free: read back an object you paid to store. Identity is an EIP-191 signature in the X-Signature header, not a payment -- a different wallet's signature over the same id is refused exactly like a nonexistent id. To read id `abc-123`, sign the domain-separated string `agent-exec:storage:read:v1\nabc-123\n<nonce>\n<signedAt>` (fields joined by a literal newline; `<nonce>`/`<signedAt>` are the exact values sent as this call's query parameters) -- NOT the raw request body, NOT JSON.","security":[],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"The id returned by POST /storage when the object was stored. This exact string is the `<id>` field in the signed message described in this operation's summary."},{"name":"nonce","in":"query","required":true,"schema":{"type":"string"},"description":"Caller-chosen, unique per read. Must equal the `<nonce>` used in the signed message above."},{"name":"signedAt","in":"query","required":true,"schema":{"type":"integer"},"description":"Unix seconds this request was signed. Must equal the `<signedAt>` used in the signed message above, and be within 300 seconds of the server's clock."},{"name":"X-Signature","in":"header","required":true,"schema":{"type":"string"},"description":"An EIP-191 personal-sign signature (65-byte hex, 0x-prefixed) over the exact string described in this operation's summary, from the wallet that paid to store this object."}],"responses":{"200":{"description":"The raw bytes as stored, `application/octet-stream`.","content":{"application/octet-stream":{"schema":{"type":"string","format":"binary"}}}},"400":{"description":"Missing/malformed nonce or signedAt (STORAGE_READ_REQUEST_INVALID), or an invalid/expired/replayed signature (STORAGE_SIGNATURE_MISSING, STORAGE_SIGNATURE_INVALID, STORAGE_SIGNATURE_EXPIRED, STORAGE_SIGNATURE_REPLAYED). Free -- never billable. Every one of these carries `details.expected` naming the exact string to sign, so the refusal alone is enough to construct a correct retry."},"404":{"description":"No such object for this wallet -- expired, deleted, never stored, or stored by a different wallet. Free."},"429":{"description":"This source IP has exceeded its request rate limit (20 requests per 60s window). Code LISTING_RATE_LIMITED. Free -- nothing was written or charged. A `Retry-After` header (seconds) and `details.retryAfterMs` name exactly when to retry. This route's ceiling is its own instance, separate from every /listings route's."}}},"delete":{"operationId":"deleteStorageObject","summary":"Free: erase an object you paid to store before it expires, freeing its bytes against your wallet cap -- this is how a wallet that filled its cap makes room without waiting for its own objects to age out. Deleting buys nothing back: no refund, and no retention credit moved to another object. Identity is an EIP-191 signature in the X-Signature header, not a payment -- and NOT the same signature GET /storage/{id} uses: a read signature here is refused, because delete is authorized by its own domain-separated message, distinct from the read domain, so a captured read proof can never double as a destruction order for the object it read. To delete id `abc-123`, sign the domain-separated string `agent-exec:storage:delete:v1\nabc-123\n<nonce>\n<signedAt>` (fields joined by a literal newline; `<nonce>`/`<signedAt>` are the exact values sent as this call's query parameters) -- note the `delete` domain, a read signature is not accepted.","security":[],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"The id returned by POST /storage when the object was stored. This exact string is the `<id>` field in the signed message described in this operation's summary. You must own the object."},{"name":"nonce","in":"query","required":true,"schema":{"type":"string"},"description":"Caller-chosen, unique per delete. Must equal the `<nonce>` used in the signed message above. Shares its single-use tracking with GET /storage/{id}'s nonce -- a (wallet, nonce) pair spent on either action cannot be reused on the other within the freshness window."},{"name":"signedAt","in":"query","required":true,"schema":{"type":"integer"},"description":"Unix seconds this request was signed. Must equal the `<signedAt>` used in the signed message above, and be within 300 seconds of the server's clock."},{"name":"X-Signature","in":"header","required":true,"schema":{"type":"string"},"description":"An EIP-191 personal-sign signature (65-byte hex, 0x-prefixed) over the exact string described in this operation's summary, from the wallet that paid to store this object. A signature produced for GET /storage/{id} (the read domain) is refused here -- delete signs its own domain-separated message so a captured read proof can never double as authorization to destroy what it read."}],"responses":{"200":{"description":"Deleted. Body is `{ id, deleted: true, freedBytes }` -- `freedBytes` is how many bytes this freed against your wallet cap, not a refund or credit.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StorageDeleteResult"}}}},"400":{"description":"Missing/malformed nonce or signedAt (STORAGE_READ_REQUEST_INVALID), or an invalid/expired/replayed signature (STORAGE_SIGNATURE_MISSING, STORAGE_SIGNATURE_INVALID, STORAGE_SIGNATURE_EXPIRED, STORAGE_SIGNATURE_REPLAYED). Free -- never billable. Every one of these carries `details.expected` naming the exact string to sign, so the refusal alone is enough to construct a correct retry."},"404":{"description":"No such object for this wallet -- never stored, already deleted, or stored by a different wallet (this route never reveals which). Free -- the row is untouched."},"410":{"description":"The object exists and you own it, but it has already expired -- STORAGE_EXPIRED, not reported as a successful delete. Free -- the row is untouched."},"429":{"description":"This source IP has exceeded its request rate limit (20 requests per 60s window). Code LISTING_RATE_LIMITED. Free -- nothing was written or charged. A `Retry-After` header (seconds) and `details.retryAfterMs` name exactly when to retry. This route's ceiling is its own instance, separate from every /listings route's."}}}},"/claim":{"post":{"operationId":"acquireClaim","summary":"Pay once to atomically win a caller-named key for a paying wallet's own fleet -- an accountless compare-and-set. `outcome: \"acquired\"` (201) means you won it; `outcome: \"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 duplicated retry from doing an irreversible action twice. Scoped to the PAYING WALLET: one payer's keys are invisible to and unaffected by every other payer's. Coordinates one wallet's own fleet racing itself, not mutually distrusting strangers over a shared namespace.","x-payment-info":{"protocols":[{"x402":{}}],"network":"base","payTo":"0xc8Fceb0F3611BE99c41586eA2B83F6A0911d2Ff0","price":{"mode":"dynamic","currency":"USD","min":"0.002300","max":"0.047000"},"pricing":{"model":"fixed-price-plans","plans":[{"name":"hour","seconds":3600,"priceUsd":0.0023,"price":"$0.0023"},{"name":"day","seconds":86400,"priceUsd":0.0168,"price":"$0.0168"},{"name":"week","seconds":604800,"priceUsd":0.047,"price":"$0.0470"}]}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["key","holder","plan"],"properties":{"key":{"type":"string","minLength":1,"maxLength":256,"description":"1..256 UTF-8 bytes, no '/' character -- the name you want to hold, scoped to your own paying wallet."},"holder":{"type":"string","minLength":1,"maxLength":256,"description":"1..256 UTF-8 bytes -- an id identifying which of your own processes is claiming it."},"plan":{"type":"string","enum":["hour","day","week"],"description":"Which plan to buy -- see x-payment-info.pricing for each plan's hold duration and price."}}}}}},"responses":{"200":{"description":"Somebody already holds this key. Body is `{ outcome: \"held\", plan, claim, mine }` -- `claim` is `{ key, holder, acquiredAt, expiresAt }` for the CURRENT holder, and `mine` is true exactly when that holder is you. This is a successful call, not a refusal: you paid to learn who holds the key, atomically, and that is what you were sold.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClaimResult"}}}},"201":{"description":"You won the key. Body is `{ outcome: \"acquired\", plan, claim, payment }` -- `claim` is `{ key, holder, acquiredAt, expiresAt }` for the hold you just created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClaimResult"}}}},"400":{"description":"Malformed body or an unrecognised `plan` (CLAIM_REQUEST_INVALID / CLAIM_PLAN_INVALID). The store refuses the two text fields separately and says which: CLAIM_KEY_INVALID -- `key` was empty, not a string, or over its byte cap; CLAIM_HOLDER_INVALID -- the same, for `holder`. All four are checked BEFORE the paywall, so none of them is ever a charge. Free -- nothing was charged."},"402":{"description":"Payment required. Body is an x402 challenge: `{ x402Version, error, accepts: [PaymentRequirements] }`, priced for the single plan you asked for -- `accepts` always holds exactly one entry, never one per plan. See x-payment-header for how to pay. A bare GET on this route also answers 402 with this same challenge -- never 405."},"403":{"description":"CLAIM_WALLET_KEY_LIMIT -- this wallet already holds 10000 unexpired keys, its cap. Checked BEFORE settlement (`ClaimLedger.wouldRefuse()`), so nothing was charged. The remedy is releasing keys you no longer need via DELETE /claim/{key}, not retrying."},"429":{"description":"This source IP has exceeded its request rate limit (20 requests per 60s window). Code LISTING_RATE_LIMITED. Free -- nothing was written or charged. A `Retry-After` header (seconds) and `details.retryAfterMs` name exactly when to retry. This route's ceiling is its own instance, separate from every /listings and /storage route's."}}}},"/claim/free":{"post":{"operationId":"acquireClaimFree","summary":"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: this lease is 12x shorter than the cheapest paid plan (`hour`, 3600s), so a caller needing a lock across a deploy, a batch, or a day is on the paid ladder from the first minute. Identity is an EIP-191 signature in the X-Signature header, over its OWN domain -- distinct from both the read domain (GET /claim/{key}) and the release domain (DELETE /claim/{key}), so neither of those signatures works here and a captured one of either can never be replayed into a free acquire. To acquire key `deploy-lock`, sign the domain-separated string `agent-exec:claim:free-acquire:v1\n11:deploy-lock\n<byteLength(nonce)>:<nonce>\n<signedAt>` (fields joined by a literal newline; the key and nonce are each prefixed with their UTF-8 byte length and a colon; `<nonce>`/`<signedAt>` are the exact values sent as this call's JSON request body fields) -- you sign this exact string, NOT the JSON body that carries these values -- and note the `free-acquire` domain, distinct from both `read` and `release`; neither of those signatures is accepted here.","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["key","holder","nonce","signedAt"],"properties":{"key":{"type":"string","minLength":1,"maxLength":256,"description":"1..256 UTF-8 bytes, no '/' character -- the name you want to hold, scoped to your own wallet. This exact string is the `<key>` field in the signed message described in this operation's summary."},"holder":{"type":"string","minLength":1,"maxLength":256,"description":"1..256 UTF-8 bytes -- an id identifying which of your own processes is claiming it. Unsigned, exactly like DELETE /claim/{key}'s `holder` -- the signature proves the wallet, not which of its processes is acting."},"nonce":{"type":"string","description":"Caller-chosen, unique per request. Must equal the `<nonce>` used in the signed message above. Its own namespace is shared with GET/DELETE /claim/{key}: a (wallet, nonce) pair spent on any one of the three cannot be reused on either of the others within the freshness window."},"signedAt":{"type":"integer","description":"Unix seconds this request was signed. Must equal the `<signedAt>` used in the signed message above, and be within 300 seconds of the server's clock."}}}}}},"responses":{"200":{"description":"Somebody already holds this key. Body is `{ outcome: \"held\", plan: \"free\", claim, mine }` -- `claim` is `{ key, holder, acquiredAt, expiresAt }` for the CURRENT holder, and `mine` is true exactly when that holder is you. This is a successful call, not a refusal.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClaimResult"}}}},"201":{"description":"You won the key, for 300 seconds. Body is `{ outcome: \"acquired\", plan: \"free\", claim }` -- `claim` is `{ key, holder, acquiredAt, expiresAt }` for the hold you just created. No `payment` field: nothing was charged.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClaimResult"}}}},"400":{"description":"CLAIM_FREE_REQUEST_INVALID -- the body is not a JSON object, or `key`/`holder`/`nonce`/`signedAt` is missing or malformed (checked in that order, before the signature is ever recovered). Or an invalid/expired/replayed signature (CLAIM_SIGNATURE_MISSING, CLAIM_SIGNATURE_INVALID, CLAIM_SIGNATURE_EXPIRED, CLAIM_SIGNATURE_REPLAYED) -- a signature produced for GET /claim/{key} (the read domain) or DELETE /claim/{key} (the release domain) is refused here. Every one of these carries `details.expected` naming the exact string to sign, so the refusal alone is enough to construct a correct retry. Free -- never billable, since there is no payment on this route at all. Once the wallet is known, CLAIM_KEY_INVALID or CLAIM_HOLDER_INVALID can also surface here if the store disagrees with this route's own pre-check, though the two are checked identically and this should not occur live."},"403":{"description":"CLAIM_WALLET_KEY_LIMIT -- this wallet already holds 10000 unexpired keys, its cap, counting BOTH free and paid holds -- there is only one namespace per wallet. The remedy is releasing keys you no longer need via DELETE /claim/{key}, not retrying."},"429":{"description":"This source IP has exceeded its request rate limit (20 requests per 60s window). Code LISTING_RATE_LIMITED. Free -- nothing was written or charged. A `Retry-After` header (seconds) and `details.retryAfterMs` name exactly when to retry. This route's ceiling is shared with POST /claim, GET /claim/{key} and DELETE /claim/{key}, and separate from every other product's -- this route pays for a signature recovery and a store call before it can refuse, so it needs the same ceiling a paid route would."},"503":{"description":"CLAIM_SIGNATURE_TRACKING_CAPACITY -- the nonce tracker is momentarily full. Retry shortly; nothing here is ever billable."}}}},"/claim/{key}":{"get":{"operationId":"inspectClaim","summary":"Free: who currently holds this key, if anyone -- scoped to YOUR wallet, so a stranger cannot use this to probe your namespace. Identity is an EIP-191 signature in the X-Signature header, not a payment -- checking your own lock must not cost again, or a fleet polling its own hold is charged for not doing work twice. A key nobody holds (or holds no longer) answers 200 with `claim: null`, never 404: \"is this free?\" is a legitimate question with a legitimate negative answer. To inspect key `deploy-lock`, sign the domain-separated string `agent-exec:claim:read:v1\n11:deploy-lock\n<byteLength(nonce)>:<nonce>\n<signedAt>` (fields joined by a literal newline; the key and nonce are each prefixed with their UTF-8 byte length and a colon; `<nonce>`/`<signedAt>` 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.","security":[],"parameters":[{"name":"key","in":"path","required":true,"schema":{"type":"string"},"description":"The key to inspect. This exact string is the `<key>` field in the signed message described in this operation's summary."},{"name":"nonce","in":"query","required":true,"schema":{"type":"string"},"description":"Caller-chosen, unique per read. Must equal the `<nonce>` used in the signed message above."},{"name":"signedAt","in":"query","required":true,"schema":{"type":"integer"},"description":"Unix seconds this request was signed. Must equal the `<signedAt>` used in the signed message above, and be within 300 seconds of the server's clock."},{"name":"X-Signature","in":"header","required":true,"schema":{"type":"string"},"description":"An EIP-191 personal-sign signature (65-byte hex, 0x-prefixed) over the exact string described in this operation's summary, from the wallet whose namespace is being inspected."}],"responses":{"200":{"description":"Body is `{ key, claim }` -- `claim` is `{ key, holder, acquiredAt, expiresAt }` for whoever currently holds it, or `null` if nobody does (including if it expired)."},"400":{"description":"Missing/malformed nonce or signedAt (CLAIM_READ_REQUEST_INVALID), or an invalid/expired/replayed signature (CLAIM_SIGNATURE_MISSING, CLAIM_SIGNATURE_INVALID, CLAIM_SIGNATURE_EXPIRED, CLAIM_SIGNATURE_REPLAYED). Free -- never billable. Every one of these carries `details.expected` naming the exact string to sign, so the refusal alone is enough to construct a correct retry. A signature produced for DELETE /claim/{key} (the release domain) is refused here."},"429":{"description":"This source IP has exceeded its request rate limit (20 requests per 60s window). Code LISTING_RATE_LIMITED. Free -- nothing was written or charged. A `Retry-After` header (seconds) and `details.retryAfterMs` name exactly when to retry. This route's ceiling is its own instance, shared with DELETE /claim/{key} and separate from every other product's."},"503":{"description":"CLAIM_SIGNATURE_TRACKING_CAPACITY -- the nonce tracker is momentarily full. Retry shortly; nothing here is ever billable."}}},"delete":{"operationId":"releaseClaim","summary":"Free: give up a key you hold before it expires, so the next racer does not have to wait out your hold. Identity is an EIP-191 signature in the X-Signature header, not a payment -- and NOT the same signature GET /claim/{key} uses: release signs its own domain-separated message, distinct from the read domain, so a captured inspect proof can never double as a lock-breaking order for the key it inspected. The signature proves the WALLET; it does not by itself authorize the release -- `holder` is a separate, unsigned query parameter, and the release is refused unless it names the CURRENT holder (CLAIM_NOT_YOURS), which is what stops one member of your own fleet from breaking a sibling's lock. To release key `deploy-lock`, sign the domain-separated string `agent-exec:claim:release:v1\n11:deploy-lock\n<byteLength(nonce)>:<nonce>\n<signedAt>` (fields joined by a literal newline; the key and nonce are each prefixed with their UTF-8 byte length and a colon; `<nonce>`/`<signedAt>` 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.","security":[],"parameters":[{"name":"key","in":"path","required":true,"schema":{"type":"string"},"description":"The key to release. This exact string is the `<key>` field in the signed message described in this operation's summary."},{"name":"holder","in":"query","required":true,"schema":{"type":"string"},"description":"Which of your holders is releasing the key. Unsigned -- checked against the key's CURRENT holder, not against anything in the signature."},{"name":"nonce","in":"query","required":true,"schema":{"type":"string"},"description":"Caller-chosen, unique per release. Must equal the `<nonce>` used in the signed message above. Shares its single-use tracking with GET /claim/{key}'s nonce -- a (wallet, nonce) pair spent on either action cannot be reused on the other within the freshness window."},{"name":"signedAt","in":"query","required":true,"schema":{"type":"integer"},"description":"Unix seconds this request was signed. Must equal the `<signedAt>` used in the signed message above, and be within 300 seconds of the server's clock."},{"name":"X-Signature","in":"header","required":true,"schema":{"type":"string"},"description":"An EIP-191 personal-sign signature (65-byte hex, 0x-prefixed) over the exact string described in this operation's summary, from the wallet that holds this key. A signature produced for GET /claim/{key} (the read domain) is refused here."}],"responses":{"200":{"description":"Released. Body is `{ key, released: true }`."},"400":{"description":"Missing/malformed holder/nonce/signedAt (CLAIM_READ_REQUEST_INVALID), or an invalid/expired/replayed signature (CLAIM_SIGNATURE_MISSING, CLAIM_SIGNATURE_INVALID, CLAIM_SIGNATURE_EXPIRED, CLAIM_SIGNATURE_REPLAYED). Free -- never billable. Every one of these carries `details.expected` naming the exact string to sign."},"403":{"description":"CLAIM_NOT_YOURS -- the wallet is right, but `holder` does not name the key's current holder. The body never discloses who does; the status is the whole disclosure. The row is untouched."},"404":{"description":"CLAIM_NOT_FOUND -- nobody holds this key, whether it never was, was already released, or has expired. All three answer identically: a claim key is caller-supplied text, so distinguishing them would let an unpaid prober enumerate a namespace. Free."},"429":{"description":"This source IP has exceeded its request rate limit (20 requests per 60s window). Code LISTING_RATE_LIMITED. Free -- nothing was written or charged. A `Retry-After` header (seconds) and `details.retryAfterMs` name exactly when to retry. This route's ceiling is its own instance, shared with GET /claim/{key} and separate from every other product's."},"503":{"description":"CLAIM_SIGNATURE_TRACKING_CAPACITY -- the nonce tracker is momentarily full. Retry shortly; nothing here is ever billable."}}}},"/claim/{key}/renew":{"post":{"operationId":"renewClaim","summary":"Pay again to EXTEND a hold you already have, without losing your place in line. The heartbeat that lets a lease be SHORT (so a crash frees it fast) while a live holder keeps it alive by paying again -- a re-acquire deliberately does NOT do this (see POST /claim), so a retry loop cannot hold a key indefinitely by accident.","x-payment-info":{"protocols":[{"x402":{}}],"network":"base","payTo":"0xc8Fceb0F3611BE99c41586eA2B83F6A0911d2Ff0","price":{"mode":"dynamic","currency":"USD","min":"0.002300","max":"0.047000"},"pricing":{"model":"fixed-price-plans","plans":[{"name":"hour","seconds":3600,"priceUsd":0.0023,"price":"$0.0023"},{"name":"day","seconds":86400,"priceUsd":0.0168,"price":"$0.0168"},{"name":"week","seconds":604800,"priceUsd":0.047,"price":"$0.0470"}]}},"parameters":[{"name":"key","in":"path","required":true,"schema":{"type":"string"},"description":"The key to extend. Must be one your wallet currently holds: a key belonging to another wallet, or one that has already expired, answers 404 exactly like a key that never existed -- this route is not an existence oracle and never silently re-acquires."},{"name":"plan","in":"query","required":true,"schema":{"type":"string","enum":["hour","day","week"]},"description":"Which plan to buy. Its duration is ADDED to whatever the key has left, never substituted for it, so extending early forfeits nothing."},{"name":"holder","in":"query","required":true,"schema":{"type":"string"},"description":"Which of your holders is extending the key. Unsigned -- checked against the key's CURRENT holder (CLAIM_NOT_YOURS if it does not match), the same way DELETE /claim/{key}'s `holder` is: identity is the paying wallet, but a wallet's own fleet can still have more than one process, and only the one that actually holds the key may extend it."}],"responses":{"200":{"description":"Extended. Body is `{ key, plan, expiresAt, previousExpiresAt, payment }` -- the SAME key, its new expiry (unix ms), what the expiry was before, the plan bought, and the settlement receipt. 200 and not 201: nothing new was created, and there is no `held`/`acquired` discriminant here -- unlike POST /claim, an extend either succeeds or is refused, since there is nothing valuable to learn from failing to extend a key you do not hold."},"400":{"description":"An unrecognised `plan` (CLAIM_PLAN_INVALID), or a missing/empty `holder` (CLAIM_READ_REQUEST_INVALID). Free: both are checked before the payment settles, so nothing was charged."},"402":{"description":"Payment required. Body is an x402 challenge, priced from the same table POST /claim is priced from. A bare GET on this route also answers 402 with this same challenge -- never 405."},"403":{"description":"CLAIM_NOT_YOURS -- the wallet holds this key, but `holder` does not name its CURRENT holder. The body never discloses who does; the status is the whole disclosure. Nothing was charged."},"404":{"description":"CLAIM_NOT_FOUND -- nobody holds this key under your wallet, or the hold has already expired. Both answer identically, exactly as release()/inspect() do: an expired-but-not-yet-swept hold must never be resurrected by an extend, since somebody else may have raced in and legitimately won the key already. Nothing was charged."},"405":{"description":"Any method other than POST or GET (e.g. PUT, DELETE) is refused with 405."},"409":{"description":"CLAIM_RENEWAL_WINDOW_EXCEEDED -- extending now would leave the key holding more than 2592000s of unexpired future, the ledger's own ceiling. Unlike the other refusals this one heals with the clock: the identical request succeeds nearer the key's expiry. Nothing was charged."},"429":{"description":"This source IP has exceeded its request rate limit (20 requests per 60s window). Code LISTING_RATE_LIMITED. Free -- nothing was written or charged. A `Retry-After` header (seconds) and `details.retryAfterMs` name exactly when to retry. This route's ceiling is shared with every other /claim route, separate from every other product's."}}}},"/attest":{"post":{"operationId":"createAttestation","summary":"Pay once to have us fetch a URL from OUR vantage point and return a signed, timestamped attestation of what we saw -- hash, byte count, HTTP status and headers, never the body itself. Retrievable afterwards for free at GET /attest/{id}, so the id can be cited to a THIRD PARTY who never paid us and can verify the signature themselves. A fetch that fails to connect, times out, or is refused for safety is still billed and still signed: the observation (\"we tried, from our vantage, and here is exactly what happened\") is what is sold, not a guaranteed 200. Body is read up to 1048576 bytes; a larger response is hashed over that truncated prefix and `body_truncated` is set.","x-payment-info":{"protocols":[{"x402":{}}],"network":"base","payTo":"0xc8Fceb0F3611BE99c41586eA2B83F6A0911d2Ff0","price":{"mode":"fixed","currency":"USD","amount":"0.008000"},"pricing":{"model":"fixed-price-plans","plans":[{"name":"default","priceUsd":0.008,"price":"$0.0080"}]}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["url"],"properties":{"url":{"type":"string","minLength":1,"maxLength":2048,"description":"An absolute http:// or https:// URL, at most 2048 UTF-8 bytes. Fetched from OUR vantage point; the body is never returned to you."}}}}}},"responses":{"201":{"description":"Fetched, signed and stored. Body is `{ attestation_id, url, observed_at, fetch_ok, fetch_code, http_status, body_sha256, body_bytes, body_truncated, content_type, final_url_after_redirects, redirects, detail, signature, signer_address, payment }`. `fetch_ok`/`fetch_code` distinguish a real HTTP response (however unsuccessful) from a fetch that never got one at all (DNS/connect failure, timeout, or an unsafe redirect refused for SSRF safety).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AttestationResult"}}}},"400":{"description":"ATTEST_URL_INVALID -- `url` missing, not a string, too long, or not an absolute http:// or https:// URL. Free -- nothing was fetched or charged."},"402":{"description":"Payment required. Body is an x402 challenge: `{ x402Version, error, accepts: [PaymentRequirements] }`. See x-payment-header for how to pay. A bare GET on this route also answers 402 with this same challenge -- never 405."},"429":{"description":"This source IP has exceeded its request rate limit (20 requests per 60s window). Code LISTING_RATE_LIMITED. Free -- nothing was written or charged. A `Retry-After` header (seconds) and `details.retryAfterMs` name exactly when to retry. This route's ceiling is its own instance, shared with GET /attest/{id} and separate from every other product's."}}}},"/attest/{id}":{"get":{"operationId":"getAttestation","summary":"Free and unauthenticated: read a previously-created attestation back by id. Deliberately NOT wallet-scoped -- the whole point of the product is that a THIRD PARTY who never paid us can fetch it and verify the signature themselves, so unlike GET /storage/{id} this needs no X-Signature proof at all.","security":[],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"The attestation_id returned by POST /attest."}],"responses":{"200":{"description":"Body is the same shape POST /attest's 201 returns, minus `payment`. `signature` covers attestation_id, url, final_url_after_redirects, observed_at, http_status, body_sha256, body_bytes, body_truncated, content_type, fetch_ok, fetch_code and redirects -- everything else in this body (including `detail` and `signer_address` itself) is server-served context, not part of the signed claim. An attestation created before this field set shipped (domain \"agent-exec attestation v2\") was signed under the prior domain \"agent-exec attestation v1\", over only attestation_id/url/final_url_after_redirects/observed_at/http_status/body_sha256/body_bytes/content_type -- it is never re-signed, so a verifier that fails against v2 should retry against v1 before concluding an attestation is invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AttestationResult"}}}},"404":{"description":"ATTEST_NOT_FOUND -- no attestation with this id was ever written. Free."},"429":{"description":"This source IP has exceeded its request rate limit (20 requests per 60s window). Code LISTING_RATE_LIMITED. Free -- nothing was written or charged. A `Retry-After` header (seconds) and `details.retryAfterMs` name exactly when to retry. This route's ceiling is shared with POST /attest and separate from every other product's."}}}},"/handoff":{"post":{"operationId":"createHandoff","summary":"Pay once 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. Returns an id. The successor fetches it back for free at GET /handoff/{id} with NO identity, header or payment of any kind, so the id itself is the capability and neither side needs an account with us. Retained 3 days, then swept. The 'done' section is structurally forbidden from holding an unverified claim: a briefing cannot assert something is finished on intent alone.","x-payment-info":{"protocols":[{"x402":{}}],"network":"base","payTo":"0xc8Fceb0F3611BE99c41586eA2B83F6A0911d2Ff0","price":{"mode":"fixed","currency":"USD","amount":"0.003300"},"pricing":{"model":"fixed-price-plans","plans":[{"name":"default","seconds":259200,"priceUsd":0.0033,"price":"$0.0033"}]}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HandoffBriefing"}}}},"responses":{"201":{"description":"Stored. Body is `{ handoff_id, expires_at, payment }`. Give `handoff_id` to the successor; it needs nothing else."},"400":{"description":"The briefing was malformed. Typed `error.code` is one of HANDOFF_BODY_NOT_OBJECT, HANDOFF_SECTION_INVALID, HANDOFF_SECTION_TOO_LARGE, HANDOFF_CLAIM_INVALID, HANDOFF_CLAIM_TEXT_INVALID, HANDOFF_CLAIM_CONFIDENCE_INVALID, HANDOFF_CLAIM_EVIDENCE_INVALID, HANDOFF_DONE_REQUIRES_MEASURED or HANDOFF_PICK_UP_FIRST_INVALID, with `details` naming the offending section or field. Free -- input is validated before the paywall, so nothing was charged."},"402":{"description":"Payment required. Body is an x402 challenge: `{ x402Version, error, accepts: [PaymentRequirements] }`. See x-payment-header for how to pay. A bare GET on this route also answers 402 with this same challenge -- never 405."},"403":{"description":"HANDOFF_WALLET_CAP_EXCEEDED -- this wallet already holds the maximum number of unexpired briefings. Checked before settlement, so nothing was charged. Waiting does not fix it in the short term; the remedy is letting existing briefings expire."},"429":{"description":"This source IP has exceeded its request rate limit (20 requests per 60s window). Code LISTING_RATE_LIMITED. Free -- nothing was written or charged. A `Retry-After` header (seconds) and `details.retryAfterMs` name exactly when to retry. This route's ceiling is its own instance, shared with GET /handoff/{id} and separate from every other product's."}}}},"/handoff/{id}":{"get":{"operationId":"getHandoff","summary":"Free and unauthenticated: read a briefing back by id. Deliberately NOT wallet-scoped and deliberately requires no signature -- the successor is by construction a DIFFERENT process from the depositor and may hold no wallet at all, so demanding proof of the payer would make the product undeliverable. Possession of the id is the entire authorisation; treat it as a secret and pass it over a channel you trust.","security":[],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"The handoff_id returned by POST /handoff. Must be a canonical 8-4-4-4-12 UUID; a non-UUID id is refused at the edge with HANDOFF_ID_MALFORMED."}],"responses":{"200":{"description":"Body is the briefing as deposited: `{ done, notDone, inFlight, pickUpFirst }`."},"404":{"description":"HANDOFF_NOT_FOUND -- no handoff with this id was ever written, or it was written, expired and has since been swept. Free. A non-UUID id is refused earlier, at the edge, with a typed HANDOFF_ID_MALFORMED. An id that has expired but has NOT yet been swept answers 410, not 404 -- see below."},"410":{"description":"HANDOFF_EXPIRED -- this handoff existed and its retention window has passed, but the sweeper has not yet removed the row, so we can still tell you it was real rather than pretending it never existed. Free. Distinct from 404 on purpose, and worth branching on separately: 410 means the id was genuine and you missed the window (the deposit was real, so re-requesting it is pointless -- ask the depositor to deposit again), while 404 means we have no record either way. Once the sweeper runs, the same id starts answering 404."},"429":{"description":"This source IP has exceeded its request rate limit (20 requests per 60s window). Code LISTING_RATE_LIMITED. Free -- nothing was written or charged. A `Retry-After` header (seconds) and `details.retryAfterMs` name exactly when to retry. This route's ceiling is shared with POST /handoff and separate from every other product's."}}}},"/deadman":{"post":{"operationId":"registerDeadman","summary":"Pay to register an intent you are about to act on irreversibly, with a lease and a verification probe: a GET request plus a predicate (expectedBodyContains and/or expectedStatus). Confirm before the lease expires at POST /deadman/{id}/confirm, free -- otherwise, once the lease lapses, we run your probe on your behalf and publish the verdict: confirmed-by-probe, refuted-by-probe, or unresolved (a first-class, honest outcome when the probe itself could not complete -- never guessed as refuted). The raw probe response is published alongside the verdict at GET /deadman/{id}, free, so you can re-run the probe yourself and check our answer rather than simply believing it. Lease: 1-604800 seconds.","x-payment-info":{"protocols":[{"x402":{}}],"network":"base","payTo":"0xc8Fceb0F3611BE99c41586eA2B83F6A0911d2Ff0","price":{"mode":"fixed","currency":"USD","amount":"0.003700"},"pricing":{"model":"fixed-price-plans","plans":[{"name":"default","priceUsd":0.0037,"price":"$0.0037","minLeaseSeconds":1,"maxLeaseSeconds":604800}]}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeadmanRegisterRequest"}}}},"responses":{"201":{"description":"Registered. Body is `{ id, status: \"pending\", description, probe, createdAt, leaseExpiresAt, payment }`. Keep `id` -- it is both how you confirm and how anyone reads the eventual verdict back."},"400":{"description":"The registration was malformed. Typed `error.code` is one of DEADMAN_BODY_NOT_OBJECT, DEADMAN_DESCRIPTION_INVALID, DEADMAN_LEASE_SECONDS_INVALID, DEADMAN_PROBE_INVALID, DEADMAN_PROBE_URL_INVALID, DEADMAN_PROBE_PREDICATE_MISSING or DEADMAN_PROBE_PREDICATE_INVALID, with `details` naming the offending field. Free -- input is validated before the paywall and before any network access, so nothing was charged."},"402":{"description":"Payment required. Body is an x402 challenge: `{ x402Version, error, accepts: [PaymentRequirements] }`. See x-payment-header for how to pay. A bare GET on this route also answers 402 with this same challenge -- never 405."},"403":{"description":"DEADMAN_WALLET_CAP_EXCEEDED -- this wallet already holds the maximum number of pending entries."},"429":{"description":"This source IP has exceeded its request rate limit (20 requests per 60s window). Code LISTING_RATE_LIMITED. Free -- nothing was written or charged. A `Retry-After` header (seconds) and `details.retryAfterMs` name exactly when to retry. This route's ceiling is its own instance, shared with POST /deadman/{id}/confirm, and separate from every other product's -- including GET /deadman/{id}, which has its OWN, more generous budget so reading your entry back never competes with registering it."}}}},"/deadman/{id}/confirm":{"post":{"operationId":"confirmDeadman","summary":"Free: the live caller closes the loop before the lease expires. Deliberately NOT wallet-scoped -- possession of the id is the entire authorisation, the same reasoning as GET /handoff/{id}, because the confirming call has no wallet of its own to prove against. Refused (never a silent no-op) once the entry has left pending: confirming an already-resolved entry could otherwise overwrite a probe's verdict with a claim from a caller who may not even be the process that registered it.","security":[],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"The id returned by POST /deadman."}],"responses":{"200":{"description":"Confirmed. Body is the full entry with `status: \"confirmed\"`."},"404":{"description":"DEADMAN_NOT_FOUND -- no deadman entry with this id was ever written. Free."},"409":{"description":"DEADMAN_ALREADY_RESOLVED -- the entry already left pending (already confirmed, or its probe already ran), or DEADMAN_LEASE_EXPIRED -- the lease has lapsed and the probe owns this entry now. Free."},"429":{"description":"This source IP has exceeded its request rate limit (20 requests per 60s window). Code LISTING_RATE_LIMITED. Free -- nothing was written or charged. A `Retry-After` header (seconds) and `details.retryAfterMs` name exactly when to retry. This route's ceiling is shared with POST /deadman, and separate from every other product's -- including GET /deadman/{id}, which has its OWN, more generous budget."}}}},"/deadman/{id}":{"get":{"operationId":"getDeadman","summary":"Free and unauthenticated: read an entry back by id, including its verdict once resolved. If the lease has lapsed and nobody has resolved it yet, the probe runs right here, synchronously, before responding -- bounded because POST /deadman is paid, so the number of entries that can ever reach this path is bounded by what was paid for, and each entry can trigger at most one live probe ever.","security":[],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"The id returned by POST /deadman."}],"responses":{"200":{"description":"Body is `{ id, status, description, probe, createdAt, leaseExpiresAt, confirmedAt, resolvedAt, probeResult }`. `status` is one of pending, confirmed, confirmed-by-probe, refuted-by-probe, unresolved. `probeResult` is null until the probe has run; once it has, it carries the raw response (`ok`, `code`, `httpStatus`, `bodyText`, `bodyBytes`, `bodyTruncated`, `contentType`, `finalUrl`, `redirects`, `detail`) so you can verify our verdict yourself."},"404":{"description":"DEADMAN_NOT_FOUND -- no deadman entry with this id was ever written. Free."},"429":{"description":"This source IP has exceeded its request rate limit (120 requests per 60s window). Code LISTING_RATE_LIMITED. Free -- nothing was written or charged. A `Retry-After` header (seconds) and `details.retryAfterMs` name exactly when to retry. This route's ceiling is its OWN instance, sized for polling until your entry resolves, and separate from POST /deadman and POST /deadman/{id}/confirm's shared, more conservative budget."}}}},"/drift":{"post":{"operationId":"driftFamily","summary":"Same product as POST /drift/{venue}, with the venue carried as a JSON body field instead of a path segment, so this literal advertised URL is itself payable. Pay to re-fetch the venue's recorded URL and diff it against our archived baseline -- see POST /drift/{venue} for the full description of the comparison.","x-payment-info":{"protocols":[{"x402":{}}],"network":"base","payTo":"0xc8Fceb0F3611BE99c41586eA2B83F6A0911d2Ff0","price":{"mode":"fixed","currency":"USD","amount":"0.006100"},"pricing":{"model":"fixed-price-plans","plans":[{"name":"default","priceUsd":0.0061,"price":"$0.0061"}]}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["venue"],"properties":{"venue":{"type":"string","pattern":"^[a-zA-Z0-9_-]+$","enum":["agentry","clearedindex","percall","verantis","x402-list"],"description":"One of the endpoints we already snapshot."}}}}}},"responses":{"200":{"description":"Body is `{ venue, t1, t2, events, payment }`, identical shape to POST /drift/{venue}'s 200."},"400":{"description":"DRIFT_VENUE_INVALID -- the body has no `venue` field, or it does not match `^[a-zA-Z0-9_-]+$`, or the body is not valid JSON. `details.expected` names the accepted shape. Free, never charged."},"402":{"description":"Payment required. Body is an x402 challenge: `{ x402Version, error, accepts: [PaymentRequirements] }`. See x-payment-header for how to pay."},"404":{"description":"DRIFT_NO_BASELINE -- no snapshot has ever been recorded for this venue. Free -- checked before the paywall."},"429":{"description":"This source IP has exceeded its request rate limit (20 requests per 60s window). Code LISTING_RATE_LIMITED. Free -- nothing was written or charged. A `Retry-After` header (seconds) and `details.retryAfterMs` name exactly when to retry. This route's ceiling is shared with GET/POST /drift/{venue} and separate from every other product's."}}},"get":{"operationId":"driftFamilyGet","summary":"Free, unauthenticated discovery: names every venue this deployment can currently serve, so a cold caller never has to guess (or send a malformed request) to learn what it can buy. `venue` in the POST body/path schemas is a closed set -- this is where it is published.","security":[],"responses":{"200":{"description":"Body is `{ path, venues, plans, payment, note }` -- `venues` is the same list published as `enum` in the POST /drift and GET/POST /drift/{venue} `venue` schemas, and as `expected.venues` on every /drift refusal (400/404). Never behind the paywall."},"429":{"description":"This source IP has exceeded its request rate limit (20 requests per 60s window). Code LISTING_RATE_LIMITED. Free -- nothing was written or charged. A `Retry-After` header (seconds) and `details.retryAfterMs` name exactly when to retry. This route's ceiling is its own instance, shared with GET/POST /drift/{venue} and separate from every other product's."}}}},"/drift/{venue}":{"get":{"operationId":"getDriftBaseline","summary":"Free and unauthenticated: report whether we hold a baseline snapshot for this venue at all, and a hash/byte-count of it, without spending the live re-fetch a POST costs. Lets a prospective buyer decide whether paying is worthwhile before paying.","security":[],"parameters":[{"name":"venue","in":"path","required":true,"schema":{"type":"string","pattern":"^[a-zA-Z0-9_-]+$","enum":["agentry","clearedindex","percall","verantis","x402-list"]},"description":"One of the endpoints we already snapshot (an operator-controlled, small set -- see #1429). An unrecognised venue answers 404 DRIFT_NO_BASELINE, never a guess."}],"responses":{"200":{"description":"Body is `{ venue, baseline: { fetched_at, url, http_status, content_type, body_bytes, body_sha256, has_fingerprint } }`."},"404":{"description":"DRIFT_NO_BASELINE -- no snapshot has ever been recorded for this venue. Free, and the same code POST /drift/{venue} answers before the paywall for the identical reason."},"429":{"description":"This source IP has exceeded its request rate limit (20 requests per 60s window). Code LISTING_RATE_LIMITED. Free -- nothing was written or charged. A `Retry-After` header (seconds) and `details.retryAfterMs` name exactly when to retry. This route's ceiling is its own instance, shared with POST /drift/{venue} and separate from every other product's."}}},"post":{"operationId":"createDrift","summary":"Pay to re-fetch the venue's recorded URL from OUR vantage point and diff it against our archived baseline: field_added, field_removed, weights_rebased, enum_extended, nulled_fleet_wide. Returns BOTH raw bodies -- the archived baseline (T1) and the fresh read (T2) -- byte-for-byte, so you can verify the diff yourself rather than trusting ours; same \"self-proving, no belief required\" shape as /attest. The live fetch reuses /attest's SSRF-safe egress (DNS-rebinding-safe redirect handling), reads up to 1048576 bytes and truncates a larger body. A fetch that fails to connect, times out, or is refused for safety is still billed: the observation (\"we tried, from our vantage, and here is exactly what happened\") is what is sold, not a guaranteed 200 -- see `t2.fetch_ok`/`t2.fetch_code`.","x-payment-info":{"protocols":[{"x402":{}}],"network":"base","payTo":"0xc8Fceb0F3611BE99c41586eA2B83F6A0911d2Ff0","price":{"mode":"fixed","currency":"USD","amount":"0.006100"},"pricing":{"model":"fixed-price-plans","plans":[{"name":"default","priceUsd":0.0061,"price":"$0.0061"}]}},"parameters":[{"name":"venue","in":"path","required":true,"schema":{"type":"string","pattern":"^[a-zA-Z0-9_-]+$","enum":["agentry","clearedindex","percall","verantis","x402-list"]},"description":"One of the endpoints we already snapshot. Checked BEFORE the paywall: DRIFT_VENUE_INVALID (400) for a malformed name, DRIFT_NO_BASELINE (404) for a well-formed name we have never snapshotted -- neither is ever charged."}],"responses":{"200":{"description":"Body is `{ venue, t1, t2, events, payment }`. `t1`/`t2` are each `{ fetched_at, url, http_status, content_type, body_bytes, body_sha256, body }` -- `t2` additionally carries `fetch_ok`, `fetch_code`, `detail`, `body_truncated`. `events` is our typed diff (null if `t2`'s body did not parse as JSON) -- verify it yourself against `t1.body`/`t2.body` rather than trusting it."},"400":{"description":"DRIFT_VENUE_INVALID -- `venue` does not match `^[a-zA-Z0-9_-]+$`, or DRIFT_REQUEST_INVALID -- a valid request body is always empty (the URL to re-fetch comes from our own corpus, never from you) and this one exceeded the 512-byte cap. Free either way -- nothing was fetched or charged."},"402":{"description":"Payment required. Body is an x402 challenge: `{ x402Version, error, accepts: [PaymentRequirements] }`. See x-payment-header for how to pay. A bare GET on this route never sees this -- GET is free and answers the baseline check above instead."},"404":{"description":"DRIFT_NO_BASELINE -- no snapshot has ever been recorded for this venue. Free -- checked before the paywall."},"429":{"description":"This source IP has exceeded its request rate limit (20 requests per 60s window). Code LISTING_RATE_LIMITED. Free -- nothing was written or charged. A `Retry-After` header (seconds) and `details.retryAfterMs` name exactly when to retry. This route's ceiling is its own instance, shared with GET /drift/{venue} and separate from every other product's."}}}}},"components":{"schemas":{"Limits":{"type":"object","description":"Per-request resource budget. Every field is optional; an omitted field takes its documented default. A field outside [minimum, maximum] is rejected outright, never clamped.","properties":{"timeoutMs":{"type":"integer","minimum":100,"maximum":60000,"default":5000},"maxMemoryBytes":{"type":"integer","minimum":1207959552,"maximum":4294967296,"default":2147483648},"maxOutputBytes":{"type":"integer","minimum":1024,"maximum":8388608,"default":1048576},"workDirBytes":{"type":"integer","minimum":1048576,"maximum":268435456,"default":67108864}}},"ListingCore":{"type":"object","required":["url","name","description","tags","tier","lastSeenAlive","paidUntil","dead","lastProbeCode","operator","consecutiveFailures","deadSince","evictsAt","remedy","provenance","claimable","claim","id"],"properties":{"url":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"tags":{"type":"array","items":{"type":"string"}},"tier":{"type":"string","enum":["free","paid"],"description":"This LISTING's placement tier in the directory -- \"paid\" means placement was bought via POST /listings/promote, ranking it above free listings. It says nothing about whether the advertised SERVICE itself is free to use: this deployment's own /execute, /storage and /listings/promote all charge over x402 and still read tier: \"free\" here, because placement was never promoted, not because they are free to call."},"lastSeenAlive":{"type":["integer","null"],"description":"Unix ms of the last successful liveness probe, or null before one has ever run."},"paidUntil":{"type":["integer","null"],"description":"Unix ms the paid placement expires, or null while tier is free."},"dead":{"type":"boolean","description":"3 consecutive failed probes with none since. Ranked last regardless of tier. Never deleted; one successful probe clears it."},"lastProbeCode":{"type":["string","null"],"description":"The outcome code of the last probe attempt, or null if never probed -- e.g. PROBE_OK, PROBE_TIMEOUT, PROBE_STATUS_DEAD. How an owner with no account and no support channel is told why their listing ranks last."},"operator":{"type":"boolean","description":"True when this listing is owned by this deployment's own receive wallet. Disclosed rather than hidden."},"consecutiveFailures":{"type":"integer","description":"Probes since the last success. Reset to 0 by any success."},"deadSince":{"type":["integer","null"],"description":"Unix ms this listing became continuously dead, or null while alive or never probed."},"evictsAt":{"type":["integer","null"],"description":"Unix ms this listing will be PERMANENTLY DELETED if nothing changes, or null while it is not dead. This is not owner-private data: an owner with no account and no contact field has no other channel to learn a deletion is coming. Re-POST the same listing id with a working url before this time to clear dead/consecutiveFailures and cancel it -- see `remedy`."},"remedy":{"type":["string","null"],"description":"One-line, machine-readable fix for a dead row (re-POST the same id with a working url), or null while the row is not dead."},"provenance":{"type":"string","enum":["owner","operator","curated"],"description":"Who this row is FROM. \"owner\" -- signed by the party that actually runs the service, the default and the majority. \"operator\" -- this deployment's own products. \"curated\" -- listed BY US, ABOUT SOMEONE ELSE, who never asked to be listed and never claimed the row; see `claimable` and `claim`."},"claimable":{"type":"boolean","description":"True only when `provenance` is \"curated\" AND at least one claim credential is available: EITHER a claim address this service recorded at listing time (a signature from that wallet is the proof), OR -- for a curated row with no such address -- the well-known-file fallback (serve the expected challenge text at `/.well-known/agent-exec-listing-claim.txt` on this listing's own origin instead of signing anything). When true, the real operator may take this row over, or have it removed, via POST /listings/claim -- see `claim` for which of the two proofs this specific row needs."},"claim":{"type":["string","null"],"description":"How to claim or remove this row, present only when `claimable` is true. Null otherwise."},"id":{"type":"string","description":"This row's id, present on every row. It is the one value `claim` (above) asks a curated row's real operator to POST back to `POST /listings/claim`, and it is also the only input `GET /listings/{id}/uptime` takes -- so a buying agent needs it too, to look up the uptime of a row it is deciding whether to call."},"ownerVerifiedAlternate":{"type":"object","description":"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.","required":["url","verifiedVia","signedByOwner"],"properties":{"url":{"type":"string","description":"A candidate replacement url, never substituted for `url` above."},"verifiedVia":{"type":"string"},"signedByOwner":{"type":"boolean"}}}}},"Listing":{"allOf":[{"$ref":"#/components/schemas/ListingCore"},{"type":"object","required":["promote"],"properties":{"promote":{"type":"object","required":["url","method","plans"],"description":"Always present. Where and how to buy paid placement for THIS listing, and the live price table -- the same numbers the real POST /listings/promote 402 challenge charges, so this can never advertise a number the live charge disagrees with. See POST /listings/promote.","properties":{"url":{"type":"string","description":"Absolute URL of POST /listings/promote on this deployment."},"method":{"type":"string","enum":["POST"]},"plans":{"type":"array","items":{"type":"object","required":["duration","name","days","priceUsd","price"],"properties":{"duration":{"type":"string","enum":["day","week","month"]},"name":{"type":"string","description":"Duplicates `duration` -- see `promotionCatalogue()`."},"days":{"type":"number"},"priceUsd":{"type":"number"},"price":{"type":"string","description":"The same amount as `priceUsd`, formatted as an x402 price string (e.g. \"$1.0000\")."}}}}}}}}]},"PromotionResult":{"type":"object","required":["listing","promotion","payment"],"properties":{"listing":{"$ref":"#/components/schemas/Listing","description":"This purchase's listing, in the SAME shape GET/POST/PUT /listings return it -- including the `promote` pointer, so a caller that just paid for placement also gets the live price table for renewing it before it lapses (see POST /listings/promote and `DirectoryStore.promote()`'s extend-not-replace semantics)."},"promotion":{"type":"object","properties":{"duration":{"type":"string","enum":["day","week","month"]},"durationMs":{"type":"integer"},"paidUntil":{"type":"integer","description":"Unix ms the paid placement now expires."},"priceUsd":{"type":"number"}}},"payment":{"type":"object","properties":{"payer":{"type":"string","description":"The paying wallet's address."},"amount":{"type":"string","description":"Atomic units of the settlement asset that were charged."},"asset":{"type":"string"},"network":{"type":"string"},"transaction":{"type":"string","description":"On-chain settlement transaction hash."}}}}},"ExecuteResult":{"type":"object","properties":{"outcome":{"type":"string","enum":["completed","timeout","error"]},"stdout":{"type":"string"},"stderr":{"type":"string"},"exitCode":{"type":["integer","null"]},"durationMs":{"type":"number"},"truncated":{"type":"object","properties":{"stdout":{"type":"boolean"},"stderr":{"type":"boolean"}}},"error":{"type":["object","null"],"properties":{"code":{"type":"string","enum":["SANDBOX_INVALID_REQUEST","RUNTIME_RESERVED_FILE_PATH","REQUEST_MALFORMED","PAYMENT_MALFORMED","RUNTIME_UNSUPPORTED","RUNTIME_LIMIT_OUT_OF_RANGE","SANDBOX_TIMEOUT","EXECUTION_MEMORY_LIMIT_EXCEEDED","EXECUTION_OUTPUT_LIMIT_EXCEEDED","SERVICE_BUSY","PAYER_RATE_LIMITED","PAYER_CONCURRENCY_LIMITED","EXECUTE_IP_RATE_LIMITED","EXECUTION_RUNTIME_ERROR","EXECUTION_RUNTIME_ERROR_POSSIBLE_SANDBOX_DENIAL","PAYMENT_INVALID","PAYMENT_AMOUNT_INSUFFICIENT","PAYMENT_REQUIREMENTS_MISMATCH","PAYMENT_REPLAYED","PAYMENT_SETTLEMENT_FAILED","PAYMENT_SETTLEMENT_INDETERMINATE","SANDBOX_UNAVAILABLE","SANDBOX_SETUP_FAILED","SERVICE_INTERNAL_FAILURE","PAYMENT_FACILITATOR_UNAVAILABLE"]},"category":{"type":"string"},"billable":{"type":"boolean"},"message":{"type":"string"}}}}},"StoragePutResult":{"type":"object","required":["id","expiresAt","plan"],"properties":{"id":{"type":"string","description":"Pass as GET /storage/{id} to read this object back. A UUID for a paid POST /storage; `free:<your wallet address>` for the one object POST /storage/free maintains -- stable across every free renewal for that wallet."},"expiresAt":{"type":"integer","description":"Unix ms this object stops being retained."},"plan":{"type":"string","enum":["small","medium","large","free"],"description":"One of the paid plans (small/medium/large) from POST /storage, or \"free\" from POST /storage/free."},"label":{"type":["string","null"],"description":"The label you passed as ?label=, stored verbatim, or null if you passed none. Echoed back so you can confirm what was stored rather than assuming it."},"payment":{"type":"object","description":"Only present for a paid POST /storage write. Absent for POST /storage/free -- nothing was charged.","properties":{"payer":{"type":"string","description":"The paying wallet's address."},"amount":{"type":"string","description":"Atomic units of the settlement asset that were charged."},"asset":{"type":"string"},"network":{"type":"string"},"transaction":{"type":"string","description":"On-chain settlement transaction hash."}}}}},"StorageDeleteResult":{"type":"object","required":["id","deleted","freedBytes"],"properties":{"id":{"type":"string","format":"uuid","description":"The object that was deleted."},"deleted":{"type":"boolean","description":"Always true -- a refusal is a 4xx, never a 200 with `deleted: false`."},"freedBytes":{"type":"integer","description":"How many bytes this deletion freed against the wallet's cap. Not a refund or a credit -- see this operation's summary."}}},"StorageUsageResult":{"type":"object","required":["objectCount","totalBytes"],"properties":{"objectCount":{"type":"integer","description":"How many live objects this wallet currently has stored."},"totalBytes":{"type":"integer","description":"Sum of those objects' sizes, in bytes."}}},"StorageListResult":{"type":"object","required":["objects","objectCount","truncated","nextCursor"],"properties":{"objects":{"type":"array","description":"This wallet's currently-unexpired objects, newest first, at most 1000 of them. Metadata only -- the bytes come from GET /storage/{id}.","items":{"type":"object","required":["id","size","createdAt","expiresAt"],"properties":{"id":{"type":"string","description":"The object id to pass to GET /storage/{id}."},"size":{"type":"integer","description":"Stored size in bytes."},"createdAt":{"type":"integer","description":"Unix milliseconds the object was stored."},"expiresAt":{"type":"integer","description":"Unix milliseconds the object is deleted at."},"label":{"type":["string","null"],"description":"The name you gave this object as POST /storage's optional ?label=, or null if you gave none (including for anything stored before labels existed). Not unique -- two objects may carry the same label -- and not addressable: there is no lookup-by-label, so find the row you want here and then GET /storage/{id}."}}}},"objectCount":{"type":"integer","description":"This wallet's true unexpired object count across ALL pages, not just this one -- so it does not shrink as you page. It exceeds `objects.length` exactly when there is more than one page."},"truncated":{"type":"boolean","description":"True when more rows follow this page -- either because the wallet holds more than 1000 live objects, or because you are mid-way through paging. Always equal to `nextCursor !== null`."},"nextCursor":{"type":["string","null"],"description":"Opaque token to pass back verbatim as the next request's `after` query parameter, or null when this page is the last. Do not parse or construct one. Paging with this is the only way to reach an object past the newest page, since there is no lookup-by-label."}}},"ClaimResult":{"type":"object","required":["outcome","plan","claim"],"properties":{"outcome":{"type":"string","enum":["acquired","held"],"description":"\"acquired\": you won the key (201). \"held\": somebody already holds it (200) -- NOT an error, see `mine`."},"plan":{"type":"string","enum":["hour","day","week","free"],"description":"One of the paid plans (hour/day/week) from POST /claim, or \"free\" from POST /claim/free."},"claim":{"type":"object","required":["key","holder","acquiredAt","expiresAt"],"description":"The current hold on this key -- yours if `outcome` is \"acquired\", or the existing holder's if `outcome` is \"held\". Never carries a wallet: the caller already knows its own.","properties":{"key":{"type":"string"},"holder":{"type":"string"},"acquiredAt":{"type":"integer","description":"Unix ms the hold was granted, from the server clock at the commit that granted it."},"expiresAt":{"type":"integer","description":"Unix ms the hold is free again."}}},"mine":{"type":"boolean","description":"Only present when `outcome` is \"held\": true when the existing holder is YOU -- a crash-resume or duplicated retry, the exact signal this product is sold to give."},"payment":{"type":"object","description":"Only present when `outcome` is \"acquired\".","properties":{"payer":{"type":"string","description":"The paying wallet's address."},"amount":{"type":"string","description":"Atomic units of the settlement asset that were charged."},"asset":{"type":"string"},"network":{"type":"string"},"transaction":{"type":"string","description":"On-chain settlement transaction hash."}}}}},"HandoffClaim":{"type":"object","required":["text","confidence","evidence"],"properties":{"text":{"type":"string","minLength":1,"maxLength":1000,"description":"The claim itself, one statement."},"confidence":{"type":"string","enum":["measured","unverified"],"description":"'measured' means you verified it and `evidence` says how; 'unverified' means you did not, and `evidence` MUST then be null."},"evidence":{"type":["string","null"],"maxLength":1000,"description":"How it was verified -- a command, a probe result, a file read; the evidence itself, not a restatement of `text`. Required and non-null when confidence is 'measured'; must be null when 'unverified'."}}},"HandoffBriefing":{"type":"object","required":["done","notDone","inFlight","pickUpFirst"],"properties":{"done":{"type":"array","maxItems":100,"items":{"$ref":"#/components/schemas/HandoffClaim"},"description":"What is DONE. EVERY entry must have confidence 'measured' with non-null evidence -- a briefing cannot claim something is finished on unverified intent. Violating this is a free 400 (HANDOFF_DONE_REQUIRES_MEASURED), not a silent acceptance."},"notDone":{"type":"array","maxItems":100,"items":{"$ref":"#/components/schemas/HandoffClaim"},"description":"What is NOT done. May be measured or unverified."},"inFlight":{"type":"array","maxItems":100,"items":{"$ref":"#/components/schemas/HandoffClaim"},"description":"What was IN FLIGHT at the cut -- neither finished nor abandoned. May be measured or unverified."},"pickUpFirst":{"type":"string","minLength":1,"maxLength":2000,"description":"What the successor should do FIRST. Mandatory and non-empty: a briefing with no next action is not a handoff."}}},"DeadmanProbeSpec":{"type":"object","required":["url"],"properties":{"url":{"type":"string","format":"uri","maxLength":2048,"description":"Absolute http:// or https:// URL, GETted from OUR vantage point once the lease expires."},"expectedBodyContains":{"type":"string","minLength":1,"maxLength":500,"description":"The response body must contain this substring for the verdict to be confirmed-by-probe."},"expectedStatus":{"type":"integer","minimum":100,"maximum":599,"description":"The response status must equal this for the verdict to be confirmed-by-probe."}},"description":"At least one of expectedBodyContains / expectedStatus is required -- a probe with no predicate can never resolve to confirmed or refuted."},"DeadmanRegisterRequest":{"type":"object","required":["description","leaseSeconds","probe"],"properties":{"description":{"type":"string","minLength":1,"maxLength":500,"description":"What you are about to do."},"leaseSeconds":{"type":"number","minimum":1,"maximum":604800,"description":"How long before an unconfirmed entry's probe runs."},"probe":{"$ref":"#/components/schemas/DeadmanProbeSpec"}}},"AttestationResult":{"type":"object","required":["attestation_id","url","observed_at","fetch_ok","fetch_code","http_status","body_sha256","body_bytes","body_truncated","content_type","final_url_after_redirects","redirects","detail","signature","signer_address"],"properties":{"attestation_id":{"type":"string","format":"uuid","description":"Pass as GET /attest/{id} to read this back, or hand it to a third party to verify."},"url":{"type":"string","description":"The URL you asked us to fetch, exactly as sent."},"observed_at":{"type":"string","format":"date-time","description":"When the fetch completed, ISO 8601."},"fetch_ok":{"type":"boolean","description":"Whether the fetch reached a real HTTP response, however unsuccessful. false means DNS/connect failure, timeout, or a redirect refused for SSRF safety -- see fetch_code."},"fetch_code":{"type":"string","enum":["ATTEST_FETCH_OK","ATTEST_FETCH_URL_REFUSED","ATTEST_FETCH_REDIRECT_UNSAFE","ATTEST_FETCH_TOO_MANY_REDIRECTS","ATTEST_FETCH_TIMEOUT","ATTEST_FETCH_CONNECT_FAILED"]},"http_status":{"type":["integer","null"],"description":"The HTTP status the final hop answered with, or null if fetch_ok is false."},"body_sha256":{"type":["string","null"],"description":"sha256 of the fetched body, hex-encoded, or null if fetch_ok is false. The body itself is never returned or stored beyond this hash."},"body_bytes":{"type":"integer","description":"Bytes actually read off the wire, capped at 1048576 per fetch -- see body_truncated."},"body_truncated":{"type":"boolean","description":"True if the body was larger than the per-fetch cap and body_sha256/body_bytes cover only the truncated prefix."},"content_type":{"type":["string","null"],"description":"The Content-Type header on the final response, or null if fetch_ok is false."},"final_url_after_redirects":{"type":["string","null"],"description":"The URL actually fetched after following redirects, or null if the fetch never reached one."},"redirects":{"type":"integer","description":"How many redirects were followed before the final response (or the refusal)."},"detail":{"type":["string","null"],"description":"Free-text detail on a non-OK fetch_code (e.g. the timeout budget, the unsafe redirect target). Null when fetch_ok is true."},"signature":{"type":"string","description":"An EIP-191 personal-sign signature over attestation_id, url, final_url_after_redirects, observed_at, http_status, body_sha256, body_bytes, body_truncated, content_type, fetch_ok, fetch_code and redirects (domain \"agent-exec attestation v2\"), from signer_address. `detail` and `signer_address` are NOT part of the signed message. Verify with viem's verifyMessage({address: signer_address, message, signature}) -- see GET /attest/{id}'s description for the exact message and for handling an attestation signed under the prior v1 domain."},"signer_address":{"type":"string","description":"The address that signed this attestation. Also published at GET /health as attestSignerAddress, if you lost track of which address to verify against."},"payment":{"type":"object","description":"Only present on POST /attest's 201, never on a GET /attest/{id} read.","properties":{"payer":{"type":"string","description":"The paying wallet's address."},"amount":{"type":"string","description":"Atomic units of the settlement asset that were charged."},"asset":{"type":"string"},"network":{"type":"string"},"transaction":{"type":"string","description":"On-chain settlement transaction hash."}}}}}}},"x-runtimes":[{"id":"python3","pinnedVersion":"3.13","entryFileName":"main.py"},{"id":"node","pinnedVersion":"20.18.1","entryFileName":"main.js"}],"x-error-codes":[{"code":"SANDBOX_INVALID_REQUEST","category":"invalid_request","billable":false,"message":"The request was malformed. Nothing was run."},{"code":"RUNTIME_RESERVED_FILE_PATH","category":"invalid_request","billable":false,"message":"The request was malformed. Nothing was run."},{"code":"REQUEST_MALFORMED","category":"invalid_request","billable":false,"message":"The request was malformed. Nothing was run."},{"code":"PAYMENT_MALFORMED","category":"invalid_request","billable":false,"message":"The request was malformed. Nothing was run."},{"code":"RUNTIME_UNSUPPORTED","category":"unsupported_runtime","billable":false,"message":"The requested runtime is not on the supported allowlist. Nothing was run."},{"code":"RUNTIME_LIMIT_OUT_OF_RANGE","category":"limit_out_of_range","billable":false,"message":"A requested limit fell outside the permitted range. Nothing was run."},{"code":"SANDBOX_TIMEOUT","category":"timeout","billable":true,"message":"The run exceeded its wall-clock budget and was killed."},{"code":"EXECUTION_MEMORY_LIMIT_EXCEEDED","category":"memory_exceeded","billable":true,"message":"The run was killed after exceeding its memory limit."},{"code":"EXECUTION_OUTPUT_LIMIT_EXCEEDED","category":"output_limit_exceeded","billable":true,"message":"The run completed but stdout or stderr was truncated at the request's output limit."},{"code":"SERVICE_BUSY","category":"service_busy","billable":false,"message":"The service is at capacity. Nothing was run; retry the request."},{"code":"PAYER_RATE_LIMITED","category":"service_busy","billable":false,"message":"The service is at capacity. Nothing was run; retry the request."},{"code":"PAYER_CONCURRENCY_LIMITED","category":"service_busy","billable":false,"message":"The service is at capacity. Nothing was run; retry the request."},{"code":"EXECUTE_IP_RATE_LIMITED","category":"service_busy","billable":false,"message":"The service is at capacity. Nothing was run; retry the request."},{"code":"EXECUTION_RUNTIME_ERROR","category":"runtime_error","billable":true,"message":"The payload ran to completion and exited non-zero. This is the caller's program failing, not the service."},{"code":"EXECUTION_RUNTIME_ERROR_POSSIBLE_SANDBOX_DENIAL","category":"runtime_error","billable":true,"message":"The payload exited non-zero, and stderr looks like an OS-level permission or not-implemented error (the shape this sandbox's own syscall filter produces), not necessarily a bug in the payload. This is a HEURISTIC, not a certainty -- it is derived from caller-controlled stderr text, so it can be wrong in either direction. Billed the same as any other completed run, because it consumed real resources either way -- but if this looks like an ordinary operation, it may be a gap in the sandbox rather than an error in your code; reporting the stderr text helps distinguish the two."},{"code":"PAYMENT_INVALID","category":"payment_required","billable":false,"message":"A valid payment is required. Nothing was charged."},{"code":"PAYMENT_AMOUNT_INSUFFICIENT","category":"payment_required","billable":false,"message":"A valid payment is required. Nothing was charged."},{"code":"PAYMENT_REQUIREMENTS_MISMATCH","category":"payment_required","billable":false,"message":"A valid payment is required. Nothing was charged."},{"code":"PAYMENT_REPLAYED","category":"payment_required","billable":false,"message":"A valid payment is required. Nothing was charged."},{"code":"PAYMENT_SETTLEMENT_FAILED","category":"settlement_failed","billable":false,"message":"The code ran, but the payment could not be settled. Nothing was charged."},{"code":"PAYMENT_SETTLEMENT_INDETERMINATE","category":"settlement_indeterminate","billable":false,"message":"The code ran, and the payment settlement result could not be confirmed -- it may already have succeeded. Do NOT retry with a new payment authorization yet; that risks paying twice. The payer address and nonce in details are enough to check the authorization on chain or reference in a support request. If details.authorizationConsumed is true, the authorization is confirmed consumed on chain -- this is a known charge, not a maybe."},{"code":"SANDBOX_UNAVAILABLE","category":"internal_failure","billable":false,"message":"The service failed to run the request through no fault of the caller."},{"code":"SANDBOX_SETUP_FAILED","category":"internal_failure","billable":false,"message":"The service failed to run the request through no fault of the caller."},{"code":"SERVICE_INTERNAL_FAILURE","category":"internal_failure","billable":false,"message":"The service failed to run the request through no fault of the caller."},{"code":"PAYMENT_FACILITATOR_UNAVAILABLE","category":"internal_failure","billable":false,"message":"The service failed to run the request through no fault of the caller."}],"x-pricing":{"model":"fixed-price-tiers","tiers":[{"name":"default","upToFraction":0,"priceUsd":0.001},{"name":"standard","upToFraction":0.25,"priceUsd":0.005},{"name":"elevated","upToFraction":0.5,"priceUsd":0.01},{"name":"high","upToFraction":0.75,"priceUsd":0.02},{"name":"max","upToFraction":1,"priceUsd":0.05}],"defaultQuote":{"limits":{"timeoutMs":5000,"maxMemoryBytes":2147483648,"maxOutputBytes":1048576,"workDirBytes":67108864},"tier":"default","priceUsd":0.001,"price":"$0.0010"}},"x-payment-header":{"summary":"Every priced route (POST /execute, POST /listings/promote, POST /storage) answers an unpaid request with an HTTP 402 whose body is `{ x402Version, error, accepts: [PaymentRequirements] }` -- pick one entry from `accepts[]` and pay it. Two wire shapes are accepted back; send whichever this client already builds, not both.","transports":{"X-PAYMENT":{"header":"X-PAYMENT","encoding":"base64 of a JSON object","json":{"x402Version":1,"scheme":"exact","network":"<the network named in the challenge>","payload":{"signature":"0x<EIP-712 signature over the transferWithAuthorization>","authorization":{"from":"0x<payer>","to":"0x<the payTo named in the challenge>","value":"<atomic units, >= maxAmountRequired>","validAfter":"<unix seconds>","validBefore":"<unix seconds>","nonce":"0x<32 random bytes>"}}},"description":"x402 protocol v1. The header every x402 client historically agrees on."},"PAYMENT-SIGNATURE":{"header":"PAYMENT-SIGNATURE","encoding":"base64 of a JSON object","json":{"x402Version":2,"accepted":{"scheme":"exact","network":"<CAIP-2 id of the network named in the challenge, e.g. eip155:8453>","amount":"<atomic units, >= the challenge amount>","asset":"<the asset named in the challenge>","payTo":"<the payTo named in the challenge>","maxTimeoutSeconds":"<the maxTimeoutSeconds named in the challenge>"},"payload":{"signature":"0x<EIP-712 signature over the transferWithAuthorization>","authorization":{"from":"0x<payer>","to":"0x<the payTo named in the challenge>","value":"<atomic units, >= maxAmountRequired>","validAfter":"<unix seconds>","validBefore":"<unix seconds>","nonce":"0x<32 random bytes>"}}},"description":"x402 protocol v2. Only needed if this client already speaks v2; v1 on X-PAYMENT is accepted from every client."}},"encoding":"Both headers carry base64 of a UTF-8 JSON object -- base64-decode the header value, then JSON.parse the result.","signing":{"description":"`payload.signature` is NOT a personal-sign string over the JSON payload. It is an EIP-712 typed-data signature over a `TransferWithAuthorization` message (EIP-3009) -- the same on-chain primitive both wire shapes carry verbatim as `payload.authorization`.","domain":{"name":"accepts[].extra.name -- the paid asset's EIP-712 domain name (e.g. \"USD Coin\"), taken from the 402 challenge just received, never hardcoded by the client.","version":"accepts[].extra.version, same source.","chainId":"the EVM chain id for accepts[].network -- 8453 for \"base\" (mainnet), 84532 for \"base-sepolia\".","verifyingContract":"accepts[].asset -- the paid asset's contract address, from the same challenge. This IS the USDC contract for whichever network accepts[].network names."},"primaryType":"TransferWithAuthorization","types":{"TransferWithAuthorization":[{"name":"from","type":"address"},{"name":"to","type":"address"},{"name":"value","type":"uint256"},{"name":"validAfter","type":"uint256"},{"name":"validBefore","type":"uint256"},{"name":"nonce","type":"bytes32"}]},"message":{"from":"the paying wallet -- this doubles as this service's notion of identity; see x-discovery.","to":"accepts[].payTo -- paying anyone else fails PAYMENT_REQUIREMENTS_MISMATCH.","value":"atomic units (not decimal), >= accepts[].maxAmountRequired. Overpaying is accepted; underpaying by one atomic unit is refused.","validAfter":"unix seconds; 0 is valid (immediately usable).","validBefore":"unix seconds. Must be > now, and must NOT exceed now + accepts[].maxTimeoutSeconds (300s on every route this service prices) plus 60s of clock-skew slack -- so at most 360s out. Both bounds are enforced locally before any facilitator call, and previously were only learnable by triggering a PAYMENT_INVALID rejection -- stated here now so that costs nothing.","nonce":"0x-prefixed 32 random bytes, caller-chosen. Reusing one within its validity window is a replay and is refused."}}},"x-discovery":{"ownershipProofs":["0xc8Fceb0F3611BE99c41586eA2B83F6A0911d2Ff0"]}}