{"id":48853103,"url":"https://github.com/bispo-daniel/morphereum-community-api","last_synced_at":"2026-04-15T10:32:58.626Z","repository":{"id":320662729,"uuid":"1079050013","full_name":"bispo-daniel/morphereum-community-api","owner":"bispo-daniel","description":"A compact, rate-limited, cache-aware API for a fictional meme coin ecosystem: $Morphereum","archived":false,"fork":false,"pushed_at":"2025-10-25T03:44:50.000Z","size":73,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-10-25T05:35:44.220Z","etag":null,"topics":["cloudinary-sdk","cors","express","express-rate-limit","express-slow-down","jsonwebtoken","mongodb","mongoose","morgan","node-cache","nodejs","typescript","zod"],"latest_commit_sha":null,"homepage":"https://morphereum-community-api.onrender.com","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/bispo-daniel.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-10-19T01:34:02.000Z","updated_at":"2025-10-25T03:44:43.000Z","dependencies_parsed_at":"2025-10-25T05:45:52.679Z","dependency_job_id":null,"html_url":"https://github.com/bispo-daniel/morphereum-community-api","commit_stats":null,"previous_names":["bispo-daniel/morphereum-community-api"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/bispo-daniel/morphereum-community-api","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bispo-daniel%2Fmorphereum-community-api","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bispo-daniel%2Fmorphereum-community-api/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bispo-daniel%2Fmorphereum-community-api/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bispo-daniel%2Fmorphereum-community-api/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/bispo-daniel","download_url":"https://codeload.github.com/bispo-daniel/morphereum-community-api/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bispo-daniel%2Fmorphereum-community-api/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31837165,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-15T10:26:52.245Z","status":"ssl_error","status_checked_at":"2026-04-15T10:26:51.649Z","response_time":63,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cloudinary-sdk","cors","express","express-rate-limit","express-slow-down","jsonwebtoken","mongodb","mongoose","morgan","node-cache","nodejs","typescript","zod"],"created_at":"2026-04-15T10:32:58.042Z","updated_at":"2026-04-15T10:32:58.609Z","avatar_url":"https://github.com/bispo-daniel.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Morphereum — Community API ⚙️\n\n\\*A compact, rate-limited, cache-aware API for a fictional meme coin ecosystem: **$Morphereum\\***\n\n\u003e Powers the Community Interface with token quotes, daily raids, curated links, community arts, and engagement metrics.\n\n---\n\n## ✨ What this API does\n\n- **Token quotes**: formatted price/volume/changes/supply/holders from CoinMarketCap DEX endpoints.\n- **Daily Raid**: fetch today’s raid (platform, URL, share copy).\n- **Curated Links**: official + community links, with trending metrics.\n- **Community Arts**: approve-gated gallery + image upload pipeline (compression + Cloudinary).\n- **Engagement Metrics**: visits, raids, links, chat (user/raid messages), arts submissions — with daily breakdowns and trending.\n\nAll endpoints live under the base path: **`/api`**.\n\n---\n\n## 🧱 Tech Stack\n\n**Runtime \u0026 Server**\n\n- **Node.js** + **Express**\n- **HTTPS (dev)** via local certs (`localhost.pem`, `localhost-key.pem`)\n- **CORS** (allow-list)\n- **Rate limiting** + **progressive slowdown** (`express-rate-limit` + `express-slow-down`)\n- **morgan** logging with colorful status + timestamps\n- **dotenv** + **zod** for strict **env validation**\n\n**Data \u0026 Storage**\n\n- **MongoDB** (Mongoose models) for raids, links, arts and metrics\n- **Cloudinary** for image storage (uploads from API)\n- **NodeCache** for in-memory caching (end-of-day TTL or hours)\n\n**Utils \u0026 DX**\n\n- **multer** (memory) + **sharp** (JPEG 80%, width 800) for upload pipeline\n- **date-fns** for date math/formatting\n- Type-safe schemas via **zod**\n\n---\n\n## 🚦 Security, CORS, Rate-limit \u0026 Slowdown\n\n- **CORS origins**: `https://localhost:5173`, `https://morphereum.netlify.app`.\n- **Slowdown**: after **150 req/min**, each hit adds `(hits * 500ms)` delay.\n- **Rate-limit**: **1000 req / 10 min / IP** (standard headers, no legacy).\n- **HTTPS (dev only)**: if `NODE_ENV=development`, server boots with local certs.\n- **Trust proxy**: enabled (for correct client IP when proxied).\n- **Logging**: custom `morgan` format with colorized HTTP status + local timestamp.\n\n---\n\n## 🧭 REST Endpoints\n\n\u003e Base path: `/api`\n\n### Token\n\n- `GET /token` → Latest formatted token stats (cached for `TOKEN_CACHE_HOURS`).  \n  **Response (example)**:\n  ```json\n  {\n    \"tokenPriceInUSD\": \"$0.01234\",\n    \"volumeIn24H\": \"$1.2M\",\n    \"changeIn1H\": \"+0.54%\",\n    \"changeIn24H\": \"-2.10%\",\n    \"marketCap\": \"12M\",\n    \"buy24H\": \"1.2K\",\n    \"sell24H\": \"980\",\n    \"transactions24H\": \"2.1K\",\n    \"totalSupply\": \"420M\",\n    \"holders\": 12345\n  }\n  ```\n  (Formatted fields come from CMC DEX “pairs/quotes/latest”.)\n\n### Raid\n\n- `GET /raid` → Today’s raid (by UTC date): `{ date, platform, url, shareMessage, content }`. Cached until end of day.\n\n### Links\n\n- `GET /links` → Curated links list: `[{ _id, label, url, icon, type }]` (`type` ∈ `community-links | official-links`). Cached until end of day.\n\n### Arts\n\n- `GET /arts?page={n}` → Paginated, only `approved: true`. Returns:\n  ```json\n  {\n    \"arts\": [\n      {\n        \"_id\": \"...\",\n        \"approved\": true,\n        \"name\": \"...\",\n        \"creator\": \"...\",\n        \"xProfile\": \"...\",\n        \"description\": \"...\",\n        \"url\": \"https://...\"\n      }\n    ],\n    \"page\": 1,\n    \"next\": true\n  }\n  ```\n  Page size: **20**. Cached until end of day.\n- `POST /arts` (**multipart/form-data**) → Upload + register art. Fields:\n  - `image` (file) — **required**; images only; **≤ 10MB**; processed to **800px wide, JPEG 80%**\n  - `creator` (string), `xProfile` (string), `description` (string) — all **required**  \n    **Status**: `201` on success; `400` on bad input; `500` on processing error.  \n    (Uploads go to Cloudinary; original is never stored permanently.)\n\n### Metrics\n\n\u003e All “GET” metrics default to a **7-day** window ending **yesterday (UTC)** for stability. Many endpoints aggregate to `{ total, highestCount, daily[] }`.\n\n- **Visits**\n  - `POST /metrics/visits` → body: `{ \"country\": \"Brazil\" }`\n  - `GET  /metrics/visits` → time series over last 7 days\n  - `GET  /metrics/visits/countries` → `{ highestCount, countries: [{ country, count }] }`\n- **Raids**\n  - `POST /metrics/raids`\n  - `GET  /metrics/raids` → 7-day time series\n  - `GET  /metrics/raids/trending` → Top dates + platform: `{ total, raids: [{ date: \"dd/MM/yyyy\", count, platform }] }`\n- **Links**\n  - `POST /metrics/links` → body: `{ \"linkId\": \"\u003cMongoID\u003e\" }`\n  - `GET  /metrics/links` → 7-day time series\n  - `GET  /metrics/links/trending` → Top links + icon: `{ total, links: [{ label, icon, count }] }`\n- **Chat**\n  - `POST /metrics/chat` → user messages\n  - `GET  /metrics/chat` → 7-day user message series\n  - `POST /metrics/chat/raid-message` → raid messages\n  - `GET  /metrics/chat/raid-message` → 7-day raid message series\n- **Arts**\n  - `POST /metrics/arts` → body: `{ \"xProfile\": \"https://x.com/...\" }`\n  - `GET  /metrics/arts` → 7-day submission series\n  - `GET  /metrics/arts/producers` → `{ producers, arts }`\n  - `GET  /metrics/arts/producers/trending` → Top producers with counts/approvedCount\n\nAll endpoints return `404` when no data is found for the requested aggregation window.\n\n---\n\n## 📚 API Documentation (Swagger / OpenAPI)\n\n- **Swagger UI** is served at: `http(s)://localhost:\u003cPORT\u003e/docs` (automatically mounted by the server).\n- **Raw OpenAPI JSON**: `http(s)://localhost:\u003cPORT\u003e/openapi/openapi.json`\n  Both routes are registered in `src/server.ts` using **swagger-ui-express** and a static mount of the `docs/` directory.\n\n### How it’s organized\n\nThe OpenAPI specification is **fully modular** and stored as JSON files inside `docs/`:\n\n- `docs/openapi.json` – root spec that composes all other definitions.\n- `docs/components/` – reusable **schemas**, **responses**, and **parameters**.\n\n  - Schemas include: **Token**, **Raid**, **Link**, **ArtItem**, and all **metrics** models (e.g., visits, links, raids, chat, arts).\n  - Responses define empty-body patterns for success and errors (`Empty200`, `BadRequest`, `NotFound`, etc.).\n  - Common query and path parameters (e.g., `Page` for pagination).\n\n- `docs/paths/**` – individual route operation files (Token, Raid, Links, Arts, Metrics).\n  Each operation references shared components through `$ref`.\n\n### Design notes\n\n- **Public by design** — `/docs` and `/openapi` are intentionally exposed in production to simplify integration and testing.\n  Only the documentation endpoints are public; all API resources remain protected as defined.\n- **No build step needed** — the JSON specs are read directly by Swagger UI. Any edit to `docs/**/*.json` is instantly reflected on reload.\n- Responses that return no body (e.g., `400`, `404`, `500`) are explicitly modeled as “empty” to mirror real runtime behavior.\n- Consistent naming and `$ref` usage ensure parity with the Admin API structure.\n\n### How to update\n\n1. Edit or add new schemas in `docs/components/schemas.json`.\n2. Reuse shared responses and parameters from `docs/components/*.json`.\n3. Create or modify endpoint definitions under `docs/paths/**`, and reference them in `docs/openapi.json`.\n4. Open `/docs` in your browser to preview and verify updates.\n\n\u003e To restrict documentation access in production, remove or secure the `/docs` and `/openapi` mounts in `server.ts`.\n\u003e By default, they remain publicly accessible for transparency and developer experience.\n\n---\n\n## 🔁 Cross-API Cache Invalidation (RabbitMQ / CloudAMQP)\n\n**Why messaging?**\nInstead of using HTTP callbacks between services, this project uses **RabbitMQ (CloudAMQP – Little Lemur)** to practice message-driven patterns and ensure consistent cache invalidation between APIs.\n\n## Summary\n\n- **Goal:** keep in-memory caches synchronized between the **Community API** and the **Admin API**.\n- **Approach:** both APIs share a common topic exchange (`cache.flush`).\n\n  - The **Admin API** publishes events when data changes.\n  - The **Community API** listens to those events and clears its caches accordingly.\n\n- **Broker:** RabbitMQ via CloudAMQP.\n- **Exchange:** topic exchange dedicated to cache-flush events.\n- **Routing keys:** `arts.flush`, `links.flush`, `raids.flush`.\n- **Message payload:** includes the event type, timestamp, and a `source` identifier, allowing each API to ignore its own messages if needed.\n\n## Publishers (Senders)\n\nThis API publishes **only one event type**:\n\n- **Arts:** when a new art submission is registered (`POST /arts`), this service clears its own cache and **publishes `arts.flush`** to notify the Admin API and any other subscribers.\n\nAll other mutation events (links, raids, etc.) originate from the Admin API.\n\n## Consumer (Listener)\n\n- **Bindings:** listens to `arts.flush`, `links.flush`, and `raids.flush`.\n- **Effect:** when an event is received, the corresponding NodeCache entries are cleared (`artsData`, `linksData`, `raidData`).\n- **Self-skip:** messages published by this same service (identified by the `source` field) are safely ignored to avoid redundant flushes.\n\n## Environment \u0026 Conventions\n\n- **Broker URL:** provided via environment variable `RABBITMQ_URL` (CloudAMQP connection string).\n- **Exchange name:** `cache.flush` (type: topic).\n- **Routing keys:** `arts.flush`, `links.flush`, `raids.flush`.\n- **Delivery semantics:** lightweight, fire-and-forget notifications; duplicate deliveries are harmless since cache clears are idempotent.\n\n## Operational Notes\n\n- **Startup:** the RabbitMQ consumer is initialized on boot and remains subscribed to the exchange.\n- **Observability:** monitor queue and routing activity via the CloudAMQP dashboard.\n- **Failure behavior:** if the broker is down, local caches are still invalidated; remote APIs update once connectivity returns (eventual consistency).\n- **Security:** keep broker credentials private; use per-environment CloudAMQP URLs.\n- **Performance:** payloads are small and processing is near-instant.\n\n## Quick Checklist\n\n- Confirm `RABBITMQ_URL` and `cache.flush` exchange are set in environment variables.\n- Verify this service **publishes only `arts.flush`** on art submission.\n- Ensure listeners are active for all three routing keys (`arts.flush`, `links.flush`, `raids.flush`).\n- Check that cache clearing is idempotent and consistent across both APIs.\n\n---\n\n## 🕵️ Observability (Sentry)\n\nThis API ships with **Sentry** for runtime error tracking, **performance traces (APM)** and optional **CPU profiling**.\n\n- **Where it’s wired:** initialization + process handlers live in `src/observability/sentry.ts`, and are mounted in `src/server.ts` (initialized **before** routes and the error handler attached **after** the router).\n- **What we capture:** Express errors (via `Sentry.setupExpressErrorHandler`), HTTP spans, unhandled rejections and uncaught exceptions.\n- **Packages:** `@sentry/node` and `@sentry/profiling-node`.\n\n### Configuration\n\nAdd these env vars (already validated at boot):\n\n```env\n# sentry\nSENTRY_DSN=\nSENTRY_TRACES_SAMPLE_RATE=0.1     # 0.1 (APM)\nSENTRY_PROFILES_SAMPLE_RATE=0.1   # 0.1 (CPU profiling)\n```\n\nIf `SENTRY_DSN` is blank, Sentry is **skipped** (boot logs a warning). Default sample rates fall back to `0` when not set.\n\n### How it works\n\n- **`setupSentry(app)`**: initializes Sentry **before** routes with HTTP + Express integrations, **tracing (APM)** and **CPU profiling** via `@sentry/profiling-node`.\n- **`wireProcessHandlers()`**: forwards `unhandledRejection` and `uncaughtException` to Sentry.\n- **`attachSentryErrorHandler(app)`**: installs Sentry’s Express error middleware **after** the router.\n\n\u003e Tip: start with low sample rates in production (e.g., `0.1`) and adjust as needed.\n\n---\n\n## 🗂️ Data Models (MongoDB via Mongoose)\n\n- **Raid**: `{ date: Date, platform: string, url: string, shareMessage: string, content: string }`\n- **Links**: `{ label, url, icon, type }` (`type` ∈ `community-links | official-links`)\n- **Arts**: `{ approved, name, creator, xProfile, description, url }`\n- **Metrics (collections)**\n  - `visits_metrics`: `{ country, date }`\n  - `raid_metrics`: `{ date }`\n  - `links_metrics`: `{ date, linkId(ref Links) }`\n  - `chat_metrics`: `{ date, type ∈ user-message|raid-message }`\n  - `arts_metrics`: `{ xProfile, date }`\n\nValidation for inbound/outbound shapes is done with **zod** where applicable.\n\n---\n\n## 🧮 Caching Strategy\n\n- **End-of-day TTL**: Many GET controllers cache responses in memory (`NodeCache`) until **23:59:59 UTC today** (computed once on boot).\n- **Token**: Cached for `TOKEN_CACHE_HOURS` (env-driven).\n- **Cache keys**: e.g., `tokenData`, `raidData`, `linksData`, `artsData-page-{n}`, etc.\n- **Not found** (or validation failures) are **not** cached.\n\nThis keeps traffic to DB/CMCap low while keeping the dashboard snappy.\n\n---\n\n## 🔐 Environment Variables\n\nAll envs are **validated at startup** (process exits on failure):\n\n```env\n# core\nNODE_ENV=development            # or production\nPORT=8080\n\n# token source (CMC)\nTOKEN_POOL_ADDRESS=...\nCMC_API_URL=https://pro-api.coinmarketcap.com\nCMC_API_TOKEN=...\nCMC_SOL_NETWORK_ID=...\nCMC_API_AUX_FIELDS=...\n\n# token cache\nTOKEN_CACHE_HOURS=1\n\n# database\nMONGODB_CONNECTION_STRING=mongodb+srv://...\nMONGODB_DB_NAME=morphereum\n\n# cloudinary (uploads)\nCLOUDINARY_CLOUD_NAME=...\nCLOUDINARY_API_KEY=...\nCLOUDINARY_API_SECRET=...\n```\n\n\u003e In **development**, the server attempts to use local HTTPS and will look for `localhost.pem` and `localhost-key.pem` at the project root.\n\u003e Use `mkcert` to generate the certicates with the command `mkcert localhost`\n\n---\n\n## ▶️ Getting Started\n\n```bash\n# 1) install deps\npnpm install        # or: npm i / yarn\n\n# 2) create .env file\ncp .env.example .env   # then fill in all required fields\n\n# 3) run dev (uses HTTPS if NODE_ENV=development and certs exist)\npnpm dev\n\n# 4) production build \u0026 run (typical PM2 / container flow)\npnpm build \u0026\u0026 pnpm start\n```\n\nRecommended: **Node 18+**. The app connects to MongoDB on boot and logs connection status.\n\n---\n\n## 🏗️ Project Structure (API)\n\n```\nsrc/\n  config/               # env schema (zod), dev cert loader\n  controllers/          # route handlers (token, raid, links, arts, metrics/*)\n  middlewares/          # imageHandler (multer + sharp)\n  models/               # mongoose schemas (arts, links, raids, metrics/*)\n  router/               # mounts /token, /raid, /links, /arts, /metrics\n  services/             # DB/CLOUD/CMC orchestration \u0026 aggregations\n  types/                # zod schemas (e.g., CoinMarketCap)\n  utils/                # cache TTL, dates, http helpers, logging, connections\n  server.ts             # express bootstrap, CORS, limits, HTTPS(dev)\n```\n\nKey flows:\n\n- **Token**: fetch from CMC → zod parse → format fields → cache.\n- **Arts upload**: `multer` (memory) → `sharp` (resize/compress) → temp write → **Cloudinary** upload → Mongo record → remove temp.\n- **Metrics**: query by UTC-normalized windows, generate daily series with `date-fns`.\n\n---\n\n## 🧪 Status \u0026 Errors\n\n- **200** JSON payloads via `sendJson()`.\n- **201** (art created), **400** (validation/missing fields), **404** (no data), **500** (internal).\n- Minimal bodies for error statuses by design.\n- Console logging uses a custom wrapper for colored timestamps (dev-friendly).\n\n---\n\n## 🧩 Integration Notes (Frontend)\n\n- Frontend should pass **`page`** on `/arts` (1-based).\n- For `/metrics/*` GETs, assume **7-day** windows ending **yesterday (UTC)**.\n- For `/token`, values are **pre-formatted strings** (UI-ready).\n- For uploads, field name **`image`**; enforce client-side size/type too.\n\n---\n\n### Made with ⚡ by the $Morphereum community\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbispo-daniel%2Fmorphereum-community-api","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbispo-daniel%2Fmorphereum-community-api","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbispo-daniel%2Fmorphereum-community-api/lists"}