Skip to content

PramodTKodag/snark

Repository files navigation

snark logo

Snark

Sarcasm as a Service — an open-source, self-hostable AI humor API that generates a unique, witty response on every request.

CI License: AGPL v3 Python 3.12+ Django 5.1

Snark API returning live AI-generated humor


What is Snark?

Snark is an open-source, self-hostable REST API that serves AI-generated humor — roasts, excuses, corporate jargon, fortune cookies, and 30+ other endpoints. Deploy it in one command with Docker, or use the free public API. Every response is uniquely generated by an LLM. No two responses are ever the same.

  • Zero authentication — fully public, no API keys needed
  • Multi-provider — supports Groq (free), Gemini, and Claude with automatic fallback
  • Rate limited — 50 requests/hour per IP
  • One-command stackdocker compose up brings the API, PostgreSQL, and Redis together; nothing external to provision
  • Self-hosted — run the whole thing on your own infrastructure

Quick Start

Prerequisites

  • Docker & Docker Compose — the only requirement; PostgreSQL and Redis are bundled into the stack
  • Python 3.12+ — optional, only for running tests or tooling outside Docker

1. Clone and configure

git clone https://github.com/PramodTKodag/snark.git
cd snark
cp .env.example .env

Edit .env and add your API key for at least one provider:

# Groq is free — get a key at https://console.groq.com/keys
GROQ_API_KEY=your-groq-api-key-here

2. Run with Docker

docker compose --profile dev up --build

The API will be available at http://localhost:8100.

3. Try it out

# Get a creative excuse to say no
curl http://localhost:8100/v1/wit/say-no/

# Get roasted
curl http://localhost:8100/v1/wit/roast/dave/

# Explain quantum physics like you're 5
curl "http://localhost:8100/v1/wit/explain-like-im-5/?q=quantum+physics"

# Generate an honest git commit message
curl http://localhost:8100/v1/wit/commit-message/

Example response

Every endpoint returns the same compact JSON shape:

// GET /v1/wit/proverb/
{
  "response": "He who checks phone during conversation is lost in a sea of distraction, says the Scroll of Digital Wisdom.",
  "persona": "The Ancient Sage",
  "cached": false
}

response is the generated text, persona is the voice that produced it, and cached tells you whether it came from the 5-minute response cache.

API Endpoints

All endpoints are GET requests under /v1/wit/. Most accept optional ?q=, ?mood=, ?length=, ?lang=, and ?stream= query parameters.

Core

Endpoint Description
/v1/wit/say-no/ Creative excuse to say no
/v1/wit/random-excuse/ Random excuse generator
/v1/wit/corporate-jargon/ Corporate buzzword generator
/v1/wit/commit-message/ Honest git commit message
/v1/wit/hot-take/ Spicy hot take
/v1/wit/compliment/ Backhanded compliment
/v1/wit/bug-blame/ Who to blame for the bug
/v1/wit/roast/<name>/ Personalized roast
/v1/wit/worth-it/?q= Decision oracle
/v1/wit/explain-like-im-5/?q= ELI5 any topic

Viral and Workplace

Endpoint Description
/v1/wit/pickup-line/ Tech pickup line
/v1/wit/social-bio/ Social media bio generator
/v1/wit/motivation/ Questionable motivation
/v1/wit/fortune-cookie/ Fortune cookie message
/v1/wit/name-suggestion/?q= Name suggestion for anything
/v1/wit/standup-update/ Standup update generator
/v1/wit/code-review/ Code review comment
/v1/wit/meeting-excuse/ Meeting escape excuse
/v1/wit/jargon-translator/?q= Translate jargon to plain English
/v1/wit/incident-postmortem/ Incident postmortem generator

More

Endpoint Description
/v1/wit/tech-battle/?q= Tech vs tech showdown
/v1/wit/rate-anything/?q= Rate anything out of 10
/v1/wit/horoscope/ Developer horoscope
/v1/wit/tldr/?q= TL;DR any topic
/v1/wit/interview-question/ Absurd interview question
/v1/wit/honest-changelog/ Honest changelog entry
/v1/wit/debug-story/ Debugging horror story
/v1/wit/proverb/ Tech proverb

Discovery and Meta

Endpoint Description
/v1/wit/personas/ List every available persona (slug, name, tone)
/v1/wit/moods/ List accepted mood values
/v1/wit/random/ Surprise me — random persona
/v1/wit/roast-github/<username>/ Roast a public GitHub profile
/v1/wit/stats/ Usage stats (totals and top personas)

