Backend API for the Neko Singa portfolio project. Built as a direct response to the Full-Stack Engineer (Frontend-Leaning) role at Elfa AI — the goal wasn't just to show I can code, but to show stack alignment and decision-making that matches what the role actually needs.
Live: api-nekosinga.vercel.app
The job posting calls out several specifics, and every technical decision in this repo is aimed at demonstrating those:
| Requirement from the Job Posting | How It's Addressed Here |
|---|---|
| "Node/TypeScript backend experience: Express, REST APIs, Postgres (we use Kysely), Redis, queues" | This backend uses Express + TypeScript, Postgres via Neon, and Kysely as the query builder — the exact stack they mention |
| "Appreciation for financial markets & trading" | Real crypto data integration (trending tokens, sentiment, market news) via the Elfa SDK, not mock data |
| "Work AI-first" | Originally designed to use Elfa SDK's AI Chat feature as part of the product flow (see note below) |
| "Own features end-to-end, from UX/interaction design through frontend, API, and release" | This repo is the backend half of a polyrepo system (web, app, api, docs) built and deployed from scratch to live |
Queues (BullMQ/RabbitMQ in their production stack) are replaced here with Upstash QStash, since the entire backend is deployed as serverless functions on Vercel — BullMQ/RabbitMQ need a long-running process, which doesn't fit the serverless model. This is a deliberate architectural adaptation, not unfamiliarity with the concept.
This section is intentionally written transparently, because I think this part of the process is worth showing, not hiding.
The original plan was to build /api/agent/chat using elfa.chat() from @elfa-ai/sdk — Elfa's built-in AI Chat feature. On testing, the request came back with:
{
"error": "ERR_FORBIDDEN",
"message": "The AI Chat (Ask Elfa) endpoint requires a Grow or Pay-as-you-go plan."
}That feature (along with Trending Narratives) turned out to be gated behind the Grow plan ($290/mo), not included in the Free tier I was using for development/testing.
Rather than ship a feature I couldn't actually test end-to-end — and risk it being unreliable for anyone trying to demo this — I chose to:
- Focus on what's available on the Free tier and make sure it works solidly: trending tokens, keyword mentions, token news, trending contract addresses, and account smart stats.
- Fix the error handling — the 403 from Elfa was originally collapsing into a generic
500 Internal Server Erroron my side. I fixed this so Elfa's actual status code and message get forwarded, instead of being masked. - Document the scope in the PRD (
/docs), including which features were intentionally excluded and why, so there's no confusion for anyone reviewing this later.
AI Chat and Trending Narratives remain planned as a v2 milestone — the architecture (routes, request/response types, integration point) is already scaffolded, ready to enable if the plan gets upgraded.
This is a straight account of what went wrong shipping this API to Vercel and how each issue was tracked down, because debugging a "successful" deployment that serves nothing useful is its own skill worth documenting.
The deployment showed a green "Ready" status in the Vercel dashboard, but every single route — including / — returned Vercel's generic 404: NOT_FOUND page. A "Ready" deployment with zero working routes is misleading: it means the build step didn't error out, not that the app actually built correctly.
Investigation: Compared this project's vercel.json against a known-working sibling project. This repo mixed the legacy builds + routes schema with the modern buildCommand / outputDirectory keys in the same file — a combination Vercel does not reliably support, and a strong early suspect. That pointed at config, but the real answer came from the build logs, not the config diff (see next issue).
Pulling the actual Build Logs (not just the deployment status) surfaced the real problem:
Using TypeScript 7.0.2 (local user-provided)
Error: Cannot read properties of undefined (reading 'readFile')
package.json had "typescript": "^7.0.2" — which resolves to TypeScript's native (Go-based) preview compiler, not a stable release. That preview has a different internal API from TS 5.x, and tsc crashed outright instead of compiling. This explains Symptom 1: the build never produced valid output, so there was nothing for Vercel to route to.
Fix: Pinned typescript to a stable 5.x release and reinstalled:
- "typescript": "^7.0.2"
+ "typescript": "^5.7.3"With the build fixed, the function was deployed and actually got invoked — but every request now failed with FUNCTION_INVOCATION_FAILED (500). The Runtime Logs showed:
Error [ERR_REQUIRE_ESM]: require() of ES Module /var/task/node_modules/kysely/dist/index.js
from /var/task/src/db/client.js not supported.
Instead change the require of index.js in .../src/db/client.js to a dynamic import()
which is available in all CommonJS modules.
Root cause: kysely ships as a pure ESM package (no CommonJS build). This project's tsconfig.json had "module": "commonjs", so every import compiled down to require() — which Node refuses for ESM-only packages.
Fix: Migrated the project to ESM instead of trying to force an ESM package to work under CommonJS:
# package.json
+ "type": "module"
# tsconfig.json
- "module": "commonjs",
- "moduleResolution": "bundler",
+ "module": "NodeNext",
+ "moduleResolution": "NodeNext",Every relative import in src/**/*.ts also needed an explicit .js extension, which NodeNext module resolution requires:
- import { db } from './db/client';
+ import { db } from './db/client.js';| Layer | Symptom | Root Cause | Fix |
|---|---|---|---|
| Routing | 404 on every path |
Misleading — see Build below | N/A, resolved once the build actually succeeded |
| Build | Deployment "Ready" but produced nothing | typescript@^7.0.2 (native preview) crashing tsc |
Pinned to stable typescript@^5.x |
| Runtime | 500 FUNCTION_INVOCATION_FAILED |
kysely is ESM-only; project compiled to CommonJS |
Migrated project to ESM (NodeNext + .js import extensions) |
Takeaway: A "successful" deployment status only means the platform didn't detect an error — it doesn't mean the app works. Build Logs and Runtime Logs are two different failure surfaces and need to be checked separately; the fix for a 404 can live entirely outside routing config.
| Method | Endpoint | Data Source |
|---|---|---|
| GET | /api/health |
— |
| GET | /api/market/trending |
elfa.getTrendingTokens |
| GET | /api/market/sentiment/:token |
elfa.getKeywordMentions |
| GET | /api/market/news |
elfa.getTokenNews |
| GET | /api/market/trending-cas |
elfa.getTrendingCAsTwitter |
| GET | /api/market/stats/:username |
elfa.getAccountSmartStats |
| POST | /api/auth/login |
— |
- Runtime: Node.js + TypeScript (ESM)
- Framework: Express
- Database: PostgreSQL (Neon) + Kysely
- Cache: Redis (Upstash)
- Queue: Upstash QStash (serverless-friendly alternative to BullMQ)
- Auth: JWT
- Data Source:
@elfa-ai/sdk - Deploy: Vercel (Serverless Functions)
Part of the nekosinga polyrepo: