API

Everything on this site is data first. These endpoints return the same records the pages are built from. All of them are static JSON generated at build time, except /api/ask, which turns a question into SQL, /api/questions, which passes on a question a visitor chose to send, and /api/stats, which reports the counters.

Rate limits: 10 questions a minute per address, shared by /api/ask and /api/questions, and a monthly cap; when the cap is reached the endpoint returns 503 with reason "budget". Every 429 carries a Retry-After header with the seconds to wait before you try again. /api/questions also returns 429 with error "daily_cap" once the day's 50 sent questions are used up. An answer from /api/ask is cached for 30 days and a refusal for 1 day, keyed by the question's normalized wording, so repeat questions are free. The monthly cap counts model calls, not questions, in the UTC calendar month.

Try it:

curl https://alexkachur.com/api/projects.json

The whole API as an OpenAPI 3.1 document, built from the same schema and endpoint list, for loading into any client: /api/openapi.json

GET /api/projects.json

The projects on this site, in site order

curl https://alexkachur.com/api/projects.json
[
  {
    "id": 1,
    "slug": "splitroof-ai-assistant",
    "page": "/work/splitroof-ai-assistant",
    "name": "SplitRoof AI assistant",
    "kind": "team",
    "summary": "An assistant inside a shared-household expense app that answers question...",
    "role": "AI integration, documentation, schedule",
    "year_start": 2026,
    "year_end": null,
    "client_name": null,
    "live_url": null,
    "repo_url": "https://github.com/AlexKachur98/splitroof-assistant-spike",
    "has_live_demo": 0,
    "uses_llm": 1,
    "llm_job": "answers questions about a household's shared expenses by calling typed t...",
    "paid": 0,
    "featured": 1,
    "card": "A read-only assistant that answers \"who owes what this month\" from a hou...",
    "team": "group project, COMP 231 Software Development Project 1, Fall 2026",
    "highlights": "Own the assistant for a shared-household expense app: a read-only agent ...",
    "row_image": "splitroof-flow",
    "row_image_alt": "How the SplitRoof assistant answers: a question goes to Claude Haiku 4.5..."
  },
  ...
]

GET /api/projects/{slug}.json

The projects row with this slug, plus the names of its technologies. Its screenshots are the project_images rows with its id, at /api/project_images.json.

curl https://alexkachur.com/api/projects/splitroof-ai-assistant.json
{
  "id": 1,
  "slug": "splitroof-ai-assistant",
  "page": "/work/splitroof-ai-assistant",
  "name": "SplitRoof AI assistant",
  "kind": "team",
  "summary": "An assistant inside a shared-household expense app that answers question...",
  "role": "AI integration, documentation, schedule",
  "year_start": 2026,
  "year_end": null,
  "client_name": null,
  "live_url": null,
  "repo_url": "https://github.com/AlexKachur98/splitroof-assistant-spike",
  "has_live_demo": 0,
  "uses_llm": 1,
  "llm_job": "answers questions about a household's shared expenses by calling typed t...",
  "paid": 0,
  "featured": 1,
  "card": "A read-only assistant that answers \"who owes what this month\" from a hou...",
  "team": "group project, COMP 231 Software Development Project 1, Fall 2026",
  "highlights": "Own the assistant for a shared-household expense app: a read-only agent ...",
  "row_image": "splitroof-flow",
  "row_image_alt": "How the SplitRoof assistant answers: a question goes to Claude Haiku 4.5...",
  "technologies": [
    "React",
    "Node.js",
    ...
  ]
}

GET /api/facts.json

Single facts about Alex, one per row

curl https://alexkachur.com/api/facts.json
[
  {
    "key": "available_from",
    "value": "2027-05",
    "description": "The month Alex can start full-time work, as YYYY-MM"
  },
  {
    "key": "email",
    "value": "alexkachur98@gmail.com",
    "description": "Alex's email address"
  },
  ...
]

GET /api/technologies.json

Languages, frameworks, libraries and services Alex has used

curl https://alexkachur.com/api/technologies.json
[
  {
    "id": 1,
    "name": ".NET",
    "category": "framework",
    "core": 0,
    "skill_area": "Backend"
  },
  {
    "id": 2,
    "name": "Anthropic API",
    "category": "ai",
    "core": 1,
    "skill_area": "LLM integration"
  },
  {
    "id": 3,
    "name": "Astro",
    "category": "framework",
    "core": 0,
    "skill_area": "Frontend"
  },
  ...
]

GET /api/project_images.json

The screenshots on each case study, in display order

