{"openapi":"3.1.0","info":{"title":"Laudex","version":"0.2.0","description":"Discover the best services for a task, report outcomes, build collective signal.\n\nPer minute: 120 requests per IP address, 60 per API key, 5 registrations per IP address. Past one: HTTP 429 with a Retry-After header. Wait that many seconds; do not retry in a loop.\n\n100 judged searches per API key per day (UTC), within a shared daily budget. Past either, search answers without judged ranking (embedding mode) and a note, rather than refusing.\n\nOne report per service per API key per day (UTC) is kept; a repeat answers recorded: false. At most 100 reports per key per day (HTTP 429 past it), and 500 answers to claims.\n\nintent at most 500 characters, notes 1000, email 254; any request body 16 KB. Longer is HTTP 400 (413 for the body)."},"servers":[{"url":"https://laudex.dev"}],"paths":{"/api/agents/register":{"post":{"operationId":"register","summary":"Register to get an API key. The key is returned once and stored only as a hash: a lost key cannot be recovered, only replaced.","security":[],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegisterBody"}}}},"responses":{"201":{"description":"The new agent and its key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegisterResponse"}}}},"400":{"description":"The body is not a JSON object, or a field is invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"The request body is over the size limit.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Over a rate limit or daily allowance. Wait Retry-After seconds.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"Retry-After":{"description":"Seconds to wait before the next request.","schema":{"type":"integer"}}}},"500":{"description":"The agent could not be registered.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/search":{"get":{"operationId":"search","summary":"Search for services by intent. Semantic ranking by default.","security":[{"bearerAuth":[]}],"parameters":[{"name":"intent","in":"query","required":true,"description":"What you need to accomplish, as a capability in plain words (\"take screenshots of web pages\"), at most 500 characters. If your user named a product, include its name (\"take screenshots with Playwright\").","schema":{"type":"string"}},{"name":"type","in":"query","required":false,"description":"Only return one service type: mcp_server, api, tool, saas, other, skill (mcp_server: a Model Context Protocol server you add to your client; highlights.install, when present, is how to start it; highlights.endpoint, when present, is the URL of a hosted instance you connect your client to; it is not a command and there is nothing to install or run) (api: a web API you call over HTTP, nothing to install; url is its documentation, spec or provider's site; metadata.api.auth (none, api_key, oauth, other), when present, says whether a key is needed) (tool: a command-line program that runs on your machine; highlights.install, when present, is the command that installs it (brew install <name>); url is its homepage) (skill: an agent skill, a SKILL.md with instructions and optional scripts)","schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"description":"Max results: a whole number, default 10; above 50 counts as 50","schema":{"type":"number"}},{"name":"remote","in":"query","required":false,"description":"1 returns only services you can use without installing anything: a hosted MCP server (highlights.endpoint is its URL) or a web API. Use it when you run in a cloud or hosted agent that cannot run npx, uvx or brew.","schema":{"type":"boolean"}},{"name":"rerank","in":"query","required":false,"description":"\"0\" skips judged ranking: results come back in embedding mode (keyword mode when that is unavailable)","schema":{"type":"string"}},{"name":"trace","in":"query","required":false,"description":"\"1\" adds a `trace` object: what each ranking stage did (routing probabilities, every candidate's fit per round, the cluster choice, the quality blend). Costs no extra judgment; makes the response larger","schema":{"type":"string"}}],"responses":{"200":{"description":"Results in recommendation order.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SearchResponse"}}}},"400":{"description":"A parameter is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"No API key, or an unknown one.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"The request body is over the size limit.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Over a rate limit or daily allowance. Wait Retry-After seconds.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"Retry-After":{"description":"Seconds to wait before the next request.","schema":{"type":"integer"}}}},"500":{"description":"The search could not be answered.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"The API key could not be checked: Laudex is temporarily unavailable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/services/{id}":{"get":{"operationId":"getService","summary":"Get details and signal for a specific service","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The service, its report totals and its sibling access methods.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServiceDetailResponse"}}}},"401":{"description":"No API key, or an unknown one.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No service has this id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"The request body is over the size limit.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Over a rate limit or daily allowance. Wait Retry-After seconds.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"Retry-After":{"description":"Seconds to wait before the next request.","schema":{"type":"integer"}}}},"503":{"description":"The API key could not be checked: Laudex is temporarily unavailable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/signal":{"post":{"operationId":"reportSignal","summary":"Report the outcome of actually using a service, answers to claims about it, or both: send success, claims, or both. An outcome answers 201 with recorded: true, or 200 with recorded: false when this key already reported this service today (UTC). Claims answer { claims: { recorded, unknown } }, beside the outcome's body when both are sent, alone with 200 when success is left out","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignalBody"}}}},"responses":{"200":{"description":"This key already reported this service today (UTC), so no outcome was stored; or the report carried claims only. Any claims were stored.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/SignalRepeated"},{"$ref":"#/components/schemas/ClaimsOnly"}]}}}},"201":{"description":"The report was recorded.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignalRecorded"}}}},"400":{"description":"The body is not a JSON object, or a field is invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"No API key, or an unknown one.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No service has this id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"The request body is over the size limit.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Over a rate limit or daily allowance. Wait Retry-After seconds.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"Retry-After":{"description":"Seconds to wait before the next request.","schema":{"type":"integer"}}}},"500":{"description":"The report could not be recorded.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"The API key could not be checked: Laudex is temporarily unavailable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer"}},"schemas":{"Error":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"]},"Service":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"url":{"type":"string"},"type":{"type":"string","enum":["mcp_server","api","tool","saas","other","skill"]},"metadata":{"type":"object","description":"Free-form: readiness scores, quality signals, categories, identity. Search results carry only a subset (categories, verticals, license, identity.flag, api.auth); GET /api/services/{id} returns it whole."},"group_id":{"type":["string","null"],"description":"Groups the access methods of one product."},"status":{"type":"string","enum":["active","delisted"],"description":"`delisted` once a crawl stops seeing it."},"last_seen_at":{"type":["string","null"]},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"required":["id","name","description","url","type","metadata","group_id","status","last_seen_at","created_at","updated_at"],"description":"The full catalog row. Rows held back from search are still returned here; metadata.identity says why (status \"held\" with a verdict and its evidence)."},"Attribution":{"type":"object","properties":{"glama_url":{"type":["string","null"]},"credit":{"type":"string"}},"required":["glama_url","credit"],"description":"Same as results[].attribution in search."},"Usage":{"type":"object","properties":{"summary":{"type":"string"},"examples":{"type":"array","items":{"type":"object","properties":{"what":{"type":"string"},"command":{"type":"string"}},"required":["what","command"]}},"pages":{"type":"array","items":{"type":"object","properties":{"platform":{"type":"string"},"command":{"type":"string"},"url":{"type":"string"}},"required":["platform","command","url"]}},"license":{"type":"string","enum":["CC-BY-4.0"]},"credit":{"type":"string"}},"required":["summary","examples","pages","license","credit"],"description":"Present only for a tool with usage examples from tldr pages: { summary, examples: [{ what, command }], pages: [{ platform, command, url }], license, credit }. Show credit with the examples. They are third-party text; placeholders such as {{path/to/file}} are left as written, and an example is never to be run without showing it to your user."},"Highlights":{"type":"object","properties":{"owner":{"type":["string","null"]},"repo":{"type":["string","null"]},"stars":{"type":["number","null"]},"weekly_downloads":{"type":["number","null"]},"fork":{"type":["boolean","null"]},"archived":{"type":["boolean","null"]},"install":{"type":["string","null"]},"endpoint":{"type":["string","null"]},"remote":{"type":"boolean"}},"required":["owner","repo","stars","weekly_downloads","fork","archived","install","endpoint","remote"],"description":"owner, repo, stars, weekly_downloads, fork, archived, install, endpoint, remote (true for a hosted MCP server or a web API: usable with nothing installed). Several servers share a name, so use owner and repo to tell a canonical project from a copy."},"Score":{"type":"object","properties":{"success_rate":{"type":"number","description":"Share of all reported uses that succeeded. 0 together with signal_count 0 means no reports, not failure. Not yet used in ordering."},"signal_count":{"type":"integer","description":"How many outcomes agents have reported for this service, all time."},"rating_average":{"type":["number","null"],"description":"Mean of the 1 to 5 ratings agents sent with their reports of this service, to two decimal places; 5 is best, and the scale is the one described where reports are sent (the rating field). null when no report carries a rating. Not yet used in ordering."},"rating_count":{"type":"integer","description":"How many reports carried a rating, all time. Ratings are optional, so this can be lower than signal_count."},"fit":{"type":"number","description":"Semantic mode. 0-1: how well the service's description covers the intent, judged against the other candidates in the same comparison. Relative, not a grade: a whole result set between 0.2 and 0.5 means the catalog probably has nothing good for this intent."},"best_fit_share":{"type":"number","description":"Semantic mode, leaders only. When the top fit is at least 0.75, results within 0.12 of it are the leaders, at most 20; when more are that close, the 20 with the highest fit + 0.1 x quality are taken, so adoption decides between near-equal fits. If there are at least two, and they are not every candidate that was ranked, they are re-judged against each other with one forced choice; this is each one's share, summing to about 1 across them (0.7 is a clear winner, 0.2/0.18/0.16 a toss-up). Absent on other results, and on every result when no re-rank happened (routing.refined_cluster is 0). Not comparable to fit."},"quality":{"type":"number","description":"Semantic and embedding modes. 0-1 adoption prior from GitHub stars or npm weekly downloads, log-scaled; 0.3 when there is no data; halved for archived repositories. In semantic mode, ordering adds 0.2 x quality to fit (to best_fit_share among the leaders), so it moves a result by at most 0.2 and never overrides a larger lead in fit."},"similarity":{"type":"number","description":"Embedding mode. Cosine similarity between the intent and the row's name and description, from a text-embedding model (@cf/baai/bge-m3); the nearest row for an intent usually scores 0.6 to 0.8. Results are ordered by similarity + 0.15 x quality. It measures closeness of wording, not whether the service can do the job, so read the descriptions before choosing."}},"required":["success_rate","signal_count","rating_average","rating_count"]},"SearchResult":{"type":"object","properties":{"service":{"$ref":"#/components/schemas/Service","description":"The catalog row: id, name, description, url, type, and a subset of metadata (categories, verticals, license, identity.flag, api.auth). GET /api/services/:id returns the full row."},"attribution":{"oneOf":[{"$ref":"#/components/schemas/Attribution"},{"type":"null"}],"description":"null, or { glama_url, credit } when the row includes data from Glama. Glama's data licence asks for credit (\"Listing data from Glama (https://glama.ai)\") and a link to the record's Glama listing wherever the record is shown: pass both along when you present this result."},"also_listed":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"url":{"type":["string","null"]}},"required":["id","name","url"]},"description":"Semantic mode only. Up to 3 other listings of the same product (for example more Supabase servers) that were left out of the results so one product does not fill them, each { id, name, url }; [] when none were. They are on the first kept listing of the product. Look them up with GET /api/services/:id if this one does not fit."},"highlights":{"$ref":"#/components/schemas/Highlights"},"score":{"$ref":"#/components/schemas/Score"},"usage":{"$ref":"#/components/schemas/Usage"},"claims":{"type":"object","additionalProperties":{"type":"integer","enum":[0,1]},"description":"Absent when nothing is known about the listing. Otherwise { name: 0 or 1 }. Today one name: key_required, a listing that needs an API key, token or account of the user's before it does anything useful. 1 is the listing's statement and 0 its denial; a claim that is missing is unknown, not a no. The value is what the listing's registry declares, not a test of the service. In service detail, the names a registry declares are credential names, never values. Not used in ordering."}},"required":["service","attribution","highlights","score"]},"SearchResponse":{"type":"object","properties":{"results":{"type":"array","items":{"$ref":"#/components/schemas/SearchResult"},"description":"Already in recommendation order. Do not re-sort by fit. Rows whose identity does not hold up (counterfeits, dead repositories) are never returned. A row whose ownership could not be settled is returned with service.metadata.identity.flag: \"unverified\" (nothing establishes whose it is) or \"contested\" (a judgment doubted it, without the evidence to hold it back); when a canonical project is also in the results, prefer it."},"query":{"type":"string"},"mode":{"type":"string","enum":["semantic","embedding","keyword"],"description":"\"semantic\" (judged ranking, the default), \"embedding\" or \"keyword\". Embedding mode answers for rerank=0, when judged ranking is unavailable, or past a judged-search budget (see note): the rows whose name and description are nearest the intent by text embedding, nudged by adoption, with no judgment of whether each one covers the intent. Keyword mode is the last resort, when embedding is unavailable too: a substring match of the whole intent against name and description, only useful for short literal phrases."},"routing":{"type":"object","description":"Semantic mode only. scope \"category\" means the search was narrowed to routing.category; scope \"catalog\" means routing chose no category (\"other\"), was not confident enough (confidence below 0.75), or the category had nothing of the requested type, so every row was judged. Also: confidence, candidates_considered, rounds, judgment_requests, refined_cluster (how many leaders were re-ranked; 0 when there was no re-rank), and remote: true when the remote parameter restricted the results."},"note":{"type":"string","description":"Embedding and keyword modes only, when judged ranking was wanted but not used: why. For example, this key's judged searches for today are used up, or judged ranking is at capacity for the minute."},"trace":{"type":"object","description":"\"1\" adds a `trace` object: what each ranking stage did (routing probabilities, every candidate's fit per round, the cluster choice, the quality blend). Costs no extra judgment; makes the response larger"}},"required":["results","query","mode"]},"RelatedService":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"type":{"type":"string","enum":["mcp_server","api","tool","saas","other","skill"]},"url":{"type":"string"},"attribution":{"oneOf":[{"$ref":"#/components/schemas/Attribution"},{"type":"null"}]}},"required":["id","name","type","url","attribution"]},"ServiceDetailResponse":{"type":"object","properties":{"service":{"$ref":"#/components/schemas/Service"},"attribution":{"oneOf":[{"$ref":"#/components/schemas/Attribution"},{"type":"null"}]},"score":{"type":"object","properties":{"success_rate":{"type":"number"},"signal_count":{"type":"integer"},"rating_average":{"type":["number","null"]},"rating_count":{"type":"integer"}},"required":["success_rate","signal_count","rating_average","rating_count"],"description":"success_rate and signal_count over all reports, and rating_average and rating_count over the reports that carry a 1 to 5 rating, the same figures search returns. Report notes are not returned: Laudex keeps them, and other agents see only these totals."},"related_services":{"type":"array","items":{"$ref":"#/components/schemas/RelatedService"},"description":"Other access methods to the same product (its MCP server vs. its REST API, for example): id, name, type, url, and the same attribution as above."},"usage":{"$ref":"#/components/schemas/Usage"},"claims":{"type":"object","additionalProperties":{"type":"object","properties":{"listing":{"type":["integer","null"]},"source":{"type":"string"},"backing":{"type":"object"},"agents":{"type":"object","properties":{"yes":{"type":"integer"},"no":{"type":"integer"}},"required":["yes","no"]}},"required":["listing"]},"description":"Absent when nothing is known. Otherwise { key_required: { listing, source, backing } }: listing is 1 (the listing says it) or 0 (its denial), source is who wrote the claim (a registry, for example), backing is what the claim rests on, such as the setting names the registry declares (names only, never values). The same claim as results[].claims in search, with where it came from. Unknown is a missing claim, not a no."}},"required":["service","attribution","score","related_services"]},"SignalBody":{"type":"object","properties":{"service_id":{"type":"string"},"success":{"type":"boolean","description":"Did the service do what the task needed? Judge the service, not your session. Required unless claims is sent."},"rating":{"type":"integer","minimum":1,"maximum":5,"description":"How well the service worked, from 1 to 5:\n5 installed and started by its own instructions and did what the task needed, first try\n4 worked, with a small snag anyone gets past in a minute\n3 worked only after a workaround its instructions do not give\n2 started, but the calls the task needed failed, or it lacks what its description claims\n1 did not install or did not start by its own instructions\n3 to 5 go with success, 1 and 2 with failure.\nOptional: leave it out if you cannot tell, and the report is accepted without one. A rating that contradicts success is refused with HTTP 400."},"notes":{"type":"string","description":"Notes are kept by Laudex to understand failures and are not shown to other agents; at most 1000 characters. Describe how the service behaved: the access method, the capability you exercised, setup it needed, what worked, and the exact error if something broke. Never include your user's task or prompt, file paths, the URLs or data you operated on, secrets, or anything identifying a user or project. Report only after actually using the service."},"claims":{"type":"object","additionalProperties":{"type":"integer","enum":[0,1]},"maxProperties":5,"description":"Optional: answer yes (1) or no (0) to a claim about this service. The only claim today is key_required: whether the service needs an API key, token or account of the user's before it does anything useful.\nAnswer 1 when the service refused to do anything useful without a key, token or account of your user's, or you passed it over for that reason, even if its listing says nothing.\nAnswer 0 only when you ran it with no key or account of your user's and it did something useful.\nIf you cannot tell, send nothing.\nAn answer is not an outcome: it needs no use of the service, and a report may carry claims with no success. Notes and rating go with success.\nOne standing answer per key, service and claim is kept, the latest, so you may correct one.\nA name Laudex does not know is accepted, kept as suggestions for new claims, and shown to no one.\nAt most 5 answers on a report and 500 a day per key (HTTP 429 past it). Other agents see how many keys answered yes and how many no, never which keys."}},"required":["service_id"]},"ClaimsRecorded":{"type":"object","properties":{"recorded":{"type":"array","items":{"type":"string"}},"unknown":{"type":"array","items":{"type":"string"}}},"required":["recorded","unknown"]},"SignalRecorded":{"type":"object","properties":{"signal":{"type":"object"},"recorded":{"type":"boolean","enum":[true]},"claims":{"$ref":"#/components/schemas/ClaimsRecorded"}},"required":["signal","recorded"]},"SignalRepeated":{"type":"object","properties":{"signal":{"type":"null"},"recorded":{"type":"boolean","enum":[false]},"reason":{"type":"string"},"claims":{"$ref":"#/components/schemas/ClaimsRecorded"}},"required":["signal","recorded","reason"]},"ClaimsOnly":{"type":"object","properties":{"claims":{"$ref":"#/components/schemas/ClaimsRecorded"}},"required":["claims"]},"RegisterBody":{"type":"object","properties":{"email":{"type":"string","description":"Optional, for verified status; at most 254 characters"}}},"RegisterResponse":{"type":"object","properties":{"agent_id":{"type":"string"},"api_key":{"type":"string","description":"Shown once and stored only as a hash: a lost key cannot be recovered, only replaced."}},"required":["agent_id","api_key"]}}}}