Deenclaw 101
A complete walkthrough of how Deenclaw works, every Quran Foundation API it talks to, and the architecture that glues it together inside WhatsApp.
Overview
Deenclaw is a WhatsApp-native AI assistant that helps Muslims build a consistent relationship with the Quran. You add the number as a contact, send a message, and the bot becomes a study partner, a daily-routine nudge, and a Quran search engine that never invents a single verse from memory.
It was built for the Quran Foundation Hackathon. The product goal is straightforward: most people read the Quran less than they would like to. A bot that lives in the messaging app they already check fifty times a day, drives a verse a day, nudges before each salah, and answers honest questions with sourced material is closer to their thumb than any standalone app could be.
Hard rule the entire system is built around: every ayah, translation, tafsir, and audio file comes from a live API call to Quran Foundation infrastructure. The language model is never allowed to generate or paraphrase Quranic text. If a tool call fails, the bot says so rather than reconstructing the verse from training data.
Architecture
Three processes cooperate to turn a WhatsApp message into a sourced, multi-modal reply:
WhatsApp DM or group
|
v
Node bridge (sidecar)
|
v
FastAPI process (this repo)
+-- agent.loop (LLM dispatch + tool loop)
+-- skills/* (registered tools)
+-- scheduler.jobs (APScheduler async)
+-- db.store (SQLite, single file)
|
+-- Claude Haiku 4.5 (claude-haiku-4-5-20251001)
+-- QF Content API apis.quran.foundation/content/api/v4
+-- QF User API apis.quran.foundation/auth/v1
+-- QF MCP mcp.quran.ai
+-- Aladhan api.aladhan.com/v1
Inbound WhatsApp messages land on a Node sidecar process that normalises them into a small JSON envelope and forwards to a single FastAPI entrypoint. The FastAPI app calls agent.loop.handle_message, which is the core dispatch routine. The LLM (Anthropic Claude Haiku 4.5 by default, provider-swappable via LLM_PROVIDER) sees the registered tool catalogue and either replies directly or emits tool-use turns that the loop dispatches into the skill modules.
Outbound replies are sent back through the same Node sidecar, which addresses the recipient and persists delivery state. The entire conversation, including mid-loop tool-call and tool-result turns, is stored as raw JSON in conversation_history so the bot can resume context across sessions.
The core agent codebase (loop, registry, prompts, transport) is capped at 600 lines of Python. Everything domain-specific lives in skills/.
QF Content API
The Content API is the read-only catalogue of Quranic data: verses, translations, tafsir, recitations, juz/hizb/ruku/manzil indices, and metadata for every reciter and translator on Quran.com. It is the largest single surface Deenclaw consumes.
Authentication
Service-to-service OAuth2 with the client_credentials grant. Deenclaw acquires a token at the token endpoint and replays it on every Content API request:
- Token endpoint:
POST https://oauth2.quran.foundation/oauth2/tokenwith HTTP Basic auth (client_id:client_secret) and form bodygrant_type=client_credentials&scope=content. - Token TTL: 3599 seconds. The skill caches it in-process and re-acquires on expiry.
- Per-request headers:
x-auth-token: <token>,x-client-id: <QF_CONTENT_CLIENT_ID>,Accept: application/json.
Locked resource IDs
To keep behaviour reproducible across sessions and consistent with what users expect, three resource IDs are hard-pinned:
- Translation: id 20, Saheeh International (English). Most-cited English rendering on Quran.com.
- Tafsir: id 169, Ibn Kathir Abridged (English). Most widely used English tafsir on Quran.com.
- Reciter: id 7, Mishari Rashid al-Afasy. Also the row default in
db/schema.sql.
Endpoints in use
Base URL: https://apis.quran.foundation/content/api/v4. Audio URLs returned by the API are relative (e.g. Alafasy/mp3/002255.mp3) and prefixed with https://verses.quran.com/ before being sent to the user.
| Endpoint | Purpose | Tool |
|---|---|---|
/verses/by_key/{key} | Fetch one verse with optional fields, translations, tafsirs, audio, words. | get_verse, get_translation, get_tafsir, get_audio, get_verse_in_script |
/verses/by_chapter/{n} | Surrounding verses for context. | get_surrounding_verses |
/resources/translations | Catalogue every translation, filtered by language. | list_translations |
/resources/tafsirs | Catalogue every tafsir, filtered by language. | list_tafsirs |
/resources/recitations | Catalogue every reciter (id, name, style). | list_recitations |
/resources/languages | Catalogue every supported translation language. | list_languages |
/recitations/{id}/by_ayah/{key} | Per-reciter audio URL for one verse. | get_audio (per-reciter path) |
/quran/recitations/{id} | Per-word audio segment timings. | get_audio_segments |
/juzs/{n}, /hizbs/{n}, /rukus/{n}, /manzils/{n} | Quranic divisions for navigation and study. | get_juz_info, get_hizb_info, get_ruku_info, get_manzil_info |
Translations and tafsirs are HTML-bearing. The skill strips tags before display so plain WhatsApp text never leaks raw markup.
QF User API and OAuth
The User API holds per-user state on the Quran Foundation gateway: bookmarks, collections, reading sessions, goals, reflections, notes, and the daily streak. Every call is bound to a user identity, which means OAuth.
OAuth2 + PKCE + OIDC flow
Deenclaw uses the Authorization Code grant with PKCE (S256) and OpenID Connect. Discovery and token endpoints come from the pre-production tenant:
- Discovery:
GET https://prelive-oauth2.quran.foundation/.well-known/openid-configuration - Authorize:
https://prelive-oauth2.quran.foundation/oauth2/auth - Token:
https://prelive-oauth2.quran.foundation/oauth2/token - JWKS:
https://prelive-oauth2.quran.foundation/.well-known/jwks.json - Userinfo:
https://prelive-oauth2.quran.foundation/userinfo
Linking UX
The user-facing path is intentionally one sentence:
- User asks for a bookmark, streak, or anything that needs identity.
- Bot replies with a short message and an authorization URL.
- User taps the link, signs in on the Quran.com hosted page, and authorises the requested scopes.
- The hosted page redirects back to
/callbackwith acodeandstate. The FastAPI handler validatesstateagainst the row stored inoauth_state, exchanges the code for an access + refresh token, persists them on the user row, and sends a confirmation message in WhatsApp. - Subsequent tool calls now have a token. The transport transparently refreshes when expiry is within sixty seconds.
Refresh tokens are long-lived; the access token is short-lived and the skill catches 401 responses, forces a refresh, and retries the request once.
Requested scopes
openid offline_access bookmark collection reading_session preference goal streak post.create post.read note.create note.read
Endpoints in use
Base URL: https://apis.quran.foundation/auth/v1. Per-request headers: x-auth-token: <user_access_token>, x-client-id: <QF_USER_CLIENT_ID>, Accept: application/json. QuranReflect read endpoints live behind /quran-reflect/v1/ on the same host.
| Endpoint | Purpose | Tool |
|---|---|---|
POST /bookmarks, GET /bookmarks, DELETE /bookmarks/{id} | Save, list, remove bookmarks on a verse key. | save_bookmark, list_bookmarks, remove_bookmark |
GET /streak, POST /streak | Read and silently bump the daily streak (any inbound message counts, per spec). | get_streak |
POST /collections, GET /collections, POST /collections/{id}/items | Create a collection, list collections, add a verse to one. | create_collection, list_collections, add_to_collection |
POST /reading_sessions | Log a hifz or reading session by surah and verse range. | log_reading_session, get_reading_progress |
POST /goals, GET /goals/current | Create a reading goal, read current progress. | create_goal, get_goal_progress |
POST /posts | Publish a reflection to QuranReflect. | post_reflection |
POST /notes, GET /notes | Private per-verse notes. | note_create, list_notes |
GET /posts/feed (via /quran-reflect/v1) | Community reflections on a verse. | community_reflections |
QF MCP server
The Quran MCP server is an unauthenticated streamable-http endpoint that exposes higher-level Quranic capabilities (semantic search, word morphology, mushaf rendering) as MCP tools. Deenclaw treats it as a peer of the Content API.
- URL:
https://mcp.quran.ai/ - Auth: none. Streamable-http, JSON-RPC over POST with Server-Sent Events for streaming results.
- Session:
initializereturns anmcp-session-idheader. Every subsequent call must replay it.
Grounding nonce pattern
On a fresh session the skill calls fetch_grounding_rules first. The server returns a short rules block ending in <grounding_nonce>...</grounding_nonce>. The skill caches that nonce and passes it on subsequent calls. Tools that accept the nonce save a non-trivial number of tokens by skipping the inlined rules block; tools that do not accept it eat the rules every call.
Tools used
| MCP tool | Purpose | Deenclaw skill |
|---|---|---|
fetch_grounding_rules | Bootstraps the grounding nonce. | called by all MCP tools at warm-up |
fetch_quran | Fetch Arabic text by reference. | fallback path for verse fetch |
search_quran | Semantic search across the Quran. | search_quran |
fetch_translation | Fetch translation text by reference. | fallback path for translation fetch |
search_translation | Semantic search across translations. | search_translation |
fetch_tafsir | Fetch tafsir text by reference. | fallback path for tafsir fetch |
search_tafsir | Semantic search across tafsir bodies. | search_tafsir |
fetch_skill_guide | Server-side prompt for how to use the MCP tools. | read at provisioning time |
The skill also wraps MCP-side word concordance, morphology, paradigm, and mushaf-page lookups behind the same transport.
Aladhan and Hijri date
Aladhan is a free, no-auth prayer-times API at https://api.aladhan.com/v1/. Deenclaw uses it for per-user salah times and a few supporting endpoints.
| Endpoint | Purpose | Tool |
|---|---|---|
GET /timingsByCity | Resolve daily timings by city + country + calc method. | get_prayer_times |
GET /timings | Same, by latitude and longitude. | get_prayer_times (coords path) |
GET /gToH, GET /hToG | Gregorian to Hijri and back. | get_hijri_date |
GET /qibla/{lat}/{lng} | Qibla direction from a coordinate. | get_qibla |
GET /hijriCalendar | Upcoming Islamic holidays. | islamic_holidays_upcoming |
String method IDs are used throughout (ISNA, MWL, EGYPT, MAKKAH, KARACHI, JAFARI, and the rest of the Aladhan catalogue). The default is ISNA. The five canonical prayers Deenclaw surfaces are Fajr, Dhuhr, Asr, Maghrib, Isha; the API also returns Sunrise, Sunset, Imsak, Midnight, Firstthird, Lastthird, which are available to the LLM but not used for scheduled nudges.
The Asma al-Husna lookup (asma_al_husna_today, asma_al_husna_by_number) is a local rotation of the ninety-nine names, anchored to the day of year so every user sees the same name on the same date.
Skill catalogue
Every capability is a registered tool. The agent loop never hard-codes a tool name; it only sees what the registry exposes via get_schemas(). Handlers are async and must return a string. They never raise to the loop: on failure they return a plain-English error the LLM can recover from.
skills/quran_content.py
Content API surface. Nineteen tools covering single-verse fetch, translation, tafsir, audio, surrounding-verse windows, random verse, daily ayah pick, juz / hizb / ruku / manzil lookups, comparison across translations and tafsirs, audio segment timings, and the catalogue resources (list_translations, list_tafsirs, list_recitations, list_languages).
skills/quran_user.py
User API surface. Fourteen tools: bookmarks (save / list / remove), streak (read + silent bump), collections (create / list / add), reading sessions, goals, QuranReflect reflections, private notes, and the community feed for a verse.
skills/quran_mcp.py
MCP surface. Search across the Quran, translations, and tafsir; word morphology and concordance; mushaf-page lookup and rendering.
skills/prayer_times.py
Aladhan surface for salah. get_prayer_times, get_pre_prayer_dua (pre-salah dua bundled from the content library), get_hijri_date.
skills/aladhan_extras.py
Auxiliary religious tools. Asma al-Husna (name of the day, by number), get_qibla, islamic_holidays_upcoming.
skills/sunnah.py
Silat al-rahim, the family-ties sunnah. Users pick this practice on first ask; the skill tracks family members the user wants to stay in touch with and emits gentle nudges. Tools: setup_family_ties, add_family_member, remove_family_member, list_family_members, mark_family_reached_out, pick_a_sunnah_to_offer.
Scheduler
One AsyncIOScheduler runs in the same process as the watcher. Each tick is a fan-out over onboarded users; per-user failures are caught so one bad row never kills a tick. All scheduled jobs call skill API functions directly. The LLM is only invoked for short reflection prompts on the daily ayah.
| Job | Trigger | Effect |
|---|---|---|
send_daily_ayah_tick | Every 60 seconds | For each user whose local clock matches delivery_time, pick today deterministic verse, fetch Arabic + translation + audio, send with a short reflection question. |
send_word_of_day_tick | 08:00 UTC daily | Pick a Quranic word for the day and fan out to all users. |
send_asma_broadcast_tick | 08:30 UTC daily | Send today Name of Allah (Asma al-Husna) to all users. |
check_prayer_times_tick | Every 60 seconds | For each user with a city set, check whether the current minute is inside the 9 to 11 minute window before any of the five canonical prayers. If yes, send a nudge with a pre-salah dua. Dedup row in prayer_nudge_log. |
check_streaks_tick | 20:00 UTC daily | Nudge connected users whose streak would break today. |
family_nudge_tick | Every 60 seconds | For users who adopted silat al-rahim, suggest one or two family members they have not contacted in a while. |
cleanup_tick | 03:00 UTC daily | Prune stale oauth_state rows, trim long conversation histories, and roll the daily log. |
Database schema
One SQLite file. All CREATE TABLE statements are idempotent so init can run on every boot.
| Table | Purpose |
|---|---|
users | One row per WhatsApp user. Phone is the primary key. Holds preferences (reciter, delivery time, timezone, city, calc method, language), onboarding state, and the OAuth token bundle (qf_sub, access_token, refresh_token, token_expiry). |
conversation_history | Raw JSON message log, including mid-loop tool-use and tool-result turns. Indexed by phone. |
chats | Per-thread metadata: chat GUID, group flag, mute state, optional nickname. Lets /quiet and /wake be persistent. |
daily_log | Dedup guard for the daily-ayah scheduler. One row per (phone, date). |
prayer_nudge_log | Dedup guard for the prayer-nudge scheduler. One row per (phone, date, prayer). |
family_members | Silat al-rahim tracking: name, optional relationship, date added, last contacted timestamp. |
family_suggestion_log | Dedup guard for the family-nudge scheduler. |
oauth_state | OAuth roundtrip state. Holds state, nonce, PKCE verifier, and the phone the redirect belongs to. Pruned on cleanup tick. |
Agent loop
The default model is Anthropic Claude Haiku 4.5 (claude-haiku-4-5-20251001). Provider is selectable via LLM_PROVIDER so the loop can swap in a different backend without skill changes.
Per inbound message, the loop:
- Resolves the user row (
ensure_usercreates one on first contact). - If the user is mid-onboarding, runs one deterministic onboarding step. Onboarding is a five-question state machine (init, ask_name, location, location_city, done) and never invokes the LLM.
- If the user is onboarded and sent a slash command in DM (
/profile,/restart,/disconnect,/help), runs that command directly and returns. - Otherwise: bumps the streak best-effort, loads recent history, appends the new user turn, persists it, and enters the LLM dispatch loop.
- The dispatch loop calls the provider. If the model is done, it sends the reply and persists it. If the model emitted tool-use turns, each tool is dispatched, the result persisted as a tool turn, and the loop continues. A runaway guard caps iterations at eight.
Group chats
Group dispatch lives in agent.group. The bot stays quiet by default and only responds when mentioned by name (deenclaw) or addressed via reply. Slash commands /quiet, /wake, and /help always run, even in muted threads. Group history is keyed on the chat GUID so personal DMs and group threads never bleed context into each other.
Memory and prompts
Each user has a tiny memory file kept across conversations. The system prompt is rebuilt on every turn from current user preferences plus that memory; it never names the LLM provider and never embeds Quranic text.
The unbreakable rule
Never generate, paraphrase, or reconstruct Quranic text or translations from memory. Every ayah, translation, and tafsir must come from a tool call. If a tool fails, say so.
Privacy and data
Deenclaw stores only what is needed to deliver the service.See the Privacy Policy for the full text.
What is stored
- WhatsApp phone number, on the
usersrow. - Onboarding info you opted to share (name, city, timezone, reciter, language, calc method).
- Conversation history as raw JSON so the bot can resume.
- Sunnah commitments such as family members you asked the bot to nudge you about.
- OAuth tokens for your linked Quran.com account. The token is opaque; passwords are never seen or stored by Deenclaw.
What is never logged
- OAuth client secrets, user access tokens, or refresh tokens in commits, error messages, or chat output.
- Anything in the
.envfile. - The
memory/directory and*.dbare gitignored and never committed.
What is never done
- No selling data to third parties.
- No advertising on user data.
- No training models on user messages.
Deletion
Send delete my data to the bot. Account and history are removed within thirty days.