curl https://alexkachur.com/api/project_images.json
[
  {
    "project_id": 1,
    "position": 1,
    "alt": "A terminal in VS Code after npm test in splitroof-assistant-spike, with ...",
    "caption": "The spike's test run: 12 tests on the three tools before the model was w..."
  },
  {
    "project_id": 1,
    "position": 2,
    "alt": "VS Code with src/assistant.js open on the system prompt, which tells the...",
    "caption": "The system prompt: every number must come from a tool result, so the mod..."
  },
  ...
]

GET /api/courses.json

Courses Alex is taking at Centennial College

curl https://alexkachur.com/api/courses.json
[
  {
    "code": "COMP 231",
    "name": "Software Development Project 1",
    "term": "Fall 2026",
    "topics": "A team project taken from requirements to a working, tested and document..."
  },
  {
    "code": "COMP 255",
    "name": "Business and Entrepreneurship for SET",
    "term": "Fall 2026",
    "topics": "How businesses are organised and run, ethics and privacy, and turning so..."
  },
  ...
]

GET /api/timeline.json

Work, education and project events by date

curl https://alexkachur.com/api/timeline.json
[
  {
    "id": 1,
    "date": "2019-08",
    "kind": "work",
    "event": "Manager, IOAL Distributing"
  },
  {
    "id": 2,
    "date": "2022-01",
    "kind": "work",
    "event": "QA Tester, 360 Plus IT Consulting"
  },
  ...
]

GET /api/pets.json

The cats Alex lives with

curl https://alexkachur.com/api/pets.json
[
  {
    "name": "Moura",
    "species": "cat",
    "breed": "Norwegian Forest cat",
    "born": 2014,
    "personality": "makes you earn her love, then becomes your best friend",
    "photo_url": "/images/pets/moura.webp",
    "photo_alt": "Close-up of Moura on the cat tree, pink nose forward"
  },
  ...
]

GET /api/experience.json

Jobs and roles Alex has held

curl https://alexkachur.com/api/experience.json
[
  {
    "id": 1,
    "title": "Manager",
    "organization": "IOAL Distributing",
    "location": "Mississauga, ON",
    "start": "2019-08",
    "end": "2022-01",
    "summary": "Warehouse operations.",
    "highlights": "Ran daily warehouse operations (inventory control, order fulfilment, ven..."
  },
  ...
]

GET /api/interests.json

Alex's hobbies and favourites, in his words

curl https://alexkachur.com/api/interests.json
[
  {
    "id": 1,
    "category": "video game",
    "area": "Games",
    "name": "Counter-Strike",
    "note": "My competitive game. I was the in-game leader (IGL) for a semi-pro team ..."
  },
  {
    "id": 2,
    "category": "video game",
    "area": "Games",
    "name": "The Elder Scrolls V: Skyrim",
    "note": "Rich characters and stories, and endless ways to play. Around 3,000 hour..."
  },
  ...
]

GET /api/storage.json

What this site stores for a visitor or a question, how long, and why, from the constants the code uses

curl https://alexkachur.com/api/storage.json
[
  {
    "item": "The SQL and the model's one-line explanation for a question, which can r...",
    "kept_for": "30 days",
    "purpose": "Repeat questions are answered from the cache, free"
  },
  {
    "item": "The model's reason a question cannot be answered, which can repeat words...",
    "kept_for": "1 day",
    "purpose": "The same, for questions the site cannot answer"
  },
  ...
]

GET /api/uses.json

The machines, peripherals and software Alex uses, and what he is learning, as listed on /uses

curl https://alexkachur.com/api/uses.json
[
  {
    "position": 1,
    "section": "Machines",
    "item": "Main PC",
    "details": "ASUS ROG Astral GeForce RTX 5090, AMD Ryzen 7 9850X3D, ASUS ROG Strix X8..."
  },
  {
    "position": 2,
    "section": "Machines",
    "item": "Second PC",
    "details": "GeForce RTX 3080, Intel Core i9-12900K. Also built myself; now mostly fo..."
  },
  ...
]

GET /api/sections.json

The text of the site's pages and case studies, one row per section

curl https://alexkachur.com/api/sections.json
[
  {
    "page": "/#about",
    "position": 1,
    "heading": "About",
    "body": "I build full-stack web apps, and I like adding AI that solves real-world..."
  },
  ...
]

GET /api/page_images.json

The photos on the site's other pages

curl https://alexkachur.com/api/page_images.json
[
  {
    "page": "/#about",
    "position": 1,
    "alt": "Moura on the top bed of a cat tree and Simba on the lower one, in window...",
    "caption": "Moura takes the top bed and Simba takes what is left."
  },
  {
    "page": "/404",
    "position": 1,
    "alt": "Simba mid-meow on a patterned rug, mouth wide open",
    "caption": "Simba, when asked where that page went."
  }
]

