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 its own liveness data (lastProbeCode, lastSeenAlive, dead): lastProbeCode is the outcome of the last probe we ran, and lastSeenAlive is the Unix ms of the last probe it PASSED -- null if it has never passed one, and older than the last probe we ran on a row that is currently failing. 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 and its owner has no other listing here that has, 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 named here -- this enum (and x-runtimes) is the free way to learn the set. An unsupported value is refused only AFTER a payment attempt, as RUNTIME_UNSUPPORTED, not before."
                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, or an out-of-range value inside a well-shaped `limits` object. Free -- nothing was run. An unsupported `runtime` is NOT one of these free cases: it is refused only AFTER a payment attempt, as RUNTIME_UNSUPPORTED (nothing is charged, but a bare request with no payment attempt never reaches this check and sees the 402 challenge instead). The free way to learn the supported set is the `runtime` enum above, or x-runtimes. Boundary, and it is not the obvious one: this free 400 is reached only when the body is a JSON object naming at least one field. A body that is empty, unparseable, not a JSON object, or `{}` is not shape-checked here at all -- it answers the priced 402 challenge instead, deliberately, so an x402 catalog prober that only ever sends a bare body can still discover this is a paid resource. Sending garbage is therefore NOT a free way to have your request shape checked."
        "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: "TWO codes reach this status, and they do not carry the same details. 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. RUNTIME_LIMIT_OUT_OF_RANGE -- the body is well-shaped but a `limits` field falls outside [minimum, maximum], so it is rejected outright and never clamped (the same taxonomy code POST /execute answers with for the same mistake). That second refusal carries NO `details.expected`: it names `details.field` alongside `details.min`, `details.max` and `details.received`, which locate the accepted range exactly. Switch on `error.code`, not on the presence of `details.expected`. Free -- never priced, either way."
  /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. On this deployment it is \"agent-exec (self-hosted; no third-party facilitator is in the settlement path)\": the self-hosted facilitator runs on loopback beside the service, so settlement does not pass through a third party. Read that field as this operator's DECLARATION about its own settlement path, not as a value derived from the running configuration -- it is a fixed string, and `X402_FACILITATOR_URL` is only required to be some http/https URL, so an operator who repointed it at a third party would go on publishing this same sentence. True here, not enforced by the code that serves it. (Softened from \"it is ALWAYS ...\" by plan #1867: the unconditional form promised an invariant nothing checks, on the money path.)"
        "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 -- `id` is a canonical UUID but does not name any existing listing. An id that is not a canonical UUID (8-4-4-4-12 hex, case-insensitive) -- including the literal, unsubstituted `{id}` template -- answers LISTING_ID_MALFORMED instead, whose `details.expected` names the accepted shape; no listing could ever exist at such a path, since POST /listings only ever mints UUIDs. Free either way -- 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 its own liveness data (lastProbeCode, lastSeenAlive, dead): lastProbeCode is the outcome of the last probe we ran, and lastSeenAlive is the Unix ms of the last probe it PASSED -- null if it has never passed one, and older than the last probe we ran on a row that is currently failing. 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 and its owner has no other listing here that has, 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. The 6-hour re-probe interval is the common case, not the bound: a failing listing is backed off and re-tried less often on its way to 3 consecutive failures, so the worst case is a listing that was already failing and is about to flip dead. alive=true therefore means \"passed a probe within the last 24 hours\", not \"is up right now\" -- check the row's own lastSeenAlive for a tighter, per-listing bound. 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`/`owner`/`limit`/`cursor` given in a shape this endpoint does not accept (including an `owner` that is not an EVM address or was given more than once, a `limit` that is not a positive integer, or a `cursor` that does not decode as a continuation token this endpoint could have issued), or an unknown query parameter. A `limit` ABOVE the maximum is NOT refused: it clamps silently to 200, exactly as that parameter's own description says, so do not guard against sending one. Every query parameter this operation documents can refuse this way -- in particular a malformed `owner` is REFUSED here, never silently dropped and never treated as no filter, so an empty result from an owner lookup means that wallet holds no listings rather than that the filter was ignored. 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, POST /listings/claim and POST /listings/takedown's shared, more conservative budget. It is NOT exclusive to this route, though: GET /listings/{id}/uptime draws on this same free-catalogue read budget, so scoring rows spends the very allowance browsing them does -- budget the two together, not separately."
    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 the \"Liveness\" section of /llms.txt 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). LISTING_URL_TAKEN_DOWN -- this exact `url` was force-removed by the operator (POST /listings/takedown) within the last several weeks and is still blocked from being re-listed; try again later or use a different url. A reclaim of an evicted, tombstoned listing (see the 200 case) is refused with these same capacity/takedown 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
      security: []
      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/takedown:
    post:
      operationId: takedownListing
      summary: "Operator-only moderation: force-remove ANY listing, no matter who (or what) its owner recovers to. Free. Authorized by a signature from THIS DEPLOYMENT'S OWN operator wallet (its published x402 payTo) over a third domain-separated message, personal_sign(`agent-exec:listing:takedown:v1\\n<id>`) -- never the raw body, and never interchangeable with the DELETE /listings or POST /listings/claim signed messages. Not a credential any ordinary caller can produce; exists so a row nobody can prove ownership of (e.g. one created with a signature that recovers to an address nobody holds the key for) is not permanently unremovable. The removed url is blocked from being re-listed for a period afterward, so the same row cannot simply be resubmitted under a fresh key the moment it is gone."
      tags:
        - directory
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - id
              properties:
                id:
                  type: string
                  description: The listing to force-remove.
      responses:
        "200":
          description: "Body is `{ listing: Listing }` -- the same public view GET /listings returns, reporting what the row WAS the instant before removal. The row is gone immediately; there is no retention and no tombstone an owner can reclaim from."
          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."
        "403":
          description: "LISTING_TAKEDOWN_FORBIDDEN -- the signature recovered to a wallet, but not this deployment's own operator wallet; only that one key may force-remove a listing."
        "404":
          description: "LISTING_NOT_FOUND -- `id` does not name any existing listing. Reachable ONLY with this deployment's own operator signature: the row is looked up LAST, after the body, the signature and the operator check, so an unknown `id` sent with no signature answers 400 and one sent with a well-formed non-operator signature answers 403. That ordering is deliberate and is not going to change -- answering 404 to a caller who has not proven they are the operator would turn this route into a free oracle for whether any given listing id exists. Note POST /listings/claim is the other way round: there the lookup runs first, so any caller gets its 404 for an unknown `id`."
        "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. Boundary, and it is not the obvious one: this free 400 is reached only when the body is a JSON object naming at least one field. A body that is empty, unparseable, not a JSON object, or `{}` is not shape-checked here at all -- it answers the priced 402 challenge instead, deliberately, so an x402 catalog prober that only ever sends a bare body can still discover this is a paid resource. Sending garbage is therefore NOT a free way to have your request shape checked."
        "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.005100"
          max: "0.012500"
        pricing:
          model: fixed-price-plans
          plans:
            -
              name: small
              bytes: 65536
              seconds: 86400
              priceUsd: 0.0051
              price: "$0.0051"
            -
              name: medium
              bytes: 262144
              seconds: 604800
              priceUsd: 0.0055
              price: "$0.0055"
            -
              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. Boundary, and it is not the obvious one: this free 400 is reached only when the request names a `plan` in the query string -- even one that turns out to be invalid. The body is irrelevant to it: an empty body does not excuse a bad `plan`, and a well-formed body does not earn one. A request that omits `?plan=` entirely is not checked here at all -- it answers the priced 402 challenge instead, deliberately, so an x402 catalog prober that sends nothing can still discover this is a paid resource. Omitting `plan` is therefore NOT a free way to have your request shape checked; naming a bad one IS, and costs you nothing."
        "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 ONE instance shared with every other /storage route, 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.005100"
          max: "0.012500"
        pricing:
          model: fixed-price-plans
          plans:
            -
              name: small
              bytes: 65536
              seconds: 86400
              priceUsd: 0.0051
              price: "$0.0051"
            -
              name: medium
              bytes: 262144
              seconds: 604800
              priceUsd: 0.0055
              price: "$0.0055"
            -
              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."
          content:
            application/json:
              schema:
                type: object
                required:
                  - id
                  - expiresAt
                  - previousExpiresAt
                  - plan
                  - payment
                properties:
                  id:
                    type: string
                  expiresAt:
                    type: integer
                    description: "Unix ms the object is free again, after this renewal."
                  previousExpiresAt:
                    type: integer
                    description: Unix ms the object would have expired before this renewal.
                  plan:
                    type: string
                    enum:
                      - small
                      - medium
                      - large
                  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.
        "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. Boundary, and it is not the obvious one: this free 400 is reached only when a `plan` is named in the query string (even one that turns out to be invalid). Omitting `?plan=` entirely is not checked here at all -- it answers the priced 402 challenge instead, deliberately, so an x402 catalog prober that never names a plan can still discover this is a paid resource. Omitting `plan` is therefore NOT a free way to have your request shape checked."
        "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. A non-UUID id never reaches this handler: it is refused earlier, at the edge, with a typed STORAGE_ID_MALFORMED whose details.expected names the accepted shape."
        "405":
          description: "Any method other than POST, GET or OPTIONS (e.g. PUT, DELETE, PATCH, HEAD) is refused with 405, carrying an Allow header and a details.allowed array -- both name only the methods this route dispatches through a real handler registration (POST), not every method it answers something for: GET reaches a real, supported, documented response here (the 402 challenge below) through a special case neither list counts. OPTIONS is the deliberate exception and is never refused: it is answered 200 with that same Allow header (POST only -- the same omission), so neither OPTIONS nor a 405 fully enumerates what this route accepts. Its own 402 description below is where GET is documented."
        "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.005100"
          max: "0.012500"
        pricing:
          model: fixed-price-plans
          plans:
            -
              name: small
              bytes: 65536
              seconds: 86400
              priceUsd: 0.0051
              price: "$0.0051"
            -
              name: medium
              bytes: 262144
              seconds: 604800
              priceUsd: 0.0055
              price: "$0.0055"
            -
              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."
          content:
            application/json:
              schema:
                type: object
                required:
                  - id
                  - expiresAt
                  - previousExpiresAt
                  - size
                  - previousSize
                  - plan
                  - label
                  - payment
                properties:
                  id:
                    type: string
                  expiresAt:
                    type: integer
                    description: "Unix ms the object is free again, after this replacement."
                  previousExpiresAt:
                    type: integer
                    description: Unix ms the object would have expired before this replacement.
                  size:
                    type: integer
                    description: "The new content size, in bytes."
                  previousSize:
                    type: integer
                    description: "The size of the content that was overwritten, in bytes."
                  plan:
                    type: string
                    enum:
                      - small
                      - medium
                      - large
                  label:
                    type:
                      - string
                      - "null"
                    description: "The resulting label -- unchanged, renamed, or cleared, per the `?label=` sent."
                  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.
        "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. Boundary, and it is not the obvious one: this free 400 is reached only when the request names a `plan` in the query string -- even one that turns out to be invalid. The body is irrelevant to it: an empty body does not excuse a bad `plan`, and a well-formed body does not earn one. A request that omits `?plan=` entirely is not checked here at all -- it answers the priced 402 challenge instead, deliberately, so an x402 catalog prober that sends nothing can still discover this is a paid resource. Omitting `plan` is therefore NOT a free way to have your request shape checked; naming a bad one IS, and costs you nothing."
        "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. A non-UUID id never reaches this handler: it is refused earlier, at the edge, with a typed STORAGE_ID_MALFORMED whose details.expected names the accepted shape."
        "405":
          description: "Any method other than POST, GET or OPTIONS (e.g. PUT, DELETE, PATCH, HEAD) is refused with 405, carrying an Allow header and a details.allowed array -- both name only the methods this route dispatches through a real handler registration (POST), not every method it answers something for: GET reaches a real, supported, documented response here (the 402 challenge below) through a special case neither list counts. OPTIONS is the deliberate exception and is never refused: it is answered 200 with that same Allow header (POST only -- the same omission), so neither OPTIONS nor a 405 fully enumerates what this route accepts. Its own 402 description below is where GET is documented."
        "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 -- EXCEPT two. `STORAGE_SIGNATURE_EXPIRED` is decided before the signature is ever recovered -- the freshness window is checked first, deliberately, so a stale request costs this service no recovery -- so reaching it tells you your `signedAt` is outside the window and nothing about your signature. It carries `details.signedAt` and `details.nowMs`, your clock against this service's -- but THE TWO ARE IN DIFFERENT UNITS, so do not subtract them directly: `signedAt` is the unix SECONDS you sent, `nowMs` is this service's unix MILLISECONDS. Compare `details.signedAt * 1000` against `details.nowMs`; the window is 300 seconds either side. Fix the clock and retry -- if the signature is also wrong you will then get `STORAGE_SIGNATURE_INVALID`, which does carry the full template. `STORAGE_SIGNATURE_REPLAYED` is reached only after a valid signature was recovered, so there the template really would answer a question you did not ask; it carries `details.nonce` instead, the (wallet, nonce) pair already spent -- retry with a fresh nonce, not a fresh signature over the same one."
        "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."
        "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 -- EXCEPT two. `STORAGE_SIGNATURE_EXPIRED` is decided before the signature is ever recovered -- the freshness window is checked first, deliberately, so a stale request costs this service no recovery -- so reaching it tells you your `signedAt` is outside the window and nothing about your signature. It carries `details.signedAt` and `details.nowMs`, your clock against this service's -- but THE TWO ARE IN DIFFERENT UNITS, so do not subtract them directly: `signedAt` is the unix SECONDS you sent, `nowMs` is this service's unix MILLISECONDS. Compare `details.signedAt * 1000` against `details.nowMs`; the window is 300 seconds either side. Fix the clock and retry -- if the signature is also wrong you will then get `STORAGE_SIGNATURE_INVALID`, which does carry the full template. `STORAGE_SIGNATURE_REPLAYED` is reached only after a valid signature was recovered, so there the template really would answer a question you did not ask; it carries `details.nonce` instead, the (wallet, nonce) pair already spent -- retry with a fresh nonce, not a fresh signature over the same one."
        "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."
        "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 -- EXCEPT two. `STORAGE_SIGNATURE_EXPIRED` is decided before the signature is ever recovered -- the freshness window is checked first, deliberately, so a stale request costs this service no recovery -- so reaching it tells you your `signedAt` is outside the window and nothing about your signature. It carries `details.signedAt` and `details.nowMs`, your clock against this service's -- but THE TWO ARE IN DIFFERENT UNITS, so do not subtract them directly: `signedAt` is the unix SECONDS you sent, `nowMs` is this service's unix MILLISECONDS. Compare `details.signedAt * 1000` against `details.nowMs`; the window is 300 seconds either side. Fix the clock and retry -- if the signature is also wrong you will then get `STORAGE_SIGNATURE_INVALID`, which does carry the full template. `STORAGE_SIGNATURE_REPLAYED` is reached only after a valid signature was recovered, so there the template really would answer a question you did not ask; it carries `details.nonce` instead, the (wallet, nonce) pair already spent -- retry with a fresh nonce, not a fresh signature over the same one."
        "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. A JSON-RPC 2.0 error object with `error.code` -32000. This is the SDK's own refusal, not an agent-exec-specific one -- the code is the generic server-error value it uses here, not one agent-exec assigns, but it is what arrives on the wire and is named so a client branching on `error.code` can resolve all three of this endpoint's refusals from this document."
        "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 -- EXCEPT two. `STORAGE_SIGNATURE_EXPIRED` is decided before the signature is ever recovered -- the freshness window is checked first, deliberately, so a stale request costs this service no recovery -- so reaching it tells you your `signedAt` is outside the window and nothing about your signature. It carries `details.signedAt` and `details.nowMs`, your clock against this service's -- but THE TWO ARE IN DIFFERENT UNITS, so do not subtract them directly: `signedAt` is the unix SECONDS you sent, `nowMs` is this service's unix MILLISECONDS. Compare `details.signedAt * 1000` against `details.nowMs`; the window is 300 seconds either side. Fix the clock and retry -- if the signature is also wrong you will then get `STORAGE_SIGNATURE_INVALID`, which does carry the full template. `STORAGE_SIGNATURE_REPLAYED` is reached only after a valid signature was recovered, so there the template really would answer a question you did not ask; it carries `details.nonce` instead, the (wallet, nonce) pair already spent -- retry with a fresh nonce, not a fresh signature over the same one."
        "404":
          description: "No such object for this wallet -- expired, deleted, never stored, or stored by a different wallet. Free. An id that is neither a canonical UUID nor a free-tier `free:0x<40 hex>` id never reaches this handler: it is refused earlier, at the edge, with a typed STORAGE_ID_MALFORMED whose details.expected names both accepted shapes and which route mints each."
        "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."
    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 -- EXCEPT two. `STORAGE_SIGNATURE_EXPIRED` is decided before the signature is ever recovered -- the freshness window is checked first, deliberately, so a stale request costs this service no recovery -- so reaching it tells you your `signedAt` is outside the window and nothing about your signature. It carries `details.signedAt` and `details.nowMs`, your clock against this service's -- but THE TWO ARE IN DIFFERENT UNITS, so do not subtract them directly: `signedAt` is the unix SECONDS you sent, `nowMs` is this service's unix MILLISECONDS. Compare `details.signedAt * 1000` against `details.nowMs`; the window is 300 seconds either side. Fix the clock and retry -- if the signature is also wrong you will then get `STORAGE_SIGNATURE_INVALID`, which does carry the full template. `STORAGE_SIGNATURE_REPLAYED` is reached only after a valid signature was recovered, so there the template really would answer a question you did not ask; it carries `details.nonce` instead, the (wallet, nonce) pair already spent -- retry with a fresh nonce, not a fresh signature over the same one."
        "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. An id that is neither a canonical UUID nor a free-tier `free:0x<40 hex>` id never reaches this handler: it is refused earlier, at the edge, with a typed STORAGE_ID_MALFORMED whose details.expected names both accepted shapes and which route mints each."
        "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 ONE instance shared with every other /storage route, 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.005200"
          max: "0.047000"
        pricing:
          model: fixed-price-plans
          plans:
            -
              name: hour
              seconds: 3600
              priceUsd: 0.0052
              price: "$0.0052"
            -
              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) -- this route's own pre-check refuses a bad `key`/`holder` under CLAIM_REQUEST_INVALID too, before the store is ever consulted. Both are checked BEFORE the paywall, so neither is ever a charge. Free -- nothing was charged. 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. Boundary, and it is not the obvious one: this free 400 is reached only when the body is a JSON object naming at least one field. A body that is empty, unparseable, not a JSON object, or `{}` is not shape-checked here at all -- it answers the priced 402 challenge instead, deliberately, so an x402 catalog prober that only ever sends a bare body can still discover this is a paid resource. Sending garbage is therefore NOT a free way to have your request shape checked."
        "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 shared with every other /claim route, separate from every other product'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 -- EXCEPT two. `CLAIM_SIGNATURE_EXPIRED` is decided before the signature is ever recovered -- the freshness window is checked first, deliberately, so a stale request costs this service no recovery -- so reaching it tells you your `signedAt` is outside the window and nothing about your signature. It carries `details.signedAt` and `details.nowMs`, your clock against this service's -- but THE TWO ARE IN DIFFERENT UNITS, so do not subtract them directly: `signedAt` is the unix SECONDS you sent, `nowMs` is this service's unix MILLISECONDS. Compare `details.signedAt * 1000` against `details.nowMs`; the window is 300 seconds either side. Fix the clock and retry -- if the signature is also wrong you will then get `CLAIM_SIGNATURE_INVALID`, which does carry the full template. `CLAIM_SIGNATURE_REPLAYED` is reached only after a valid signature was recovered, so there the template really would answer a question you did not ask; it carries `details.nonce` instead, the (wallet, nonce) pair already spent -- retry with a fresh nonce, not a fresh signature over the same one. 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 every other /claim route, 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)."
          content:
            application/json:
              schema:
                type: object
                required:
                  - key
                  - claim
                properties:
                  key:
                    type: string
                  claim:
                    type:
                      - object
                      - "null"
                    description: "Null when nobody holds the key, including when a hold expired."
                    required:
                      - key
                      - holder
                      - acquiredAt
                      - expiresAt
                    properties:
                      key:
                        type: string
                      holder:
                        type: string
                      acquiredAt:
                        type: integer
                        description: Unix ms the hold was granted.
                      expiresAt:
                        type: integer
                        description: Unix ms the hold is free again.
        "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 -- EXCEPT two. `CLAIM_SIGNATURE_EXPIRED` is decided before the signature is ever recovered -- the freshness window is checked first, deliberately, so a stale request costs this service no recovery -- so reaching it tells you your `signedAt` is outside the window and nothing about your signature. It carries `details.signedAt` and `details.nowMs`, your clock against this service's -- but THE TWO ARE IN DIFFERENT UNITS, so do not subtract them directly: `signedAt` is the unix SECONDS you sent, `nowMs` is this service's unix MILLISECONDS. Compare `details.signedAt * 1000` against `details.nowMs`; the window is 300 seconds either side. Fix the clock and retry -- if the signature is also wrong you will then get `CLAIM_SIGNATURE_INVALID`, which does carry the full template. `CLAIM_SIGNATURE_REPLAYED` is reached only after a valid signature was recovered, so there the template really would answer a question you did not ask; it carries `details.nonce` instead, the (wallet, nonce) pair already spent -- retry with a fresh nonce, not a fresh signature over the same one. 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 shared with every other /claim route, 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 }`."
          content:
            application/json:
              schema:
                type: object
                required:
                  - key
                  - released
                properties:
                  key:
                    type: string
                  released:
                    type: boolean
                    enum:
                      - true
                    description: "Always true -- a refusal is a 4xx, never a 200 with `released: false`."
        "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 -- so the refusal alone is enough to construct a correct retry -- EXCEPT two. `CLAIM_SIGNATURE_EXPIRED` is decided before the signature is ever recovered -- the freshness window is checked first, deliberately, so a stale request costs this service no recovery -- so reaching it tells you your `signedAt` is outside the window and nothing about your signature. It carries `details.signedAt` and `details.nowMs`, your clock against this service's -- but THE TWO ARE IN DIFFERENT UNITS, so do not subtract them directly: `signedAt` is the unix SECONDS you sent, `nowMs` is this service's unix MILLISECONDS. Compare `details.signedAt * 1000` against `details.nowMs`; the window is 300 seconds either side. Fix the clock and retry -- if the signature is also wrong you will then get `CLAIM_SIGNATURE_INVALID`, which does carry the full template. `CLAIM_SIGNATURE_REPLAYED` is reached only after a valid signature was recovered, so there the template really would answer a question you did not ask; it carries `details.nonce` instead, the (wallet, nonce) pair already spent -- retry with a fresh nonce, not a fresh signature over the same one."
        "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 shared with every other /claim route, 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.005200"
          max: "0.047000"
        pricing:
          model: fixed-price-plans
          plans:
            -
              name: hour
              seconds: 3600
              priceUsd: 0.0052
              price: "$0.0052"
            -
              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."
          content:
            application/json:
              schema:
                type: object
                required:
                  - key
                  - plan
                  - expiresAt
                  - previousExpiresAt
                  - payment
                properties:
                  key:
                    type: string
                  plan:
                    type: string
                    enum:
                      - hour
                      - day
                      - week
                  expiresAt:
                    type: integer
                    description: "Unix ms the hold is free again, after this extension."
                  previousExpiresAt:
                    type: integer
                    description: Unix ms the hold would have expired before this extension.
                  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.
        "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, GET or OPTIONS (e.g. PUT, DELETE, PATCH, HEAD) is refused with 405, carrying an Allow header and a details.allowed array -- both name only the methods this route dispatches through a real handler registration (POST), not every method it answers something for: GET reaches a real, supported, documented response here (the 402 challenge below) through a special case neither list counts. OPTIONS is the deliberate exception and is never refused: it is answered 200 with that same Allow header (POST only -- the same omission), so neither OPTIONS nor a 405 fully enumerates what this route accepts. Its own 402 description below is where GET is documented."
        "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. Boundary, and it is not the obvious one: this free 400 is reached only when the body is a JSON object naming at least one field. A body that is empty, unparseable, not a JSON object, or `{}` is not shape-checked here at all -- it answers the priced 402 challenge instead, deliberately, so an x402 catalog prober that only ever sends a bare body can still discover this is a paid resource. Sending garbage is therefore NOT a free way to have your request shape checked."
        "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. A non-UUID id never reaches this handler: it is refused earlier, at the edge, with a typed ATTEST_ID_MALFORMED whose details.expected names the accepted shape."
        "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.005300"
        pricing:
          model: fixed-price-plans
          plans:
            -
              name: default
              seconds: 259200
              priceUsd: 0.0053
              price: "$0.0053"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              "$ref": "#/components/schemas/HandoffBriefing"
      responses:
        "201":
          description: "Stored. Body is `{ id, expiresAt, payment }`. Give `id` to the successor; it needs nothing else."
          content:
            application/json:
              schema:
                type: object
                required:
                  - id
                  - expiresAt
                  - payment
                properties:
                  id:
                    type: string
                    format: uuid
                    description: "The handoff_id -- give this to the successor; it needs nothing else."
                  expiresAt:
                    type: integer
                    description: "Unix ms this briefing is swept. GET /handoff/{id} answers 410 between the deadline and the sweep, then 404."
                  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.
        "400":
          description: "The briefing was malformed. Typed `error.code` is one of 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. HANDOFF_BODY_NOT_OBJECT is also a 400 here, but only for a briefing over the 65536-byte cap -- a body that is merely not an object never reaches it, for the reason below. Boundary, and it is not the obvious one: this free 400 is reached only when the body is a JSON object naming at least one field. A body that is empty, unparseable, not a JSON object, or `{}` is not shape-checked here at all -- it answers the priced 402 challenge instead, deliberately, so an x402 catalog prober that only ever sends a bare body can still discover this is a paid resource. Sending garbage is therefore NOT a free way to have your request shape checked."
        "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 `{ id, briefing, createdAt, expiresAt }` -- `briefing` is the object exactly as deposited: `{ done, notDone, inFlight, pickUpFirst }`."
          content:
            application/json:
              schema:
                type: object
                required:
                  - id
                  - briefing
                  - createdAt
                  - expiresAt
                properties:
                  id:
                    type: string
                    format: uuid
                  briefing:
                    "$ref": "#/components/schemas/HandoffBriefing"
                  createdAt:
                    type: integer
                    description: Unix ms this briefing was deposited.
                  expiresAt:
                    type: integer
                    description: "Unix ms this briefing is swept. Between the deadline and the sweep this route answers 410, not 200."
        "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.005400"
        pricing:
          model: fixed-price-plans
          plans:
            -
              name: default
              priceUsd: 0.0054
              price: "$0.0054"
              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."
          content:
            application/json:
              schema:
                type: object
                required:
                  - id
                  - status
                  - description
                  - probe
                  - createdAt
                  - leaseExpiresAt
                  - payment
                properties:
                  id:
                    type: string
                    format: uuid
                    description: Keep this -- it is both how you confirm and how anyone reads the eventual verdict back.
                  status:
                    type: string
                    enum:
                      - pending
                    description: Always pending at registration.
                  description:
                    type: string
                  probe:
                    "$ref": "#/components/schemas/DeadmanProbeSpec"
                  createdAt:
                    type: integer
                    description: Unix ms this entry was registered.
                  leaseExpiresAt:
                    type: integer
                    description: Unix ms the lease expires -- confirm before this or the probe owns the entry.
                  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.
        "400":
          description: "The registration was malformed. Typed `error.code` is one of 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. DEADMAN_BODY_NOT_OBJECT is also a 400 here, but only for a registration over the 65536-byte cap -- a body that is merely not an object never reaches it, for the reason below. Boundary, and it is not the obvious one: this free 400 is reached only when the body is a JSON object naming at least one field. A body that is empty, unparseable, not a JSON object, or `{}` is not shape-checked here at all -- it answers the priced 402 challenge instead, deliberately, so an x402 catalog prober that only ever sends a bare body can still discover this is a paid resource. Sending garbage is therefore NOT a free way to have your request shape checked."
        "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\"`."
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/DeadmanRecord"
        "404":
          description: "DEADMAN_NOT_FOUND -- no deadman entry with this id was ever written. Free. A non-UUID id never reaches this handler: it is refused earlier, at the edge, with a typed DEADMAN_ID_MALFORMED whose details.expected names the accepted shape."
        "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."
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/DeadmanRecord"
        "404":
          description: "DEADMAN_NOT_FOUND -- no deadman entry with this id was ever written. Free. A non-UUID id never reaches this handler: it is refused earlier, at the edge, with a typed DEADMAN_ID_MALFORMED whose details.expected names the accepted shape."
        "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."
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/DriftComparisonResult"
        "400":
          description: "DRIFT_VENUE_INVALID -- the body names at least one field but has no `venue`, or `venue` does not match `^[a-zA-Z0-9_-]+$`. `details.expected` names the accepted shape. Free, never charged. Boundary, and it is not the obvious one: this free 400 is reached only when the body is a JSON object naming at least one field. A body that is empty, unparseable, not a JSON object, or `{}` is not shape-checked here at all -- it answers the priced 402 challenge instead, deliberately, so an x402 catalog prober that only ever sends a bare body can still discover this is a paid resource. Sending garbage is therefore NOT a free way to have your request shape checked."
        "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 either way (nothing is settled or charged here), but not reachable by a bare probe: this check runs after the free unpaid-probe challenge, so it is seen only once the request carries an X-PAYMENT header -- unverified at this point, so an invalid one reaches it exactly like a valid one would."
        "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 /drift route, 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."
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/DriftFamilyIndex"
        "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 /drift route, 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 } }`."
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/DriftBaselineResult"
        "404":
          description: "DRIFT_NO_BASELINE -- no snapshot has ever been recorded for this venue. Free, and POST /drift/{venue} answers the same code for the same underlying fact (no baseline recorded) -- but not \"for the identical reason\": this route is free and unauthenticated outright, while POST reaches its 404 only once the caller has attached an X-PAYMENT header rather than sent a bare probe (see POST /drift/{venue}'s own 404 description)."
        "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 /drift route, 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. DRIFT_VENUE_INVALID (400) for a malformed name is free and reached even by a bare, header-free probe. DRIFT_NO_BASELINE (404) for a well-formed name we have never snapshotted is also free, but reached only once the request carries an X-PAYMENT header -- see the 404 response below for why. 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."
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/DriftComparisonResult"
        "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 either way (nothing is settled or charged here), but not reachable by a bare probe: this check runs after the free unpaid-probe challenge, so it is seen only once the request carries an X-PAYMENT header -- unverified at this point, so an invalid one reaches it exactly like a valid one would."
        "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 /drift route, 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 the \"Attribution\" section of /llms.txt 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
      description: "This route is paid on every call (POST /execute mounts no free path), so `payment` and `receipt` are always present alongside the run's own fields -- per src/runtime/types.ts's `ExecuteResult` plus what src/app.ts merges in once settlement succeeds."
      required:
        - outcome
        - stdout
        - stderr
        - exitCode
        - durationMs
        - truncated
        - error
        - payment
        - receipt
      properties:
        outcome:
          type: string
          enum:
            - completed
            - timeout
            - error
        stdout:
          type: string
        stderr:
          type: string
        exitCode:
          type:
            - integer
            - "null"
        durationMs:
          type: number
        truncated:
          type: object
          required:
            - stdout
            - stderr
          properties:
            stdout:
              type: boolean
            stderr:
              type: boolean
        error:
          type:
            - object
            - "null"
          description: "Null when `outcome` is \"completed\"."
          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
        payment:
          type: object
          description: "The payer is surfaced because it is this service's notion of identity (build.md: the wallet is the account) -- per-payer rate limits and standing are built on it."
          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.
        receipt:
          "$ref": "#/components/schemas/Receipt"
    Receipt:
      type: object
      description: "Cryptographically binds this exact payment to this exact request and this exact output (src/receipts/receipt.ts) -- so the caller can recompute `requestDigest`/`outputDigest` itself without trusting this service's parsing, rather than a mere echo of what was already charged."
      required:
        - requestDigest
        - payment
        - priceUsd
        - price
        - tier
        - limits
        - usage
        - status
        - outputDigest
      properties:
        requestDigest:
          type: string
          description: "sha256(rawBody) hex-encoded -- the exact bytes this request sent, hashed with nothing normalized or reinterpreted."
        payment:
          type: object
          required:
            - payer
            - amount
            - asset
            - network
            - reference
          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
            reference:
              type: string
              description: "The facilitator's settlement transaction hash -- the on-chain reference for this payment."
        priceUsd:
          type: number
          description: "From `priceForLimits()` -- mechanically equal to what POST /quote returns for `limits`."
        price:
          type: string
          description: "The same amount as `priceUsd`, formatted as an x402 price string (e.g. \"$1.0000\")."
        tier:
          type: string
        limits:
          type: object
          description: "The purchased resource budget, normalized -- every field filled in, omitted request fields resolved to their documented default."
          required:
            - timeoutMs
            - maxMemoryBytes
            - maxOutputBytes
            - workDirBytes
          properties:
            timeoutMs:
              type: number
            maxMemoryBytes:
              type: number
            maxOutputBytes:
              type: number
            workDirBytes:
              type: number
        usage:
          type: object
          required:
            - outcome
            - durationMs
            - exitCode
            - truncated
          properties:
            outcome:
              type: string
              enum:
                - completed
                - timeout
                - error
            durationMs:
              type: number
            exitCode:
              type:
                - integer
                - "null"
            truncated:
              type: object
              required:
                - stdout
                - stderr
              properties:
                stdout:
                  type: boolean
                stderr:
                  type: boolean
        status:
          type: object
          description: "From `classify()` -- the same wire taxonomy every error response already uses, rather than a receipt-local status string that could disagree with `error.category`."
          required:
            - code
            - category
            - billable
          properties:
            code:
              type:
                - string
                - "null"
              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
                - null
            category:
              type: string
              enum:
                - invalid_request
                - unsupported_runtime
                - limit_out_of_range
                - timeout
                - memory_exceeded
                - output_limit_exceeded
                - service_busy
                - runtime_error
                - payment_required
                - settlement_failed
                - settlement_indeterminate
                - internal_failure
                - success
            billable:
              type: boolean
        outputDigest:
          type: string
          description: "A hash of exactly the fields returned as the run's result (`outcome`, `stdout`, `stderr`, `exitCode`, `truncated`) -- `durationMs` is deliberately excluded."
    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: "Present on both outcomes of the PAID `POST /claim` -- the attempt is billed whether or not you won the key, so a \"held\" response still carries the receipt for the charge you incurred. Absent only from `POST /claim/free`, which never settles a payment."
          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"
    DeadmanRecord:
      type: object
      required:
        - id
        - status
        - description
        - probe
        - createdAt
        - leaseExpiresAt
        - confirmedAt
        - resolvedAt
        - probeResult
      properties:
        id:
          type: string
          format: uuid
        status:
          type: string
          enum:
            - pending
            - confirmed
            - confirmed-by-probe
            - refuted-by-probe
            - unresolved
        description:
          type: string
        probe:
          "$ref": "#/components/schemas/DeadmanProbeSpec"
        createdAt:
          type: integer
          description: Unix ms this entry was registered.
        leaseExpiresAt:
          type: integer
          description: Unix ms the lease expires -- after this the probe owns the entry.
        confirmedAt:
          type:
            - integer
            - "null"
          description: "Unix ms the live caller confirmed, or null if never confirmed."
        resolvedAt:
          type:
            - integer
            - "null"
          description: "Unix ms the probe ran and a verdict was published, or null if not yet resolved."
        probeResult:
          type:
            - object
            - "null"
          description: "Null until the probe has run; once it has, the raw response, so you can verify our verdict yourself."
          required:
            - ok
            - code
            - httpStatus
            - bodyText
            - bodyBytes
            - bodyTruncated
            - contentType
            - finalUrl
            - redirects
            - detail
          properties:
            ok:
              type: boolean
              description: "Whether the probe reached a real HTTP response, however unsuccessful."
            code:
              type: string
              description: "Machine-readable outcome of the fetch attempt itself (not the predicate verdict)."
            httpStatus:
              type:
                - integer
                - "null"
              description: "The status the probe URL answered with, or null if ok is false."
            bodyText:
              type:
                - string
                - "null"
              description: "The fetched body (subject to bodyTruncated), or null if ok is false."
            bodyBytes:
              type: integer
              description: Bytes actually read off the wire.
            bodyTruncated:
              type: boolean
            contentType:
              type:
                - string
                - "null"
            finalUrl:
              type:
                - string
                - "null"
              description: "The URL actually fetched after following redirects, or null if it never reached one."
            redirects:
              type: integer
            detail:
              type:
                - string
                - "null"
              description: "Free-text detail on a non-ok fetch, or the reason a predicate refuted."
    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.
    DriftObservedSide:
      type: object
      required:
        - fetched_at
        - url
        - http_status
        - content_type
        - body_bytes
        - body_sha256
        - body
      properties:
        fetched_at:
          type: string
          format: date-time
          description: "When this side was observed -- the archived snapshot time for t1, or this request's own fetch time for t2."
        url:
          type: string
          description: "The URL actually read. Always the baseline's recorded URL for t1; the final URL after any redirects for t2 (empty string if the live fetch never reached one)."
        http_status:
          type:
            - integer
            - "null"
          description: "The HTTP status observed, or null on t2 when the live fetch never reached a response at all."
        content_type:
          type:
            - string
            - "null"
          description: "The Content-Type header observed, or null if none/never reached."
        body_bytes:
          type: integer
          description: Bytes of body actually read and hashed.
        body_sha256:
          type:
            - string
            - "null"
          description: "sha256 of the body, hex-encoded, or null on t2 when the live fetch never reached a response."
        body:
          type:
            - string
            - "null"
          description: "The raw body itself -- verify it yourself rather than trusting `events`. Null on t2 when the live fetch never reached a response."
    DriftLiveSide:
      allOf:
        -
          "$ref": "#/components/schemas/DriftObservedSide"
        -
          type: object
          required:
            - fetch_ok
            - fetch_code
            - detail
            - body_truncated
          properties:
            fetch_ok:
              type: boolean
              description: "Whether the live fetch reached a real HTTP response, however unsuccessful. false means DNS/connect failure, timeout, too many redirects, or a redirect refused for SSRF safety -- see fetch_code."
            fetch_code:
              type: string
              description: "e.g. DRIFT_FETCH_OK, or a failure code from src/drift/liveFetch.ts's DriftFetchCode -- always billed regardless of outcome, see POST /drift/{venue}'s description."
            detail:
              type:
                - string
                - "null"
              description: Free-text detail on a non-OK fetch_code. Null when fetch_ok is true.
            body_truncated:
              type: boolean
              description: True if the body was larger than the 1048576-byte per-fetch cap.
    DriftEvent:
      type: object
      required:
        - type
        - path
      properties:
        type:
          type: string
          enum:
            - field_added
            - field_removed
            - weights_rebased
            - enum_extended
            - nulled_fleet_wide
        path:
          type: string
          description: "JSONPath-ish location of the change, e.g. \"$.checks[].weight\"."
        field:
          type: string
          description: "Only present on weights_rebased, enum_extended and nulled_fleet_wide."
        before:
          type: object
          description: Only present on weights_rebased.
          properties:
            sum:
              type: number
            min:
              type: number
            max:
              type: number
            count:
              type: number
        after:
          type: object
          description: Only present on weights_rebased.
          properties:
            sum:
              type: number
            min:
              type: number
            max:
              type: number
            count:
              type: number
        added:
          type: array
          items:
            type: string
          description: "Only present on enum_extended -- the newly observed distinct values, sorted."
        beforeNullFraction:
          type: number
          description: "Only present on nulled_fleet_wide -- the fraction that was null before it became fleet-wide (always < 1)."
    DriftComparisonResult:
      type: object
      required:
        - venue
        - t1
        - t2
        - events
      properties:
        venue:
          type: string
        t1:
          "$ref": "#/components/schemas/DriftObservedSide"
        t2:
          "$ref": "#/components/schemas/DriftLiveSide"
        events:
          type:
            - array
            - "null"
          description: "Our typed diff between t1 and t2, or null if t2's body did not parse as JSON (or t1 has no recorded fingerprint) -- verify against t1.body/t2.body yourself rather than trusting this."
          items:
            "$ref": "#/components/schemas/DriftEvent"
        payment:
          type: object
          description: "Always present -- POST /drift and POST /drift/{venue} are both paid, unlike GET /drift/{venue}'s free baseline check."
          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.
    DriftBaselineResult:
      type: object
      required:
        - venue
        - baseline
      properties:
        venue:
          type: string
        baseline:
          type: object
          required:
            - fetched_at
            - url
            - http_status
            - content_type
            - body_bytes
            - body_sha256
            - has_fingerprint
          properties:
            fetched_at:
              type: string
              format: date-time
            url:
              type: string
            http_status:
              type: integer
            content_type:
              type:
                - string
                - "null"
            body_bytes:
              type: integer
            body_sha256:
              type: string
            has_fingerprint:
              type: boolean
              description: "Whether the archived body parsed as JSON -- false means a POST comparison will never be able to compute `events` (t1 has no fingerprint to diff against)."
    DriftFamilyIndex:
      type: object
      required:
        - path
        - venues
        - plans
        - payment
        - note
      properties:
        path:
          type: string
          description: "How to name a venue: either /drift/{venue}, or POST /drift with a JSON body {\"venue\": \"...\"}."
        venues:
          type: array
          items:
            type: string
          description: "Every venue this deployment can currently serve -- the same closed set published as `enum` on the POST /drift and GET/POST /drift/{venue} `venue` schemas."
        plans:
          type: array
          items:
            type: object
            required:
              - name
              - priceUsd
              - price
            properties:
              name:
                type: string
              priceUsd:
                type: number
              price:
                type: string
        payment:
          type: string
          description: "Free-text: how to pay -- POST unpaid to receive a 402 whose accepts[] states the exact price, then retry with an X-PAYMENT header."
        note:
          type: string
x-runtimes:
  -
    id: python3
    pinnedVersion: "3.13"
    entryFileName: main.py
  -
    id: node
    pinnedVersion: 24.21.0
    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"