Mood Parameter

Add ?mood= to any endpoint to change the tone:

sarcastic, angry, funny, sad, excited, dramatic, passive-aggressive,
philosophical, wholesome, unhinged, dry, chaotic, chill, spicy, deadpan

Fetch this list programmatically from /v1/wit/moods/.

Example: curl "http://localhost:8100/v1/wit/motivation/?mood=passive-aggressive"

Length and Language

Add ?length= (short, medium, or long) to control response size, and ?lang= to get the response in another language:

curl "http://localhost:8100/v1/wit/hot-take/?q=pizza&lang=Spanish"
curl "http://localhost:8100/v1/wit/proverb/?q=coding&length=long"

Streaming

Add ?stream=true to any endpoint and use curl -N to receive the response token-by-token as Server-Sent Events, instead of one JSON object:

Snark streaming a response token by token, live from the LLM

curl -N "http://localhost:8100/v1/wit/hot-take/?q=tabs+vs+spaces&stream=true"

data: {"delta": "Forcing"}
data: {"delta": " everyone"}
data: {"delta": " to"}
...
data: {"persona": "The Hot Take Machine", "done": true}
data: [DONE]

Streaming bypasses the response cache, so streamed calls always hit the LLM. Omit stream for the default cached JSON response.

Batch

POST /v1/wit/batch/ runs up to 5 personas in a single request. Each item names a persona slug (see /v1/wit/personas/) plus the same optional q, mood, length, and lang fields. Results come back in order; a bad slug yields a per-item error without failing the rest.

curl -X POST "http://localhost:8100/v1/wit/batch/" \
  -H "Content-Type: application/json" \
  -d '{
    "requests": [
      {"persona": "roast", "q": "Dave"},
      {"persona": "commit-message"},
      {"persona": "hot-take", "q": "tabs vs spaces", "mood": "spicy"}
    ]
  }'

Each item is a separate LLM call, so a batch is slower than a single request and counts as one hit against the rate limit.

Replying to posts

POST /v1/wit/reply/ takes a social media post and returns one short, sarcastic reply to it — built for reply bots. The reply reacts to the post's actual content and is kept tweet-length by default (length defaults to short).

curl -X POST "http://localhost:8100/v1/wit/reply/" \
  -H "Content-Type: application/json" \
  -d '{"post": "Just shipped a feature with zero tests. What could go wrong?"}'

See docs/twitter-bot.md for a complete ~30-line Twitter reply bot using this endpoint.

Health and Docs

Endpoint Description
/v1/wit/health/live/ Liveness probe
/v1/wit/health/ready/ Readiness probe
/v1/wit/health/status/ Full health check
/v1/wit/swagger/ Swagger UI
/v1/wit/redoc/ ReDoc

Command-line client

The repo ships a dependency-free CLI that wraps the API. Install the package and use the snark command:

pip install -e .

snark hot-take "tabs vs spaces"
snark roast Dave --mood spicy
snark hot-take "remote work" --stream
snark github torvalds
snark personas
snark stats

The first positional argument is a persona slug (or one of personas, stats, random, roast <name>, github <username>); the second is optional context. It points at http://localhost:8100 by default — override with --api or the SNARK_API_URL environment variable.

Use it from AI agents (MCP)

Snark ships an MCP server (snark-mcp, in mcp-server/) so AI assistants — Claude Code, Claude Desktop, Cursor, and other MCP clients — can call snark as native tools. It's a thin client over this same REST API: point it at a running instance with SNARK_API_URL.

It exposes a curated set of tools — snark_roast, snark_roast_github, snark_hot_take, snark_commit_message, snark_reply, snark_worth_it, snark_wit (any persona by slug), and snark_list_personas — so you can just ask "roast the GitHub user torvalds" in your assistant and it calls the right one.

Quickest path with Claude Code (per-project, private to you):

make mcp-install   # build the server into mcp-server/.venv
claude mcp add snark -e SNARK_API_URL=http://localhost:8100 \
  -- "$(pwd)/mcp-server/.venv/bin/snark-mcp"

Restart the session and the tools are available. See mcp-server/README.md for Claude Desktop / Cursor configs, the make mcp / make mcp-http / make mcp-inspect run modes, and details.

Architecture