GET /api/schema.json

The tables and columns of the database, the SQL that created them, the keys of the facts table with what each means, the pages and headings of the sections table, and a hash covering all of those. The SQL and those three lists are the schema the Ask box sends to the model.

curl https://alexkachur.com/api/schema.json
{
  "hash": "9d5f3b81c745a1d6036ad9c5767d5fb18c4f825a96d43047d6d0eb4d32cf...",
  "ddl": "-- Single facts about Alex, one per row\nCREATE TABLE facts (...",
  "tables": [
    {
      "name": "facts",
      "description": "Single facts about Alex, one per row",
      "primaryKey": [
        "key"
      ],
      "columns": [
        {
          "name": "key",
          "type": "TEXT",
          "nullable": false,
          "unique": false,
          "description": "Fact name, for example location or available_from"
        },
        ...
      ]
    },
    ...
  ],
  "factKeys": [
    "available_from",
    ...
  ],
  "facts": [
    {
      "key": "available_from",
      "description": "The month Alex can start full-time work, as YYYY-MM"
    },
    ...
  ],
  "sectionPages": [
    "/#about",
    ...
  ],
  "sectionHeadings": [
    "About",
    ...
  ],
  "photoAlt": {
    "/images/pets/moura.webp": "Close-up of Moura on the cat tree, pink nose forward",
    "/images/pets/simba.webp": "Simba peeking out from between grey couch cushions"
  }
}

GET /api/resume.json

Alex's resume in the JSON Resume format, built from the same database as everything else.

curl https://alexkachur.com/api/resume.json
{
  "basics": {
    "name": "Alex Kachur",
    "label": "Full-stack & AI developer",
    "email": "alexkachur98@gmail.com",
    "url": "https://alexkachur.com",
    "summary": "I build web apps with an AI doing real work inside them.",
    "location": {
      "city": "Toronto"
    },
    "profiles": [
      {
        "network": "GitHub",
        "username": "AlexKachur98",
        "url": "https://github.com/AlexKachur98"
      },
      ...
    ]
  },
  "work": [
    {
      "name": "Self-employed",
      "position": "Freelance Web Developer",
      "location": "Thornhill, ON",
      "startDate": "2026",
      "summary": "Uraz Hoops (urazhoops.com); Think Smarter Insurance review f...",
      "highlights": [
        "Built and handed off a one-page marketing site for Uraz Hoop...",
        ...
      ]
    },
    ...
  ],
  "education": [
    {
      "institution": "Centennial College",
      "area": "Software Engineering Technology",
      "studyType": "Advanced Diploma",
      "startDate": "2025-01",
      "endDate": "2027-04",
      "score": "4.4 / 4.5",
      "courses": [
        "COMP 231 Software Development Project 1",
        ...
      ]
    }
  ],
  "skills": [
    {
      "name": "Languages",
      "keywords": [
        "SQL",
        ...
      ]
    },
    ...
  ],
  "languages": [
    {
      "language": "English",
      "fluency": "Fluent"
    },
    ...
  ],
  "projects": [
    {
      "name": "SplitRoof AI assistant",
      "description": "An assistant inside a shared-household expense app that answ...",
      "highlights": [
        "Own the assistant for a shared-household expense app: a read...",
        ...
      ],
      "keywords": [
        "React",
        ...
      ],
      "startDate": "2026",
      "roles": [
        "AI integration, documentation, schedule"
      ],
      "url": "https://github.com/AlexKachur98/splitroof-assistant-spike"
    },
    ...
  ],
  "interests": [
    {
      "name": "video game",
      "keywords": [
        "Counter-Strike",
        ...
      ]
    },
    ...
  ]
}

POST /api/ask

Send a question and get back SQL that answers it from this site's database, checked against the real database first, or a short refusal when the data can't answer it. It never runs the SQL; you do.

curl -X POST https://alexkachur.com/api/ask -H 'content-type: application/json' -d '{"question":"What has Alex built for paying clients?"}'

The fields of a 200 body:

fieldtypemeaning
sqlstringA single SELECT or WITH statement prepared against the database, or empty when the question cannot be answered from it
explanationstringOne sentence on what the SQL does, or on why there is none
cachedbooleanTrue when the answer came from the cache rather than the model
tokenstringLets the visitor send this question to Alex through /api/questions within 10 minutes; absent when the question holds a control or text-direction character, which that endpoint refuses

Every status it answers:

