DeenclawGitHub

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/token with HTTP Basic auth (client_id:client_secret) and form body grant_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.

EndpointPurposeTool
/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/translationsCatalogue every translation, filtered by language.list_translations
/resources/tafsirsCatalogue every tafsir, filtered by language.list_tafsirs
/resources/recitationsCatalogue every reciter (id, name, style).list_recitations
/resources/languagesCatalogue 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:

  1. User asks for a bookmark, streak, or anything that needs identity.
  2. Bot replies with a short message and an authorization URL.
  3. User taps the link, signs in on the Quran.com hosted page, and authorises the requested scopes.
  4. The hosted page redirects back to /callback with a code and state. The FastAPI handler validates state against the row stored in oauth_state, exchanges the code for an access + refresh token, persists them on the user row, and sends a confirmation message in WhatsApp.
  5. 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.

EndpointPurposeTool
POST /bookmarks, GET /bookmarks, DELETE /bookmarks/{id}Save, list, remove bookmarks on a verse key.save_bookmark, list_bookmarks, remove_bookmark
GET /streak, POST /streakRead and silently bump the daily streak (any inbound message counts, per spec).get_streak
POST /collections, GET /collections, POST /collections/{id}/itemsCreate a collection, list collections, add a verse to one.create_collection, list_collections, add_to_collection
POST /reading_sessionsLog a hifz or reading session by surah and verse range.log_reading_session, get_reading_progress
POST /goals, GET /goals/currentCreate a reading goal, read current progress.create_goal, get_goal_progress
POST /postsPublish a reflection to QuranReflect.post_reflection
POST /notes, GET /notesPrivate 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: initialize returns an mcp-session-id header. 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 toolPurposeDeenclaw skill
fetch_grounding_rulesBootstraps the grounding nonce.called by all MCP tools at warm-up
fetch_quranFetch Arabic text by reference.fallback path for verse fetch
search_quranSemantic search across the Quran.search_quran
fetch_translationFetch translation text by reference.fallback path for translation fetch
search_translationSemantic search across translations.search_translation
fetch_tafsirFetch tafsir text by reference.fallback path for tafsir fetch
search_tafsirSemantic search across tafsir bodies.search_tafsir
fetch_skill_guideServer-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.

EndpointPurposeTool
GET /timingsByCityResolve daily timings by city + country + calc method.get_prayer_times
GET /timingsSame, by latitude and longitude.get_prayer_times (coords path)
GET /gToH, GET /hToGGregorian to Hijri and back.get_hijri_date
GET /qibla/{lat}/{lng}Qibla direction from a coordinate.get_qibla
GET /hijriCalendarUpcoming 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.

JobTriggerEffect
send_daily_ayah_tickEvery 60 secondsFor 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_tick08:00 UTC dailyPick a Quranic word for the day and fan out to all users.
send_asma_broadcast_tick08:30 UTC dailySend today Name of Allah (Asma al-Husna) to all users.
check_prayer_times_tickEvery 60 secondsFor 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_tick20:00 UTC dailyNudge connected users whose streak would break today.
family_nudge_tickEvery 60 secondsFor users who adopted silat al-rahim, suggest one or two family members they have not contacted in a while.
cleanup_tick03:00 UTC dailyPrune 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.

TablePurpose
usersOne 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_historyRaw JSON message log, including mid-loop tool-use and tool-result turns. Indexed by phone.
chatsPer-thread metadata: chat GUID, group flag, mute state, optional nickname. Lets /quiet and /wake be persistent.
daily_logDedup guard for the daily-ayah scheduler. One row per (phone, date).
prayer_nudge_logDedup guard for the prayer-nudge scheduler. One row per (phone, date, prayer).
family_membersSilat al-rahim tracking: name, optional relationship, date added, last contacted timestamp.
family_suggestion_logDedup guard for the family-nudge scheduler.
oauth_stateOAuth 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:

  1. Resolves the user row (ensure_user creates one on first contact).
  2. 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.
  3. If the user is onboarded and sent a slash command in DM (/profile, /restart, /disconnect, /help), runs that command directly and returns.
  4. Otherwise: bumps the streak best-effort, loads recent history, appends the new user turn, persists it, and enters the LLM dispatch loop.
  5. 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 users row.
  • 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 .env file.
  • The memory/ directory and *.db are 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.