Six interpretable metrics. One POST request. No black box.
Analyze structural engagement-bait patterns in text with a lightweight, deterministic heuristic layer and an optional OpenAI embedding-based semantic score. Every score includes a full transparent breakdown showing exactly which signals were detected and how they contributed.
The API is publicly hosted and ready to use — no setup required.
| Resource | URL |
|---|---|
| Base URL | https://engagbaitapi.onrender.com |
| Interactive demo | https://engagbaitapi.onrender.com/demo |
| API Documentation | https://engagbaitapi.onrender.com/redoc |
| Health check | https://engagbaitapi.onrender.com/health |
Try it immediately with curl. Windows users, do NOT run this in Command Prompt or PowerShell. Use Git Bash, WSL, or a Unix-based terminal:
curl -s -X POST "https://engagbaitapi.onrender.com/analyze?embeddings=false" \
-H "Content-Type: application/json" \
-d '{"text": "Act now. This is your last chance to see the truth before it disappears. Everyone knows they are lying, and if you do not share this immediately, more people will be fooled. There is no middle ground, and the answer is obvious."}' \
| python -m json.tool- Social media moderation — flag posts for editorial review before they gain traction
- Journalism analytics — scan articles for one-sided framing, rhetorical pressure, and emotional manipulation signals
- Email filtering — supplement spam detection with engagement-bait signals like urgency and curiosity gaps
- Browser extension backend — score articles inline as users read them
- Media literacy education — show students exactly why a text scores high, signal by signal
All scores are in the range [0.0, 1.0]. Two metrics (evidence_density and counterargument_absence) are inverted — higher values indicate more bait-like behavior, not more evidence or more nuance.
Scans for three categories of urgency signal using a curated phrase list:
time_pressure— "act now", "last chance", "breaking", "tick tock"scarcity— "while supplies last", "limited spots", "only a few left"fomo— "don't miss out", "everyone already knows", "you'll regret this"
Score is the weighted average of the three sub-scores. Includes negation detection — "you don't need to act now" is not counted as urgency pressure.
Looks for signals that a text is grounding its claims in data. Higher score means less evidence present.
citations— "according to", "researchers found", "a study shows"stats— numeric patterns with units or percentages (e.g., "14 percent", "3.2 million")external_sources— named institutions, publications, or proper sourcing language
A text making bold claims with no data or sourcing scores close to 1.0.
Six sub-scores measuring emotional and rhetorical heat:
emotion_words— curated lexicon with valence tiers: tier 1 (worried, angry), tier 2 (furious, hatred), tier 3 (enraged, terror). Higher tiers contribute more. Includes negation detection ("not angry" → no hit) and degree modifier scaling ("extremely angry" scores higher than "slightly angry").moralized_language— words framing issues in moral absolutes: corrupt, evil, sacred, betrayalsuperlative_density— "the worst", "ever", "unprecedented", "most dangerous"caps_ratio— proportion of ALL-CAPS words (checked on original, unmodified text)exclamation_densityandquestion_density— scaled by text length to avoid false positives on short texts; a single!in a short email does not spike the score to 1.0curiosity_gap— "what they don't want you to know", "the real reason", "suppressed for years"
Checks for the absence of language that acknowledges complexity or competing views.
tradeoff_absence— missing: "on the other hand", "however", "that said", "the downside is"conditional_absence— missing: "it depends", "evidence suggests", "in some cases", "tends to"
A text presenting only one perspective with no hedging or qualification scores 1.0.
Three sub-scores measuring whether a text makes many claims without explaining them:
claims_per_word— assertion verbs (proves, reveals, exposes, confirms) relative to total word countexplanation_depth— subordinate clauses and causal connectors (because, therefore, which means, as a result). More explanation lowers this sub-score.listicle— numbered or bulleted list structures, which tend to prioritize volume of points over depth of reasoning
Uses MATTR (Moving Average Type-Token Ratio) rather than raw TTR. A sliding window scans across the token sequence and computes unique / window at each position; the final score is the average across all windows. This is length-invariant — a 500-word repetitive text and a 50-word repetitive text are judged comparably. Window size adapts for short texts.
Inverted: low vocabulary diversity (sloganeering, repetition) scores high as a bait signal.
mattr— moving average type-token ratiotype_token_ratio— total unique words / total words
When embeddings mode is enabled, the text is embedded using OpenAI text-embedding-3-small and scored against precomputed centroids of curated bait and neutral seed examples. Centroids are computed once on the first embeddings request and cached for the server's lifetime — subsequent calls do not re-embed the seed examples. Score is the transformed cosine similarity difference (bait centroid vs neutral centroid), normalized to 0–1.
This is the only metric that makes an external API call. If OpenAI is unavailable, engagement_bait_score returns null and all heuristic metrics still return normally.
- FastAPI — async Python web framework; chosen for automatic OpenAPI schema generation, which drives both the Swagger (
/docs) and ReDoc (/redoc) documentation pages with zero extra configuration - Pydantic — data validation layer; enforces strict input constraints (text length, batch size) and returns structured, descriptive error messages on every bad request
- Python 3.10+ — all heuristic analyzers are pure stdlib Python with no heavy NLP dependencies, keeping the install footprint small and the analysis path fully auditable
- OpenAI
text-embedding-3-small— lightweight, cost-efficient embedding model used for the optional semantic scoring layer; isolated from the heuristic path so the API degrades gracefully when unavailable python-dotenv— loadsOPENAI_API_KEYfrom a local.envfile for development; production key is set via Render environment variables- Render — zero-config deployment from GitHub; auto-deploys on push, keeps the API publicly accessible without any infrastructure management
- Idempotent — heuristic scores are fully deterministic. The same text submitted twice will always return the exact same six scores and breakdowns, with no randomness or model state involved.
- Stateful embeddings warm-up — on the first request with embeddings enabled, the server embeds a small set of curated seed examples and caches the resulting bait and neutral centroids in memory. Every subsequent embeddings request uses those cached centroids — no repeated seed embedding calls. The
/healthendpoint exposes whether the OpenAI path is live. - Graceful degradation — if OpenAI is unavailable or the key is missing, all six heuristic metrics still return normally. The
metaobject in every response reports exactly what ran and why.
The API is live at https://engagbaitapi.onrender.com. No API key or installation needed for heuristic scoring.
# Health check
curl https://engagbaitapi.onrender.com/health
# Analyze text (heuristics only)
curl -s -X POST "https://engagbaitapi.onrender.com/analyze?embeddings=false" -H "Content-Type: application/json" -d '{"text": "Act now. This is your last chance to see the truth before it disappears. Everyone knows they are lying, and if you do not share this immediately, more people will be fooled. There is no middle ground, and the answer is obvious."}'
# Analyze text with embeddings score
curl -s -X POST "https://engagbaitapi.onrender.com/analyze?embeddings=true" -H "Content-Type: application/json" -d '{"text": "Act now. This is your last chance to see the truth before it disappears. Everyone knows they are lying, and if you do not share this immediately, more people will be fooled. There is no middle ground, and the answer is obvious."}'python -m venv venv
venv\Scripts\activate
pip install -r requirements.txt
uvicorn app.main:app --reload --host 0.0.0.0 --port 5000Local configuration:
- Copy
.env.exampleto.env - Add your
OPENAI_API_KEY(required only for embeddings scoring)
Local URLs:
- API:
http://localhost:5000 - Docs:
http://localhost:5000/docs - Demo:
http://localhost:5000/demo
Base URL: https://engagbaitapi.onrender.com
| Method | Path | Description |
|---|---|---|
| GET | / |
API overview and key links |
| GET | /health |
Health status and OpenAI availability |
| GET | /demo |
Lightweight browser demo |
| POST | /analyze |
Analyze one text |
| POST | /analyze/batch |
Analyze up to 10 texts |
POST /analyze
Request body:
{
"text": "Your text to analyze..."
}Query param:
embeddings=true|false- If omitted, embeddings defaults to on only when
OPENAI_API_KEYis available
Constraints:
- text length: 50 to 50,000 characters
Example (curl):
curl -s -X POST "https://engagbaitapi.onrender.com/analyze?embeddings=false" \
-H "Content-Type: application/json" \
-d '{"text": "Act now. This is your last chance to see the truth before it disappears. Everyone knows they are lying, and if you do not share this immediately, more people will be fooled. There is no middle ground, and the answer is obvious."}'Example (PowerShell, local):
Invoke-RestMethod -Method Post -Uri "http://localhost:5000/analyze?embeddings=false" -ContentType "application/json" -Body (@{ text = "Act now. This is your last chance to see the truth before it disappears. Everyone knows they are lying, and if you do not share this immediately, more people will be fooled. There is no middle ground, and the answer is obvious." } | ConvertTo-Json -Compress)Example response:
{
"urgency_pressure": {
"score": 0.69,
"breakdown": {
"time_pressure": 1.0,
"scarcity": 0.0,
"fomo": 0.67
}
},
"evidence_density": {
"score": 1.0,
"breakdown": {
"citations": 1.0,
"stats": 1.0,
"external_sources": 1.0
}
},
"arousal_intensity": {
"score": 0.35,
"breakdown": {
"emotion_words": 0.67,
"exclamation_density": 0.0,
"question_density": 0.0,
"caps_ratio": 0.0,
"moralized_language": 0.75,
"superlative_density": 0.0,
"curiosity_gap": 1.0
}
},
"counterargument_absence": {
"score": 1.0,
"breakdown": {
"tradeoff_absence": 1.0,
"conditional_absence": 1.0
}
},
"claim_volume_vs_depth": {
"score": 0.62,
"breakdown": {
"claims_per_word": 1.0,
"explanation_depth": 0.13,
"listicle": 0.0
}
},
"lexical_diversity": {
"score": 0.21,
"breakdown": {
"mattr": 0.79,
"type_token_ratio": 0.79
}
},
"engagement_bait_score": null,
"meta": {
"embeddings_requested": false,
"embeddings_used": false,
"openai_available": false,
"vector_backend": "none"
}
}Notes:
evidence_densityis an inverted bait signal. Higher values mean the text includes less evidence, not more.counterargument_absenceis also bait-oriented. Higher values mean the text does less to acknowledge tradeoffs, conditions, or competing considerations.
POST /analyze/batch
Submit up to 10 texts in one request. The response preserves submission order and includes each caller-supplied id.
Request body:
{
"items": [
{ "id": "high", "text": "First text..." },
{ "id": "neutral", "text": "Second text..." }
]
}Rules:
- at least 1 item
- at most 10 items
- each item must satisfy the same text length validation as
/analyze
Example (curl):
curl -s -X POST "https://engagbaitapi.onrender.com/analyze/batch?embeddings=false" \
-H "Content-Type: application/json" \
-d '{
"items": [
{"id": "bait", "text": "Act now. This is your last chance to see the truth before it disappears. Everyone knows they are lying, and if you do not share this immediately, more people will be fooled. There is no middle ground, and the answer is obvious."},
{"id": "neutral", "text": "A review of three transit funding proposals found that ridership increased between 12 and 18 percent in pilot cities. The report recommends further study before statewide adoption and notes that two of the five authors issued a minority opinion citing timeline risks."}
]
}'All validation errors return HTTP 422 with this shape:
{"detail": "...", "field": "..."}| Trigger | detail value |
|---|---|
| text under 50 characters | "Text must be at least 50 characters (got N)" |
| text over 50,000 characters | "Text must be at most 50000 characters (got N)" |
| empty batch | "Batch must include at least 1 item" |
| batch over 10 items | "Batch must include at most 10 items" |
The meta object is included in every response and reports the state of the embeddings path:
"meta": {
"embeddings_requested": true,
"embeddings_used": false,
"openai_available": false,
"vector_backend": "none"
}Field meanings:
embeddings_requested— whether the request asked for embeddings scoringembeddings_used— whether the embeddings path actually ranopenai_available— whether the server has a valid OpenAI key configuredvector_backend—none(heuristic only) orcentroid(embeddings used)
Live demo: https://engagbaitapi.onrender.com/demo
Locally: GET /demo
It includes:
- paste-in text analysis
- a
Use Embeddingstoggle - three sample inputs: high bait, neutral, and mixed text
- score cards for all six metrics
- raw JSON output for developer inspection
The OpenAI portion of the project is intentionally narrow and opt-in:
- embeddings power
engagement_bait_scoreonly - the scorer uses centroid similarity over curated bait and neutral seed examples
- centroids are computed once on first use and cached in memory for the server's lifetime
- heuristic metrics remain the explainable, deterministic layer
- if OpenAI is unavailable, the API still returns all heuristic results cleanly and reports the reason in
meta
The project includes an internal benchmark runner for reviewing embeddings behavior on curated examples.
Run:
python -m scripts.run_ml_benchmarkRequirements:
- a valid
OPENAI_API_KEY - benchmark examples in
data/ml_benchmark_examples.json
The script prints per-example: id, label, embeddings score, all heuristic scores, response metadata, and notes for manual review.
This is a manual evaluation tool, not a normal CI test.
Run the automated tests with:
python -m pytest -q- Fact check claims
- Classify political ideology
- Detect truthfulness
- Perform content moderation
The heuristic metrics are transparent signal extractors, not a unified engagement bait score. They are intentionally kept separate so callers can interpret each signal on its own terms.
This submission is currently optimized for:
- Stripe Track
- Best Use of OpenAI API