statusbodymeaning
200sql, explanation, cached, tokenSQL for the question, or an explanation of why there is none
400error: invalid_questionThe body has no question of 3 to 200 characters
422error: unusable_outputThe model gave nothing that prepares as a safe query
429error: rate_limitedMore than 10 questions or sends in a minute from one address
500error: internalAn unexpected failure
503reason: budget, config, upstreamNot answering: the monthly cap is reached, the service is not set up, or the model did not respond

Each answer also carries a token: to send that question to Alex, pass it to /api/questions within 10 minutes.

What is stored

SELECT item, kept_for, purpose FROM storage;
itemkept_forpurpose
The SQL and the model's one-line explanation for a question, which can repeat words from it, under a SHA-256 hash of the question30 daysRepeat questions are answered from the cache, free
The model's reason a question cannot be answered, which can repeat words from it, under a SHA-256 hash of the question1 dayThe same, for questions the site cannot answer
A counter keyed by a scrambled form of your address2 minutes 1 secondThe rate limit, for questions and sent questions alike
Two monthly totals: questions answered and model calls40 daysThe footer and the monthly cap
A question you chose to send to Alex, and the day you sent it90 daysSo Alex can add what is missing
The number of questions sent today2 daysThe daily cap on sent questions
Your light or dark theme choice, in your own browser, never sent to this siteuntil you clear itKeeps the theme you picked across pages

Everything in the table but the theme choice is held in Upstash Redis.

Your address is scrambled with a keyed hash (HMAC-SHA-256) before anything is stored: a plain hash of an address could be reversed by hashing every address in turn, but without the site's secret key the stored value cannot be matched back to an address.

One line per failed request: status, error type, request ID and timing, written to Vercel's logs. Never the question or your address.

Anthropic, which runs the model, receives the question and, under its data retention policy, deletes API inputs and outputs within 30 days, or keeps them up to 2 years (and its safety classification scores up to 7) if its safety systems flag a request; it does not train on them, since this site's account has not opted in to share data.

Vercel, which hosts the site, keeps its request logs, which record your address and browser as well as that failed-request line, for one hour on this site's plan, and counts page views for Web Analytics without cookies, discarding the visitor hash after 24 hours.

{
  "sql": "SELECT name, client_name, year_start, year_end, summary, live_url FROM p...",
  "explanation": "Lists the projects Alex was paid for, with client names, years, summarie...",
  "cached": false,
  "token": "1790343268.nDY5-7-5ypA6kFe4-glW05L6A7S43ykdVpM4vw3Fv0Q"
}

A sample from 2026-09-25.

POST /api/questions

Sends Alex a question the site could not answer, only when the visitor chooses to send it, using the token /api/ask returned with it. It is kept for 90 days, and no endpoint ever returns it.

curl -X POST https://alexkachur.com/api/questions -H 'content-type: application/json' -d '{"question":"A question the site could not answer","token":"TOKEN"}'

Replace TOKEN with the token /api/ask returned for that question.

The fields of a 200 body:

fieldtypemeaning
sentThe same whether the question is new or was sent before

Every status it answers:

statusbodymeaning
200sent: trueThe question is kept for Alex
400error: invalid_questionThe body has no question of 3 to 200 characters, or it holds control or text-direction characters
403error: invalid_tokenThe token is missing, not for this question, or older than 10 minutes
429error: rate_limited, daily_capOver the rate limit, or the day has taken all the questions it can
500error: internalAn unexpected failure
503reason: budget, config, upstreamNot answering: asking is switched off, the service is not set up, or the store did not respond
{
  "sent": true
}

GET /api/stats

The numbers behind the footer: questions answered this month, model calls this month and the monthly cap, the model the Ask box runs on, and the commit and time of the build.

curl https://alexkachur.com/api/stats

The fields of a 200 body:

fieldtypemeaning
questionsThisMonthintegerQuestions answered this UTC calendar month, cached answers and refusals included
modelCallsThisMonthintegerModel calls attempted this month, retries and failures included; this is what the cap counts
capintegerModel calls allowed per month; 0 means asking is switched off
modelstringThe model id in use
commitstringThe commit the site was built from
builtAtstringWhen the site was built

Every status it answers:

statusbodymeaning
200questionsThisMonth, modelCallsThisMonth, cap, model, commit, builtAtThe counters for the current month and the build
503reason: budget, config, upstreamThe counters could not be read
{
  "questionsThisMonth": 55,
  "modelCallsThisMonth": 30,
  "cap": 2000,
  "model": "claude-haiku-4-5",
  "commit": "5d212c76a0f4c7bb94f6a1de9a75e1d3d5bbd2fd",
  "builtAt": "2026-09-25T05:31:43.690Z"
}

A sample from 2026-09-25.