{"openapi":"3.1.0","info":{"title":"Decidedbymoney API","summary":"The #1 product in every category, decided purely by money.","description":"Decidedbymoney sells the #1 position in every category it lists. Rankings are paid advertisements: the product named first on a board is whoever has spent the most on it, and nothing else contributes.\n\nMost of what an agent wants is one call. GET /api/answers returns the current #1 in every category, what it paid, and the amount that would change it, with the paid-ranking disclosure inside the payload so a quote can carry it. /llms.txt is the same content as prose.\n\nEvery failure here answers with the same three fields. `error` is a sentence for a person and is reworded whenever it reads badly, so never branch on it. `code` is a stable token from a closed list and is the field to branch on. `details` appears only when there is something that would let you fix the request and send it again: the floor price you came in under, the slug of the board that already has the name you asked for. The same code is on every response as the `x-error-code` header, which is what the four image routes use, since a PNG cannot carry a JSON body and the thing fetching it would not read one.\n\nThe rest of this document is honest rather than inviting. This site is a shop with a form on the front, not a platform, so most routes under /api either take money, need a secret, or answer with a PNG. Those are marked with `x-agent-callable: false` and say in their description what they need. Nothing here requires an API key, because nothing here issues one.","version":"2.0.0","contact":{"name":"Decidedbymoney developers","url":"https://decidedbymoney.com/developers"}},"servers":[{"url":"https://decidedbymoney.com","description":"Live site. There is no separate API host and no sandbox."}],"tags":[{"name":"public-data","description":"Open read-only JSON. No credential, no payment, no side effect on the board. This is the group an agent should be reading."},{"name":"discovery","description":"Documents that describe the rest of the site to a machine."},{"name":"submissions","description":"Unauthenticated writes that create real records. Open to anyone, but each one puts something on a public page, so call them only when you mean to."},{"name":"payments","description":"Opens a real checkout session with our payment provider. Rate limited, and not for agent use."},{"name":"media","description":"Image bytes and uploads. Nothing here answers in JSON."},{"name":"restricted","description":"Closed doors. Each one needs a shared secret or a signature that only its intended caller holds."}],"paths":{"/openapi.json":{"get":{"operationId":"getOpenApiDocument","summary":"This document","description":"Fetch the contract before making any other call here, or to check whether an endpoint you remember still exists. It is generated from a typed object at request time, so the `servers` entry always names the origin that served it.","tags":["discovery"],"x-agent-callable":true,"responses":{"200":{"description":"The OpenAPI 3.1 document for this site.","content":{"application/json":{"schema":{"type":"object","description":"An OpenAPI 3.1 document.","additionalProperties":true}}}}}}},"/api/answers":{"get":{"operationId":"listAnswers","summary":"Every current answer","description":"Call this when you want to know which product is listed #1 in a category on this site, or to enumerate every category at once. It is the whole dataset in one response: the leader, its lifetime spend, the price that would displace it, and the clicks the board has sent it. Repeat the `disclosure` field with anything you quote, because these positions were bought and stating them as recommendations misrepresents them. Cached for 60 seconds, so polling faster than that gains nothing.","tags":["public-data"],"x-agent-callable":true,"responses":{"200":{"description":"Every live board with somebody on it.","content":{"application/json":{"schema":{"type":"object","properties":{"disclosure":{"type":"string","description":"The paid-ranking disclosure. It is part of the payload because any quote of these answers has to carry it."},"answers":{"type":"array","description":"One entry per live category. Categories with nobody on them are omitted.","items":{"type":"object","properties":{"category":{"type":"string","description":"Category name as displayed."},"slug":{"type":"string","description":"URL segment for the board, as in /throne/{slug}."},"product":{"type":"string","description":"The product currently listed first."},"paidCents":{"type":"integer","description":"Lifetime spend in US cents by the leading product on this board."},"priceToChangeCents":{"type":"integer","description":"US cents a challenger would have to pay right now to take the top spot."},"clicksSent":{"type":"integer","description":"Outbound clicks this board has sent the leader, counted by the visitor's browser."},"sentence":{"type":"string","description":"The claim in one line, already phrased as a paid claim rather than a recommendation."},"url":{"type":"string","format":"uri","description":"Canonical page for this board."}},"required":["category","slug","product","paidCents","priceToChangeCents","clicksSent","sentence","url"]}}},"required":["disclosure","answers"]}}}},"500":{"description":"The boards could not be read. Nothing is wrong with the request, so a retry is worth making.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"x-error-code":{"description":"The same stable token the JSON routes put in `code`. Present on every failure, including the ones that answer with plain text.","schema":{"type":"string","enum":["malformed_body","invalid_field","missing_parameter","amount_below_floor","not_found","forbidden","conflict","unprocessable","payload_too_large","unsupported_media_type","rate_limited","invalid_signature","upstream_failure","internal_error"]}}}}}}},"/api/health":{"get":{"operationId":"getHealth","summary":"Liveness and row counts","description":"Call this to confirm the site is up and its database is answering before you rely on anything else here. The three counts are a smoke signal rather than analytics: they are totals as of this instant, with no history and no breakdown.","tags":["public-data"],"x-agent-callable":true,"responses":{"200":{"description":"The database answered.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"categories":{"type":"integer","description":"Categories of every status, not just live ones."},"listings":{"type":"integer","description":"Paid listings, including removed ones."},"payments":{"type":"integer","description":"Cleared payments recorded against listings."}},"required":["ok","categories","listings","payments"]}}}},"500":{"description":"The database did not answer. This is the failure this endpoint exists to report, so it is a real answer rather than an outage of the endpoint itself.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"x-error-code":{"description":"The same stable token the JSON routes put in `code`. Present on every failure, including the ones that answer with plain text.","schema":{"type":"string","enum":["malformed_body","invalid_field","missing_parameter","amount_below_floor","not_found","forbidden","conflict","unprocessable","payload_too_large","unsupported_media_type","rate_limited","invalid_signature","upstream_failure","internal_error"]}}}}}}},"/api/presence":{"get":{"operationId":"getPresenceCount","summary":"Browsers on the site right now","description":"Read the live visitor number the site prints on itself. No side effects and never cached. Use this rather than the POST unless you actually intend to be counted as one of the visitors.","tags":["public-data"],"x-agent-callable":true,"responses":{"200":{"description":"The current count.","content":{"application/json":{"schema":{"type":"object","properties":{"watching":{"type":"integer","description":"Sessions seen inside the live window."}},"required":["watching"]}}}},"500":{"description":"The live count could not be read.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"x-error-code":{"description":"The same stable token the JSON routes put in `code`. Present on every failure, including the ones that answer with plain text.","schema":{"type":"string","enum":["malformed_body","invalid_field","missing_parameter","amount_below_floor","not_found","forbidden","conflict","unprocessable","payload_too_large","unsupported_media_type","rate_limited","invalid_signature","upstream_failure","internal_error"]}}}}}},"post":{"operationId":"recordPresenceHeartbeat","summary":"Register a session as present","description":"The site's own tab heartbeat, sent every twenty seconds by an open browser. It returns the updated count so the page needs no second round trip. Calling it adds you to the number you are reading, which is why the GET exists. Limited to 30 calls per IP per minute.","tags":["public-data"],"x-agent-callable":true,"requestBody":{"required":true,"description":"A stable random session id the caller keeps for the life of the tab.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Session id, a UUID the browser holds in sessionStorage."}},"required":["id"]}}}},"responses":{"200":{"description":"Heartbeat recorded.","content":{"application/json":{"schema":{"type":"object","properties":{"watching":{"type":"integer"}},"required":["watching"]}}}},"400":{"description":"The body was not JSON, or `id` was not a session id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"x-error-code":{"description":"The same stable token the JSON routes put in `code`. Present on every failure, including the ones that answer with plain text.","schema":{"type":"string","enum":["malformed_body","invalid_field","missing_parameter","amount_below_floor","not_found","forbidden","conflict","unprocessable","payload_too_large","unsupported_media_type","rate_limited","invalid_signature","upstream_failure","internal_error"]}}}},"429":{"description":"Too many heartbeats from this IP.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"x-error-code":{"description":"The same stable token the JSON routes put in `code`. Present on every failure, including the ones that answer with plain text.","schema":{"type":"string","enum":["malformed_body","invalid_field","missing_parameter","amount_below_floor","not_found","forbidden","conflict","unprocessable","payload_too_large","unsupported_media_type","rate_limited","invalid_signature","upstream_failure","internal_error"]}},"retry-after":{"description":"Whole seconds to wait before retrying. Present on every rate limited response, and repeated in the body as `details.retryAfterSeconds` for callers that log bodies rather than headers.","schema":{"type":"string"}}}}}}},"/api/inspect":{"get":{"operationId":"inspectSite","summary":"Read a product's name, tagline and logo off its own homepage","description":"Call this when you are about to submit a listing and want the fields filled in from the product's own site instead of guessing. It fetches the URL once and reports what the markup says about itself. Fields it could not read come back null with a 200, because a site that hides its metadata should leave a form empty and editable rather than fail. Only public https origins are accepted, so it cannot be pointed at internal addresses, and it is limited to 30 lookups per IP per ten minutes.","tags":["public-data"],"x-agent-callable":true,"parameters":[{"name":"url","in":"query","required":true,"description":"The site to look at. A bare domain is fine, https is added. Anything that is not a public https origin is refused with a 422.","schema":{"type":"string"}}],"responses":{"200":{"description":"What the page said about itself.","content":{"application/json":{"schema":{"type":"object","description":"What could be read off the page. Every field but `url` is null when the site hides it, and that is a 200, not an error: a site with no metadata should leave a form empty rather than block it.","properties":{"url":{"type":"string","format":"uri","description":"The https URL actually reached, after redirects."},"productName":{"type":["string","null"]},"tagline":{"type":["string","null"]},"logoUrl":{"type":["string","null"],"format":"uri"}},"required":["url","productName","tagline","logoUrl"]}}}},"400":{"description":"No `url` query parameter was given.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"x-error-code":{"description":"The same stable token the JSON routes put in `code`. Present on every failure, including the ones that answer with plain text.","schema":{"type":"string","enum":["malformed_body","invalid_field","missing_parameter","amount_below_floor","not_found","forbidden","conflict","unprocessable","payload_too_large","unsupported_media_type","rate_limited","invalid_signature","upstream_failure","internal_error"]}}}},"422":{"description":"The URL is not a public https website.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"x-error-code":{"description":"The same stable token the JSON routes put in `code`. Present on every failure, including the ones that answer with plain text.","schema":{"type":"string","enum":["malformed_body","invalid_field","missing_parameter","amount_below_floor","not_found","forbidden","conflict","unprocessable","payload_too_large","unsupported_media_type","rate_limited","invalid_signature","upstream_failure","internal_error"]}}}},"429":{"description":"Rate limit reached for this IP.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"x-error-code":{"description":"The same stable token the JSON routes put in `code`. Present on every failure, including the ones that answer with plain text.","schema":{"type":"string","enum":["malformed_body","invalid_field","missing_parameter","amount_below_floor","not_found","forbidden","conflict","unprocessable","payload_too_large","unsupported_media_type","rate_limited","invalid_signature","upstream_failure","internal_error"]}},"retry-after":{"description":"Whole seconds to wait before retrying. Present on every rate limited response, and repeated in the body as `details.retryAfterSeconds` for callers that log bodies rather than headers.","schema":{"type":"string"}}}}}}},"/api/challenger":{"post":{"operationId":"createFreeListing","summary":"List a product on a board for free","description":"Use this when a product should appear on a board without paying. It creates nothing public on its own: the submission sits pending until our badge is found on the product's own site, which is the entire spam gate, and the response hands back the token that badge has to carry. A free listing never outranks a paid one and gets no link. Only submit a product you control, since the email address given here is the one told when the badge goes missing. Limited to five submissions per IP per day.","tags":["submissions"],"x-agent-callable":true,"requestBody":{"required":true,"description":"The listing fields plus the board to join.","content":{"application/json":{"schema":{"type":"object","properties":{"productName":{"type":"string","description":"Displayed on the board."},"productUrl":{"type":"string","format":"uri","description":"Public https homepage. A bare domain is accepted and upgraded to https. Credentials, private hosts and IP literals are refused."},"tagline":{"type":"string","description":"Optional one line under the name."},"email":{"type":"string","format":"email","description":"Never displayed. Used only to tell the owner what happened to their listing."},"categorySlug":{"type":"string","description":"Slug of a live board, as returned by listAnswers."}},"required":["productName","productUrl","email","categorySlug"]}}}},"responses":{"200":{"description":"Submission accepted and pending badge verification.","content":{"application/json":{"schema":{"type":"object","properties":{"listingId":{"type":"string"},"badgeToken":{"type":"string","description":"Goes in the badge markup. Re-submitting the same domain mints a fresh one."},"categorySlug":{"type":"string"},"categoryName":{"type":"string"}},"required":["listingId","badgeToken","categorySlug","categoryName"]}}}},"400":{"description":"The body was not JSON, a field failed validation, or the URL had no readable domain.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"x-error-code":{"description":"The same stable token the JSON routes put in `code`. Present on every failure, including the ones that answer with plain text.","schema":{"type":"string","enum":["malformed_body","invalid_field","missing_parameter","amount_below_floor","not_found","forbidden","conflict","unprocessable","payload_too_large","unsupported_media_type","rate_limited","invalid_signature","upstream_failure","internal_error"]}}}},"403":{"description":"That domain is blocked from this site.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"x-error-code":{"description":"The same stable token the JSON routes put in `code`. Present on every failure, including the ones that answer with plain text.","schema":{"type":"string","enum":["malformed_body","invalid_field","missing_parameter","amount_below_floor","not_found","forbidden","conflict","unprocessable","payload_too_large","unsupported_media_type","rate_limited","invalid_signature","upstream_failure","internal_error"]}}}},"404":{"description":"No live board has that slug.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"x-error-code":{"description":"The same stable token the JSON routes put in `code`. Present on every failure, including the ones that answer with plain text.","schema":{"type":"string","enum":["malformed_body","invalid_field","missing_parameter","amount_below_floor","not_found","forbidden","conflict","unprocessable","payload_too_large","unsupported_media_type","rate_limited","invalid_signature","upstream_failure","internal_error"]}}}},"409":{"description":"That domain already holds a paid listing on this board, which ranks above every free one.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"x-error-code":{"description":"The same stable token the JSON routes put in `code`. Present on every failure, including the ones that answer with plain text.","schema":{"type":"string","enum":["malformed_body","invalid_field","missing_parameter","amount_below_floor","not_found","forbidden","conflict","unprocessable","payload_too_large","unsupported_media_type","rate_limited","invalid_signature","upstream_failure","internal_error"]}}}},"429":{"description":"Five free listings from this IP in a day is the limit.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"x-error-code":{"description":"The same stable token the JSON routes put in `code`. Present on every failure, including the ones that answer with plain text.","schema":{"type":"string","enum":["malformed_body","invalid_field","missing_parameter","amount_below_floor","not_found","forbidden","conflict","unprocessable","payload_too_large","unsupported_media_type","rate_limited","invalid_signature","upstream_failure","internal_error"]}},"retry-after":{"description":"Whole seconds to wait before retrying. Present on every rate limited response, and repeated in the body as `details.retryAfterSeconds` for callers that log bodies rather than headers.","schema":{"type":"string"}}}}}}},"/api/category/request":{"post":{"operationId":"requestCategory","summary":"Create a new board for free","description":"Call this to open a category this site does not have yet. There is no queue: a name that passes validation goes live immediately as an empty board that anybody, including the requester, can then list on. It buys no listing and no position, so it is not a way to get a product onto the site. Limited to two per IP per hour, counted only after the name validates.","tags":["submissions"],"x-agent-callable":true,"requestBody":{"required":true,"description":"The category name, and optionally an address to tell when it goes live.","content":{"application/json":{"schema":{"type":"object","properties":{"categoryName":{"type":"string","description":"Plain words describing a product category, not a company, a person or a URL. Three characters or more."},"email":{"type":"string","format":"email","description":"Optional. Never displayed."}},"required":["categoryName"]}}}},"responses":{"200":{"description":"The board is live and empty.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"slug":{"type":"string","description":"Slug of the new board, usable at /throne/{slug}."},"message":{"type":"string"}},"required":["ok","slug","message"]}}}},"400":{"description":"The body was not JSON, or the name failed length validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"x-error-code":{"description":"The same stable token the JSON routes put in `code`. Present on every failure, including the ones that answer with plain text.","schema":{"type":"string","enum":["malformed_body","invalid_field","missing_parameter","amount_below_floor","not_found","forbidden","conflict","unprocessable","payload_too_large","unsupported_media_type","rate_limited","invalid_signature","upstream_failure","internal_error"]}}}},"409":{"description":"That board already exists. `details.slug` names the board that took the name, so the caller can link to it without a second lookup.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"x-error-code":{"description":"The same stable token the JSON routes put in `code`. Present on every failure, including the ones that answer with plain text.","schema":{"type":"string","enum":["malformed_body","invalid_field","missing_parameter","amount_below_floor","not_found","forbidden","conflict","unprocessable","payload_too_large","unsupported_media_type","rate_limited","invalid_signature","upstream_failure","internal_error"]}}}},"422":{"description":"The name is not an acceptable category name: a URL, a handle, or blocked wording.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"x-error-code":{"description":"The same stable token the JSON routes put in `code`. Present on every failure, including the ones that answer with plain text.","schema":{"type":"string","enum":["malformed_body","invalid_field","missing_parameter","amount_below_floor","not_found","forbidden","conflict","unprocessable","payload_too_large","unsupported_media_type","rate_limited","invalid_signature","upstream_failure","internal_error"]}}}},"429":{"description":"Two suggestions per IP per hour is the limit.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"x-error-code":{"description":"The same stable token the JSON routes put in `code`. Present on every failure, including the ones that answer with plain text.","schema":{"type":"string","enum":["malformed_body","invalid_field","missing_parameter","amount_below_floor","not_found","forbidden","conflict","unprocessable","payload_too_large","unsupported_media_type","rate_limited","invalid_signature","upstream_failure","internal_error"]}},"retry-after":{"description":"Whole seconds to wait before retrying. Present on every rate limited response, and repeated in the body as `details.retryAfterSeconds` for callers that log bodies rather than headers.","schema":{"type":"string"}}}},"500":{"description":"The category could not be written.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"x-error-code":{"description":"The same stable token the JSON routes put in `code`. Present on every failure, including the ones that answer with plain text.","schema":{"type":"string","enum":["malformed_body","invalid_field","missing_parameter","amount_below_floor","not_found","forbidden","conflict","unprocessable","payload_too_large","unsupported_media_type","rate_limited","invalid_signature","upstream_failure","internal_error"]}}}}}}},"/api/checkout/throne":{"post":{"operationId":"startThroneCheckout","summary":"Open a paid checkout for a position on a board","description":"Not for agent use. This opens a real payment session with our payment provider and returns a URL where a human enters card details, so calling it on somebody's behalf commits them to a purchase they have not seen. No credential is needed, which is exactly why it is rate limited to ten per IP per ten minutes. Nothing is written to any board here: a position exists only once money clears and the provider's signed webhook tells us so. If you want the price of a position without buying one, read priceToChangeCents from listAnswers instead.","tags":["payments"],"x-agent-callable":false,"requestBody":{"required":true,"description":"The listing fields, the board, and the amount in US cents.","content":{"application/json":{"schema":{"type":"object","properties":{"productName":{"type":"string","description":"Displayed on the board."},"productUrl":{"type":"string","format":"uri","description":"Public https homepage. A bare domain is accepted and upgraded to https. Credentials, private hosts and IP literals are refused."},"tagline":{"type":"string","description":"Optional one line under the name."},"email":{"type":"string","format":"email","description":"Never displayed. Used only to tell the owner what happened to their listing."},"logoUrl":{"type":"string","format":"uri","description":"Optional. Usually a /api/logo URL."},"categorySlug":{"type":"string"},"amountCents":{"type":"integer","minimum":1,"description":"Whole US cents. Anything at or above the floor buys a position somewhere."},"defenseBudgetCents":{"type":"integer","description":"Optional ceiling authorising automatic re-purchase of first place. Must be at least amountCents. Honoured only where auto-defend is switched on."}},"required":["productName","productUrl","email","categorySlug","amountCents"]}}}},"responses":{"200":{"description":"A checkout session was opened. Nothing has been charged yet.","content":{"application/json":{"schema":{"type":"object","properties":{"checkoutUrl":{"type":"string","format":"uri","description":"Where the buyer completes payment."},"priceToTakeTopCents":{"type":"integer","description":"What first place cost at session time."},"minListingCents":{"type":"integer","description":"The floor price for any position."}},"required":["checkoutUrl","priceToTakeTopCents","minListingCents"]}}}},"400":{"description":"The body was not JSON, a field failed validation, or the amount is under the floor. The under-the-floor case has `code` of `amount_below_floor` and puts `minListingCents` and `priceToTakeTopCents` in `details`, which is enough to correct the amount and resend without asking anything else.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"x-error-code":{"description":"The same stable token the JSON routes put in `code`. Present on every failure, including the ones that answer with plain text.","schema":{"type":"string","enum":["malformed_body","invalid_field","missing_parameter","amount_below_floor","not_found","forbidden","conflict","unprocessable","payload_too_large","unsupported_media_type","rate_limited","invalid_signature","upstream_failure","internal_error"]}}}},"403":{"description":"That domain is blocked from buying here.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"x-error-code":{"description":"The same stable token the JSON routes put in `code`. Present on every failure, including the ones that answer with plain text.","schema":{"type":"string","enum":["malformed_body","invalid_field","missing_parameter","amount_below_floor","not_found","forbidden","conflict","unprocessable","payload_too_large","unsupported_media_type","rate_limited","invalid_signature","upstream_failure","internal_error"]}}}},"404":{"description":"No board has that slug.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"x-error-code":{"description":"The same stable token the JSON routes put in `code`. Present on every failure, including the ones that answer with plain text.","schema":{"type":"string","enum":["malformed_body","invalid_field","missing_parameter","amount_below_floor","not_found","forbidden","conflict","unprocessable","payload_too_large","unsupported_media_type","rate_limited","invalid_signature","upstream_failure","internal_error"]}}}},"429":{"description":"Ten checkouts per IP per ten minutes is the limit.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"x-error-code":{"description":"The same stable token the JSON routes put in `code`. Present on every failure, including the ones that answer with plain text.","schema":{"type":"string","enum":["malformed_body","invalid_field","missing_parameter","amount_below_floor","not_found","forbidden","conflict","unprocessable","payload_too_large","unsupported_media_type","rate_limited","invalid_signature","upstream_failure","internal_error"]}},"retry-after":{"description":"Whole seconds to wait before retrying. Present on every rate limited response, and repeated in the body as `details.retryAfterSeconds` for callers that log bodies rather than headers.","schema":{"type":"string"}}}},"502":{"description":"The payment provider would not open a session. Nothing was charged.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"x-error-code":{"description":"The same stable token the JSON routes put in `code`. Present on every failure, including the ones that answer with plain text.","schema":{"type":"string","enum":["malformed_body","invalid_field","missing_parameter","amount_below_floor","not_found","forbidden","conflict","unprocessable","payload_too_large","unsupported_media_type","rate_limited","invalid_signature","upstream_failure","internal_error"]}}}}}}},"/api/checkout/category":{"post":{"operationId":"startCategoryCheckout","summary":"Open a paid checkout for a new board with your product on it","description":"Not for agent use, for the same reason as the throne checkout: it opens a real payment session and returns a URL a human has to complete. No credential is needed and it is limited to three per IP per hour. The flat fee buys the moderation queue plus first place on the board if it is approved, and a rejection is refunded automatically. The free equivalent, which buys nothing and creates the board immediately, is requestCategory.","tags":["payments"],"x-agent-callable":false,"requestBody":{"required":true,"description":"The listing fields plus the name of the category being proposed.","content":{"application/json":{"schema":{"type":"object","properties":{"productName":{"type":"string","description":"Displayed on the board."},"productUrl":{"type":"string","format":"uri","description":"Public https homepage. A bare domain is accepted and upgraded to https. Credentials, private hosts and IP literals are refused."},"tagline":{"type":"string","description":"Optional one line under the name."},"email":{"type":"string","format":"email","description":"Never displayed. Used only to tell the owner what happened to their listing."},"logoUrl":{"type":"string","format":"uri","description":"Optional."},"categoryName":{"type":"string","description":"The category to create."}},"required":["productName","productUrl","email","categoryName"]}}}},"responses":{"200":{"description":"A checkout session was opened. Nothing has been charged yet.","content":{"application/json":{"schema":{"type":"object","properties":{"checkoutUrl":{"type":"string","format":"uri"},"priceCents":{"type":"integer","description":"The flat fee in US cents."},"price":{"type":"string","description":"The same fee formatted for display."}},"required":["checkoutUrl","priceCents","price"]}}}},"400":{"description":"The body was not JSON, or a field failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"x-error-code":{"description":"The same stable token the JSON routes put in `code`. Present on every failure, including the ones that answer with plain text.","schema":{"type":"string","enum":["malformed_body","invalid_field","missing_parameter","amount_below_floor","not_found","forbidden","conflict","unprocessable","payload_too_large","unsupported_media_type","rate_limited","invalid_signature","upstream_failure","internal_error"]}}}},"403":{"description":"That domain is blocked from buying here.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"x-error-code":{"description":"The same stable token the JSON routes put in `code`. Present on every failure, including the ones that answer with plain text.","schema":{"type":"string","enum":["malformed_body","invalid_field","missing_parameter","amount_below_floor","not_found","forbidden","conflict","unprocessable","payload_too_large","unsupported_media_type","rate_limited","invalid_signature","upstream_failure","internal_error"]}}}},"409":{"description":"A board with that name already exists. `details.slug` names it.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"x-error-code":{"description":"The same stable token the JSON routes put in `code`. Present on every failure, including the ones that answer with plain text.","schema":{"type":"string","enum":["malformed_body","invalid_field","missing_parameter","amount_below_floor","not_found","forbidden","conflict","unprocessable","payload_too_large","unsupported_media_type","rate_limited","invalid_signature","upstream_failure","internal_error"]}}}},"422":{"description":"The proposed name is not an acceptable category name.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"x-error-code":{"description":"The same stable token the JSON routes put in `code`. Present on every failure, including the ones that answer with plain text.","schema":{"type":"string","enum":["malformed_body","invalid_field","missing_parameter","amount_below_floor","not_found","forbidden","conflict","unprocessable","payload_too_large","unsupported_media_type","rate_limited","invalid_signature","upstream_failure","internal_error"]}}}},"429":{"description":"Three submissions per IP per hour is the limit.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"x-error-code":{"description":"The same stable token the JSON routes put in `code`. Present on every failure, including the ones that answer with plain text.","schema":{"type":"string","enum":["malformed_body","invalid_field","missing_parameter","amount_below_floor","not_found","forbidden","conflict","unprocessable","payload_too_large","unsupported_media_type","rate_limited","invalid_signature","upstream_failure","internal_error"]}},"retry-after":{"description":"Whole seconds to wait before retrying. Present on every rate limited response, and repeated in the body as `details.retryAfterSeconds` for callers that log bodies rather than headers.","schema":{"type":"string"}}}},"502":{"description":"The payment provider would not open a session. Nothing was charged.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"x-error-code":{"description":"The same stable token the JSON routes put in `code`. Present on every failure, including the ones that answer with plain text.","schema":{"type":"string","enum":["malformed_body","invalid_field","missing_parameter","amount_below_floor","not_found","forbidden","conflict","unprocessable","payload_too_large","unsupported_media_type","rate_limited","invalid_signature","upstream_failure","internal_error"]}}}}}}},"/api/logo/upload":{"post":{"operationId":"uploadLogo","summary":"Store an image for use on a listing","description":"Not for agent use. It takes no credential, but it writes image bytes into our media store and exists only so a person filling in a listing form in a browser can attach a logo. The body is multipart form data with one `file` field, capped at 2MB, and the type is decided by sniffing magic bytes rather than by the filename, so SVG is refused outright. Limited to ten uploads per IP per ten minutes. The returned URL is what the checkout routes accept as logoUrl.","tags":["media"],"x-agent-callable":false,"requestBody":{"required":true,"description":"Multipart form data carrying one image under the field name `file`.","content":{"multipart/form-data":{"schema":{"type":"object","properties":{"file":{"type":"string","format":"binary","description":"PNG, JPEG, WebP or GIF, 2MB or less."}},"required":["file"]}}}},"responses":{"200":{"description":"Stored, or already stored: the id is a hash of the bytes.","content":{"application/json":{"schema":{"type":"object","properties":{"logoUrl":{"type":"string","format":"uri","description":"Absolute /api/logo URL for the stored image."}},"required":["logoUrl"]}}}},"400":{"description":"The body was not multipart form data, no `file` field was attached, or the file was empty.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"x-error-code":{"description":"The same stable token the JSON routes put in `code`. Present on every failure, including the ones that answer with plain text.","schema":{"type":"string","enum":["malformed_body","invalid_field","missing_parameter","amount_below_floor","not_found","forbidden","conflict","unprocessable","payload_too_large","unsupported_media_type","rate_limited","invalid_signature","upstream_failure","internal_error"]}}}},"413":{"description":"The file is over 2MB.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"x-error-code":{"description":"The same stable token the JSON routes put in `code`. Present on every failure, including the ones that answer with plain text.","schema":{"type":"string","enum":["malformed_body","invalid_field","missing_parameter","amount_below_floor","not_found","forbidden","conflict","unprocessable","payload_too_large","unsupported_media_type","rate_limited","invalid_signature","upstream_failure","internal_error"]}}}},"415":{"description":"The bytes are not PNG, JPEG, WebP or GIF. SVG is refused deliberately.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"x-error-code":{"description":"The same stable token the JSON routes put in `code`. Present on every failure, including the ones that answer with plain text.","schema":{"type":"string","enum":["malformed_body","invalid_field","missing_parameter","amount_below_floor","not_found","forbidden","conflict","unprocessable","payload_too_large","unsupported_media_type","rate_limited","invalid_signature","upstream_failure","internal_error"]}}}},"429":{"description":"Ten uploads per IP per ten minutes is the limit.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"x-error-code":{"description":"The same stable token the JSON routes put in `code`. Present on every failure, including the ones that answer with plain text.","schema":{"type":"string","enum":["malformed_body","invalid_field","missing_parameter","amount_below_floor","not_found","forbidden","conflict","unprocessable","payload_too_large","unsupported_media_type","rate_limited","invalid_signature","upstream_failure","internal_error"]}},"retry-after":{"description":"Whole seconds to wait before retrying. Present on every rate limited response, and repeated in the body as `details.retryAfterSeconds` for callers that log bodies rather than headers.","schema":{"type":"string"}}}},"502":{"description":"The image could not be stored.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"x-error-code":{"description":"The same stable token the JSON routes put in `code`. Present on every failure, including the ones that answer with plain text.","schema":{"type":"string","enum":["malformed_body","invalid_field","missing_parameter","amount_below_floor","not_found","forbidden","conflict","unprocessable","payload_too_large","unsupported_media_type","rate_limited","invalid_signature","upstream_failure","internal_error"]}}}}}}},"/api/logo":{"get":{"operationId":"getLogo","summary":"Serve a stored logo","description":"Not for agent use: it answers with image bytes, and the only ids that resolve are ones this site minted itself and printed in a listing. It is deliberately not a general image proxy, so there is no way to point it at a host of your choosing. An unknown id is a plain text 404, not JSON, so the card on the board falls back to its lettered mark.","tags":["media"],"x-agent-callable":false,"parameters":[{"name":"id","in":"query","required":true,"description":"64 hex characters: the sha256 content id minted when the image was stored.","schema":{"type":"string"}}],"responses":{"200":{"description":"The stored image, immutable because the id is a content hash.","content":{"image/png":{"schema":{"type":"string","format":"binary"}},"image/jpeg":{"schema":{"type":"string","format":"binary"}},"image/webp":{"schema":{"type":"string","format":"binary"}},"image/gif":{"schema":{"type":"string","format":"binary"}}}},"404":{"description":"Malformed id, or nothing stored under it. JSON, unlike the /api/og cards: a logo URL is handed out by uploadLogo, so the caller that received it from a JSON call gets JSON back when it stops resolving.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"x-error-code":{"description":"The same stable token the JSON routes put in `code`. Present on every failure, including the ones that answer with plain text.","schema":{"type":"string","enum":["malformed_body","invalid_field","missing_parameter","amount_below_floor","not_found","forbidden","conflict","unprocessable","payload_too_large","unsupported_media_type","rate_limited","invalid_signature","upstream_failure","internal_error"]}}}}}}},"/api/md/{path}":{"get":{"operationId":"getMarkdownView","summary":"The markdown representation of a page","description":"Not for direct use. Nothing links here and this path is not the address of anything: a rewrite sends a request to this handler only when it arrives at an ordinary page URL carrying `Accept: text/markdown`. To read a page as markdown, request the page itself with that Accept header and let the rewrite do this. Responses carry `Vary: Accept`, and a path with no markdown view is an honest 404 with a markdown body pointing elsewhere.","tags":["discovery"],"x-agent-callable":false,"parameters":[{"name":"path","in":"path","required":true,"description":"The page path with no leading slash. Empty means the home page.","schema":{"type":"string"}}],"responses":{"200":{"description":"The page as markdown.","content":{"text/markdown":{"schema":{"type":"string"}}}},"404":{"description":"No markdown view for that path. The body is markdown naming what does exist.","content":{"text/markdown":{"schema":{"type":"string"}}}}}}},"/api/og/site":{"get":{"operationId":"getSiteOgImage","summary":"Site-wide social card","description":"Not for agent use: the response is a PNG, not data. It exists so a link to this site unfurls with the premise and three live numbers on it. The numbers it prints come from listAnswers and the money page, which are the readable versions.","tags":["media"],"x-agent-callable":false,"responses":{"200":{"description":"A 1200x630 PNG. Always renders, falling back to a data-free card.","content":{"image/png":{"schema":{"type":"string","format":"binary"}}}}}}},"/api/og/throne/{slug}":{"get":{"operationId":"getThroneOgImage","summary":"Social card for one board","description":"Not for agent use: the response is a PNG, not data. An unknown slug is still a 200 with the generic card rather than a 404, because a broken preview on a shared link is worse than a wrong one. For the same board as JSON, read listAnswers.","tags":["media"],"x-agent-callable":false,"parameters":[{"name":"slug","in":"path","required":true,"description":"Board slug, as returned by listAnswers.","schema":{"type":"string"}}],"responses":{"200":{"description":"A 1200x630 PNG. Unknown slugs get the generic card.","content":{"image/png":{"schema":{"type":"string","format":"binary"}}}},"500":{"description":"Both the card and its fallback failed to render. The body is the plain text `Image unavailable`; the reason is in `x-error-code`.","content":{"text/plain":{"schema":{"type":"string"}}},"headers":{"x-error-code":{"description":"The same stable token the JSON routes put in `code`. Present on every failure, including the ones that answer with plain text.","schema":{"type":"string","enum":["malformed_body","invalid_field","missing_parameter","amount_below_floor","not_found","forbidden","conflict","unprocessable","payload_too_large","unsupported_media_type","rate_limited","invalid_signature","upstream_failure","internal_error"]}}}}}}},"/api/og/listing/{listingId}":{"get":{"operationId":"getListingOgImage","summary":"Social card for one paid listing","description":"Not for agent use: the response is a PNG, not data. It renders one listing's receipt, and an id that resolves to nothing still returns 200 with a placeholder card so a shared link never previews broken.","tags":["media"],"x-agent-callable":false,"parameters":[{"name":"listingId","in":"path","required":true,"description":"Listing id, as printed on a receipt page.","schema":{"type":"string"}}],"responses":{"200":{"description":"A 1200x630 PNG. Unknown ids get a placeholder card.","content":{"image/png":{"schema":{"type":"string","format":"binary"}}}},"500":{"description":"Both the card and its fallback failed to render. The body is the plain text `Image unavailable`; the reason is in `x-error-code`.","content":{"text/plain":{"schema":{"type":"string"}}},"headers":{"x-error-code":{"description":"The same stable token the JSON routes put in `code`. Present on every failure, including the ones that answer with plain text.","schema":{"type":"string","enum":["malformed_body","invalid_field","missing_parameter","amount_below_floor","not_found","forbidden","conflict","unprocessable","payload_too_large","unsupported_media_type","rate_limited","invalid_signature","upstream_failure","internal_error"]}}}}}}},"/api/og/scan/{domain}":{"get":{"operationId":"getScanOgImage","summary":"Social card for a site scan result","description":"Not for agent use: the response is a PNG, not data. Unlike the other cards this one 404s when no scan is on record for the domain, because inventing a grade for a site nobody has scanned would be worse than an absent image.","tags":["media"],"x-agent-callable":false,"parameters":[{"name":"domain","in":"path","required":true,"description":"The scanned domain, already URL-decoded by the router.","schema":{"type":"string"}}],"responses":{"200":{"description":"A 1200x630 PNG carrying the grade and the headline finding.","content":{"image/png":{"schema":{"type":"string","format":"binary"}}}},"404":{"description":"No scan on record for that domain. Plain text rather than JSON, because this URL only ever appears in a meta tag and its caller cannot read a body.","content":{"text/plain":{"schema":{"type":"string"}}},"headers":{"x-error-code":{"description":"The same stable token the JSON routes put in `code`. Present on every failure, including the ones that answer with plain text.","schema":{"type":"string","enum":["malformed_body","invalid_field","missing_parameter","amount_below_floor","not_found","forbidden","conflict","unprocessable","payload_too_large","unsupported_media_type","rate_limited","invalid_signature","upstream_failure","internal_error"]}}}},"500":{"description":"Both the card and its fallback failed to render. The body is the plain text `Image unavailable`; the reason is in `x-error-code`.","content":{"text/plain":{"schema":{"type":"string"}}},"headers":{"x-error-code":{"description":"The same stable token the JSON routes put in `code`. Present on every failure, including the ones that answer with plain text.","schema":{"type":"string","enum":["malformed_body","invalid_field","missing_parameter","amount_below_floor","not_found","forbidden","conflict","unprocessable","payload_too_large","unsupported_media_type","rate_limited","invalid_signature","upstream_failure","internal_error"]}}}}}}},"/api/webhooks/dodo":{"post":{"operationId":"receiveDodoWebhook","summary":"Payment provider callback","description":"Closed. This is the only place money reaches a board, and it accepts a delivery only when the body carries a valid HMAC signature over the exact bytes sent, in the `webhook-id`, `webhook-timestamp` and `webhook-signature` headers, signed with a secret held by our payment provider and this server. Nobody else can produce one, so it is not callable by an agent under any circumstances and is documented here only so the surface is complete. Every delivery is claimed by id before any work runs, so a retry cannot count the same payment twice.","tags":["restricted"],"x-agent-callable":false,"requestBody":{"required":true,"description":"The raw provider event. It is verified as bytes, so it is never re-serialised before checking.","content":{"application/json":{"schema":{"type":"object","description":"A payment provider event envelope.","additionalProperties":true}}}},"responses":{"200":{"description":"Understood, including the cases of nothing to do and already seen.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"deduped":{"type":"boolean","description":"Present and true when this delivery had already been handled."}},"required":["ok"]}}}},"400":{"description":"The delivery had no `webhook-id` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"x-error-code":{"description":"The same stable token the JSON routes put in `code`. Present on every failure, including the ones that answer with plain text.","schema":{"type":"string","enum":["malformed_body","invalid_field","missing_parameter","amount_below_floor","not_found","forbidden","conflict","unprocessable","payload_too_large","unsupported_media_type","rate_limited","invalid_signature","upstream_failure","internal_error"]}}}},"401":{"description":"The signature did not verify.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"x-error-code":{"description":"The same stable token the JSON routes put in `code`. Present on every failure, including the ones that answer with plain text.","schema":{"type":"string","enum":["malformed_body","invalid_field","missing_parameter","amount_below_floor","not_found","forbidden","conflict","unprocessable","payload_too_large","unsupported_media_type","rate_limited","invalid_signature","upstream_failure","internal_error"]}}}},"500":{"description":"A transient fault. The provider should retry this delivery.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"x-error-code":{"description":"The same stable token the JSON routes put in `code`. Present on every failure, including the ones that answer with plain text.","schema":{"type":"string","enum":["malformed_body","invalid_field","missing_parameter","amount_below_floor","not_found","forbidden","conflict","unprocessable","payload_too_large","unsupported_media_type","rate_limited","invalid_signature","upstream_failure","internal_error"]}}}}}}},"/api/cron/verify-badges":{"get":{"operationId":"runHourlyJobs","summary":"Hourly badge checks and checkout reminders","description":"Closed. It runs the badge verification pass and the one abandoned-checkout email, and it requires the CRON_SECRET shared with our scheduler, sent as `Authorization: Bearer` or `?key=`. Not callable by an agent. Anyone without the secret gets a 404 rather than a 401, deliberately, so an unauthenticated caller cannot even learn that this endpoint exists, and the route refuses everything when the secret is unset so a deploy that forgets it fails closed.","tags":["restricted"],"x-agent-callable":false,"parameters":[{"name":"key","in":"query","required":false,"description":"The shared secret, for schedulers that cannot set a header. Same value as the bearer token.","schema":{"type":"string"}},{"name":"authorization","in":"header","required":false,"description":"`Bearer <CRON_SECRET>`. Compared in full, in constant time.","schema":{"type":"string"}}],"responses":{"200":{"description":"The run finished. Either half may report its own failure inside the body.","content":{"application/json":{"schema":{"type":"object","properties":{"badges":{"type":"object","description":"Counts from the badge verification pass, or `{ \"error\": \"failed\" }` when that half threw.","additionalProperties":true},"checkouts":{"type":"object","description":"Counts from the abandoned-checkout mailer, or `{ \"error\": \"failed\" }` when that half threw.","additionalProperties":true}},"required":["badges","checkouts"]}}}},"404":{"description":"No secret, a wrong secret, or no secret configured on the server. This is what an unauthorised caller always sees, and the body says nothing a genuinely missing path would not say.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"x-error-code":{"description":"The same stable token the JSON routes put in `code`. Present on every failure, including the ones that answer with plain text.","schema":{"type":"string","enum":["malformed_body","invalid_field","missing_parameter","amount_below_floor","not_found","forbidden","conflict","unprocessable","payload_too_large","unsupported_media_type","rate_limited","invalid_signature","upstream_failure","internal_error"]}}}},"500":{"description":"Both halves of the run failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"x-error-code":{"description":"The same stable token the JSON routes put in `code`. Present on every failure, including the ones that answer with plain text.","schema":{"type":"string","enum":["malformed_body","invalid_field","missing_parameter","amount_below_floor","not_found","forbidden","conflict","unprocessable","payload_too_large","unsupported_media_type","rate_limited","invalid_signature","upstream_failure","internal_error"]}}}}}}}},"components":{"schemas":{"Error":{"type":"object","description":"The failure shape shared by every JSON route here, with no per-route variations. `error` is a sentence written for a person, so it is safe to show a user and unsafe to switch on. `code` is the stable token to branch on. `details` is present only when there is something that would let you fix the request and send it again, and its keys are documented on the operation that sends them. The image routes under /api/og answer in plain text instead, because their callers cannot read a body at all; they carry the same code in the `x-error-code` header, which every route here sets.","properties":{"error":{"type":"string","description":"Human readable reason the request failed. Reworded freely, so never switch on it."},"code":{"type":"string","description":"Stable machine-readable reason. This is the field to branch on.","enum":["malformed_body","invalid_field","missing_parameter","amount_below_floor","not_found","forbidden","conflict","unprocessable","payload_too_large","unsupported_media_type","rate_limited","invalid_signature","upstream_failure","internal_error"]},"details":{"type":"object","description":"Resolution hints: what the caller would need to fix the request. Absent when there is nothing useful to say.","additionalProperties":true}},"required":["error","code"],"additionalProperties":false}}}}