snark/
├── base/              # Django config
└── wit/
    ├── models.py      # Persona, ResponseLog, GenerationEvent
    ├── views.py       # API endpoints
    ├── services.py    # Orchestrator (caching, anti-repetition, fallback)
    ├── stats.py       # usage / cost / reliability aggregation
    ├── pricing.py     # per-model token pricing
    ├── providers/     # AI provider abstraction
    │   ├── base.py        # AIProvider interface
    │   ├── registry.py    # Provider registry with fallback
    │   ├── groq_provider.py
    │   ├── gemini_provider.py
    │   └── claude_provider.py
    └── management/    # seed / admin bootstrap / pricing / retention commands

Provider Fallback

Snark automatically falls back between AI providers. If the primary provider hits a content filter or rate limit, it retries with a softened prompt, then falls back to the next provider. Configure your preferred default in .env:

AI_DEFAULT_PROVIDER=groq  # groq (free), gemini, or claude

Development

# Run tests
make test

# Run tests with coverage
make test-cov

# Format code
make format

# Lint
make lint

# Run migrations
make migrate

# Seed personas
make seed

# Django shell
make shell

Configuration

All configuration is via environment variables. See .env.example for the full list.

Variable Default Description
AI_DEFAULT_PROVIDER groq AI provider: groq, gemini, or claude
GROQ_API_KEY Groq API key (free at console.groq.com)
GEMINI_API_KEY Google Gemini API key (optional)
ANTHROPIC_API_KEY Anthropic API key (optional, paid)
POSTGRES_DB snark PostgreSQL database name
REDIS_DB 9 Redis database number
USE_PROXY_SSL_HEADER False Trust X-Forwarded-Proto when behind a TLS-terminating proxy
NUM_PROXIES Trusted proxy count, so per-IP rate limiting reads the real client IP from X-Forwarded-For
GUNICORN_WORKERS 2 Gunicorn worker processes (prod only)
GUNICORN_THREADS 8 Threads per worker; threads serve concurrent streams (prod only)
MAX_CONCURRENT_STREAMS 6 Max concurrent SSE streams per worker; excess get a 503 (stream_capacity). 0 disables the cap
ADMIN_ENABLED False Enable the opt-in Django admin UI
ADMIN_URL admin/ Path the admin mounts at when enabled (change it)
ADMIN_USERNAME / ADMIN_EMAIL / ADMIN_PASSWORD Optional superuser auto-bootstrap on startup
PROVIDER_TOKEN_COST — (empty) Optional price override for the dashboard cost estimate: provider:input:output per $1M (e.g. claude:1:5), or legacy provider:blended. Empty uses the vendored LiteLLM map (make update-pricing)
RESPONSE_LOG_RETENTION_DAYS 30 Days to keep raw request logs (ResponseLog); 0 keeps them forever
GENERATION_EVENT_RETENTION_DAYS 90 Days to keep reliability events (GenerationEvent); 0 keeps them forever
LOG_INPUT_MODE redacted How to store user input: redacted (strip structured PII + truncate), none (don't store), or raw (verbatim)

Behind a reverse proxy? Set USE_PROXY_SSL_HEADER=True and NUM_PROXIES=<n> so HTTPS detection and per-IP rate limiting work correctly. Leave both unset for direct connections.

Streaming & concurrency

Gunicorn threads serve concurrent requests; a ?stream=true request pins a thread for the whole LLM call (streaming bypasses the cache). Tune capacity per deployment with GUNICORN_WORKERS and GUNICORN_THREADS. MAX_CONCURRENT_STREAMS caps streams per worker so a burst of streams can't consume every thread and starve normal JSON traffic — excess streams get a fast 503 with a stream_capacity error and a Retry-After header (set it to 0 to disable the cap). Keep it below GUNICORN_THREADS to leave threads free for JSON requests. For heavy sustained streaming load, an async worker (gevent) or an ASGI server is the future scale-up.

Data retention & privacy

Snark is privacy-forward by default: bounded retention is on out of the box and raw user input is not stored verbatim.

  • Bounded retention. Request logs (ResponseLog) are kept 30 days and reliability events (GenerationEvent) 90 days by default. Tune with RESPONSE_LOG_RETENTION_DAYS / GENERATION_EVENT_RETENTION_DAYS (0 = keep forever).
  • PII-minimized input. LOG_INPUT_MODE=redacted (the default) strips structured PII (email, phone, credit-card, SSN-like) from stored input and truncates it. Set none to store nothing, or raw to store input verbatim (opt-in — users may paste PII). Redaction is dependency-light and catches structured identifiers only, not free-text names/addresses; Microsoft Presidio (NER) is an optional upgrade if you need that.

Pruning runs automatically on startup and can be run on demand or from cron:

make prune-logs DAYS=90          # apply a 90-day window to both tables
# cron (daily at 03:00): apply the configured retention windows
0 3 * * * cd /app/snark && python manage.py prune_logs

Stats and anti-repetition never read the input field, so redaction does not affect them.

Logging & observability

Logs are human-readable plain text by default. Set LOG_FORMAT=json (with an optional LOG_LEVEL, default INFO) to emit one JSON line per record for both Django and app logs — structured so aggregators can filter by field. Recommended in production.

Every generation also logs a structured generation event with provider, model, split input_tokens/output_tokens (and tokens), latency_ms, and an estimated cost_usd — a machine-parseable, per-request usage line. It is INFO and privacy-safe: it never includes raw user input.

LOG_FORMAT=json     # plain (default) | json — one JSON line per record
LOG_LEVEL=INFO      # DEBUG | INFO | WARNING | ERROR

Ship these JSON lines to Loki/Grafana (or any log aggregator) for dashboards and alerting on token usage, cost, latency, and error rates — no extra dependency, just structured stdout.

Admin panel (optional)

Snark ships an opt-in Django admin for managing personas and browsing usage — off by default so public deployments never expose a login.

Enable it per deployment in .env:

ADMIN_ENABLED=True
ADMIN_URL=manage-a1b2c3/          # change to a non-guessable path; keep the trailing slash
ADMIN_USERNAME=admin              # optional: auto-bootstrap a superuser on startup
ADMIN_EMAIL=admin@example.com
ADMIN_PASSWORD=a-strong-password  # min 12 chars
# PROVIDER_TOKEN_COST=claude:1:5   # optional override, "provider:input:output" per $1M

Cost pricing comes from a vendored copy of LiteLLM's community-maintained model cost map (MIT), stored at snark/wit/pricing_data.json and refreshable with make update-pricing. Tokens are priced per model, split into input vs output. PROVIDER_TOKEN_COST is an optional override (empty by default): provider:input:output per $1M (e.g. claude:1:5), or legacy provider:blended applied to both. It remains an estimate — list prices, cached/batch discounts aren't modeled. Both streamed and non-streamed responses log split token usage. Rates are cached for the process lifetime, so restart the app after make update-pricing. Responses logged before the token-split feature carry no input/output split, so their estimated cost shows $0.

On startup the stack runs ensure_admin, which creates/updates the superuser from those vars (a no-op when unset). Or create one manually:

make admin          # interactive createsuperuser
make ensure-admin   # from ADMIN_* env vars

The admin mounts at ADMIN_URL and includes an interactive analytics dashboard (Chart.js) with usage, cost, latency, and reliability analytics — KPI cards, time-series and provider-share charts, latency percentiles, per-persona cost, provider/model breakdowns, reliability metrics (error rate, provider-fallback frequency, content-filter rate) with recent-failure detail, unused-persona detection, a recent-activity feed, DB/Redis health status, and a configurable cost estimate. It also provides persona CRUD with bulk activate/deactivate/duplicate/clear-cache actions, and read-only response-log browsing with retention pruning. Prune logs from the CLI too:

make prune-logs DAYS=90

Keep the admin behind a reverse proxy / IP allowlist on any internet-facing host.

Contributing

Contributions are welcome. Please read CONTRIBUTING.md before submitting a pull request.

By contributing, you agree to the Contributor License Agreement.

Support

snark is free and open source under the AGPL-3.0 license. Running the public API costs real money — every response is a live LLM call. If it made you laugh or saved you time, consider sponsoring its development:

Sponsorship helps keep the public instance running and funds new endpoints.

Security

To report a security vulnerability, please email pramodkodag.dev@gmail.com. Do not open a public GitHub issue. See SECURITY.md for details.

License

Copyright (C) 2026 Pramod Kodag

This project is licensed under the GNU Affero General Public License v3.0. This means:

  • You can use, modify, and distribute this software freely
  • If you modify and deploy it as a network service, you must share your changes
  • All derivative works must use the same license

Author

Pramod KodagGitHub

About

Sarcasm as a Service. an AI-powered humor API that generates a unique, witty response on every request. Roasts, honest commit messages, ELI5, and 25+ more endpoints.

Topics

Resources

License

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

1 watching

Forks

Packages

 
 
 

Contributors

Languages