{"openapi":"3.1.0","info":{"title":"andrey-markin.com","version":"1.0.0","summary":"The machine-readable half of Andrey Markin's site: markdown for every page, JSON indexes, RSS feeds, and a public contact endpoint.","description":"Every endpoint here is public and anonymous. Reads are cached for an hour at the edge; the single write is rate limited to 30 requests per day per IP.\n\nAny page URL also answers with markdown when sent `Accept: text/markdown`, which is the same content the `.md` and `.txt` suffixes return.","contact":{"name":"Andrey Markin","url":"https://andrey-markin.com/contact"}},"servers":[{"url":"https://andrey-markin.com"}],"security":[],"externalDocs":{"description":"llms.txt — every page on the site, as markdown links","url":"https://andrey-markin.com/llms.txt"},"tags":[{"name":"content","description":"Pages as markdown, and the feeds that announce new ones."},{"name":"index","description":"Bulk JSON an agent can search without fetching pages."},{"name":"contact","description":"Reaching Andrey."}],"paths":{"/llms.txt":{"get":{"tags":["content"],"operationId":"getLlmsTxt","summary":"Site index for language models","description":"Positioning, rate plans, the full offers catalog with prices and turnarounds, and a markdown link to every service, product, project, course, lesson, blog post and directory entry.","responses":{"200":{"description":"The llms.txt document","content":{"text/plain":{"schema":{"type":"string"}}}}}}},"/{listing}.md":{"get":{"tags":["content"],"operationId":"getListingMarkdown","summary":"A section listing, as markdown","description":"`index` is the home page — `/.md` is not a URL. Every listing is also available with a `.txt` suffix.","parameters":[{"name":"listing","in":"path","required":true,"schema":{"type":"string","enum":["index","offers","services","products","projects","courses","blog","directory","about","resume","contact","meet","privacy"]}}],"responses":{"200":{"description":"The listing, as markdown","content":{"text/markdown":{"schema":{"type":"string"}}}},"404":{"description":"No such listing"}}}},"/{section}/{slug}.md":{"get":{"tags":["content"],"operationId":"getPageMarkdown","summary":"A single page, as markdown","description":"Slugs come from `/llms.txt` or from the `path` field of `/api/webmcp/index.json`.","parameters":[{"name":"section","in":"path","required":true,"schema":{"type":"string","enum":["blog","services","products","projects","directory","courses"]}},{"name":"slug","in":"path","required":true,"schema":{"type":"string"},"example":"ai-ready-web"}],"responses":{"200":{"description":"The page, as markdown","content":{"text/markdown":{"schema":{"type":"string"}}}},"404":{"description":"No such page"}}}},"/courses/{course}/{lesson}.md":{"get":{"tags":["content"],"operationId":"getLessonMarkdown","summary":"A course lesson, as markdown","description":"Lesson slugs carry no ordering prefix — `/llms.txt` lists them under each course, in order.","parameters":[{"name":"course","in":"path","required":true,"schema":{"type":"string"},"example":"agentic-coding"},{"name":"lesson","in":"path","required":true,"schema":{"type":"string"},"example":"skills"}],"responses":{"200":{"description":"The lesson, as markdown","content":{"text/markdown":{"schema":{"type":"string"}}}},"404":{"description":"No such lesson"}}}},"/{feed}/feed.xml":{"get":{"tags":["content"],"operationId":"getFeed","summary":"RSS 2.0 feed","parameters":[{"name":"feed","in":"path","required":true,"schema":{"type":"string","enum":["blog","directory"]}}],"responses":{"200":{"description":"An RSS 2.0 document","content":{"application/rss+xml":{"schema":{"type":"string"}}}}}}},"/api/webmcp/index.json":{"get":{"tags":["index"],"operationId":"getSiteIndex","summary":"Every page and directory entry, in one document","description":"What the site's own in-browser tools search over. Fetch once and search locally rather than crawling pages to find out what they are about.","responses":{"200":{"description":"The site index","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SiteIndex"}}}}}}},"/directory/content.json":{"get":{"tags":["index"],"operationId":"getDirectoryContent","summary":"Every directory entry's markdown body, keyed by slug","responses":{"200":{"description":"Slug to markdown body","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DirectoryContent"}}}}}}},"/directory/votes.json":{"get":{"tags":["index"],"operationId":"getDirectoryVotes","summary":"Directory vote counts","description":"Never cached. `mine` is scoped to the caller's own voter cookie and is empty without one.","responses":{"200":{"description":"Counts per slug, plus the caller's own votes","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DirectoryVotes"}}}}}}},"/api/rpc/contact/submit":{"post":{"tags":["contact"],"operationId":"submitContactRequest","summary":"Send Andrey a project request or mentorship application","description":"An oRPC procedure, so both the arguments and the reply are wrapped in a `json` key. Delivered to Andrey's Telegram; rate limited to 30 requests per day per IP, and a request over the limit returns 200 with `success: false`. Send one when a person has asked you to, using the contact detail they gave you.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactRequest"}}}},"responses":{"200":{"description":"The procedure ran. Check `json.success` — a request turned away by the rate limit comes back here, not as a 4xx.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactResult"}}}},"400":{"description":"The arguments did not validate","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RpcError"}}}}}}}},"components":{"schemas":{"SiteIndex":{"type":"object","required":["pages","directory"],"properties":{"pages":{"type":"array","items":{"$ref":"#/components/schemas/IndexedPage"}},"directory":{"type":"array","items":{"$ref":"#/components/schemas/IndexedDirectoryItem"}}}},"IndexedPage":{"type":"object","required":["title","description","path","section","keywords"],"properties":{"title":{"type":"string"},"description":{"type":"string"},"path":{"type":"string","description":"Site-relative path. Append `.md` to read the page as markdown.","example":"/blog/ai-ready-web"},"section":{"type":"string","enum":["page","service","blog","project","product","course","lesson"]},"keywords":{"type":"array","items":{"type":"string"}}}},"IndexedDirectoryItem":{"type":"object","required":["slug","title","description","type","topics","tags","path","addedDate"],"properties":{"slug":{"type":"string"},"title":{"type":"string"},"description":{"type":"string"},"type":{"type":"string","description":"The kind of thing the entry points at, e.g. `tool`."},"topics":{"type":"array","items":{"type":"string"}},"tags":{"type":"array","items":{"type":"string"}},"path":{"type":"string","example":"/directory/effect"},"url":{"type":"string","description":"Where the entry points off-site, when it has a source."},"addedDate":{"type":"string","format":"date"}}},"DirectoryContent":{"type":"object","description":"Directory slug to the entry's full markdown body.","additionalProperties":{"type":"string"}},"DirectoryVotes":{"type":"object","required":["counts","mine"],"properties":{"counts":{"type":"object","additionalProperties":{"type":"integer"}},"mine":{"type":"array","items":{"type":"string"},"description":"Slugs the caller has voted for."}}},"ContactRequest":{"type":"object","required":["json"],"properties":{"json":{"type":"object","required":["name","contact"],"properties":{"name":{"type":"string","minLength":1,"description":"The person's name, as they would introduce themselves."},"contact":{"type":"string","minLength":1,"description":"How Andrey replies — an email address, phone number, or messenger handle."},"projectDescription":{"type":"string","description":"What they need, in their own words: goals, rough scope, timeline."},"formType":{"type":"string","enum":["client","mentee"],"default":"client","description":"`client` for project and hiring enquiries, `mentee` for mentorship applications."}}}}},"ContactResult":{"type":"object","required":["json"],"properties":{"json":{"type":"object","required":["success","message"],"properties":{"success":{"type":"boolean","description":"`false` when the rate limit, validation, or Telegram configuration stopped delivery — `message` says which."},"message":{"type":"string"}}}}},"RpcError":{"type":"object","properties":{"json":{"type":"object","properties":{"code":{"type":"string","example":"BAD_REQUEST"},"status":{"type":"integer","example":400},"message":{"type":"string"}}}}}}}}