Skip to content

feat: wire zakat into chat, live nisab pricing, and test coverage (#22) - #68

Merged
zeemscript merged 1 commit into
Deen-Bridge:devfrom
Fury03:feat/issue-22-zakat-v2
Jul 24, 2026
Merged

feat: wire zakat into chat, live nisab pricing, and test coverage (#22)#68
zeemscript merged 1 commit into
Deen-Bridge:devfrom
Fury03:feat/issue-22-zakat-v2

Conversation

@Fury03

@Fury03 Fury03 commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Closes #22

Zakat v1 shipped a working /zakat endpoint the assistant itself couldn't use, a nisab frozen in an env var, and no tests. This closes all three.

Live nisab — nisab.py

The nisab is the value of 85g of gold, so it moves with the market: a hardcoded figure drifts wrong in both directions, telling some users they owe nothing when they do and vice versa. It is now derived from a spot price:

  1. gold-api.com — spot XAU in USD per troy ounce
  2. CoinGecko / PAX Gold — PAXG is redeemable one-for-one for a troy ounce of allocated gold, so it tracks spot; used only if the primary is down
  3. ZAKAT_NISAB_USD — the configured default, when neither is reachable

Neither API needs a key. Prices are cached for NISAB_CACHE_TTL_SECONDS (6h default) through the existing semantic_cache.KeyedCache — I added an optional per-entry TTL rather than a new cache, since a market price is worth caching for hours, not for the day-long TTL that suits an immutable tafsir passage.

A price outside a plausible range ($100–$100,000/oz) is refused rather than used: if a source changes its units or field meaning, the right outcome is degrading to the next source, not silently producing a nisab off by an order of magnitude. Every quote reports live: true/false and its source, so a fallback is never mistaken for a market figure. The 85g-gold basis is stated explicitly in the response — silver gives a substantially different threshold, and which applies is a matter scholars differ on.

Chat integration

A zakat question carrying a Stellar public key — in the prompt, or in the request's context field as the frontend passes it — now reads the real on-chain balance and hands the model the actual figures plus an instruction to close with the existing scholar disclaimer verbatim. Without a key, the assistant explains how zakat is calculated and invites a public key.

Detection is offline (keywords plus a key-shaped match), so an ordinary message reaches neither Horizon nor the price API — there's a test asserting exactly that. A zakat answer is also never written to the response cache: it contains one user's real balance and must never be replayed to anyone else. ChatResponse gains an additive optional zakat block.

Verified end-to-end against a live gold price with a mocked balance: a 50,000 USDC wallet against a $4,063.70/oz gold price gives a nisab of $11,105.33 and 1,250 USDC due, and the model prompt carries the real figures rather than an example.

Read-only, and harder to misuse

Secret keys already failed validation, so one could never reach Horizon. Two gaps are now closed:

  • A message that looks like it contains a secret key is refused rather than used, and the assistant is told to warn the user to treat the key as compromised — without repeating it back.
  • The key is redacted before it reaches the model provider or stored history. This module promises secret keys are never stored or logged; that promise has to survive a user volunteering one into the chat. Confirmed: the secret appears in neither the outgoing prompt, the returned history, nor the logs.

Also fixed while in here

  • The synchronous stellar_sdk.Server call ran directly inside an async endpoint, blocking the event loop for every other in-flight request. It now runs via asyncio.to_thread, concurrently with the nisab fetch.
  • Zakat is quantized to USDC's 7 decimal places (the smallest unit the asset can represent) rather than an arbitrary constant.

Tests — tests/test_zakat.py, 79 offline

Zakat math above / below / exactly at the nisab; that 2.5% applies to the whole balance rather than the excess (a common misreading, asserted explicitly); quantization and large-balance exactness; key validation including bad checksums and secret-key rejection; Horizon mocked for the trustline / no-trustline / wrong-issuer / unknown-account paths; the recorded response shape; live nisab across both sources and every fallback path including an implausible price; cache behaviour, including that a fallback is not cached so an outage can't pin the default in place for hours; chat detection; and redaction.

No live network calls — Horizon is mocked and the price API is stubbed with httpx.MockTransport. CI extends flake8/compile to nisab.py and adds the new pytest step. Full suite: 541 passed, 1 skipped.

Note

README.md documents the price sources, the fallback chain, and the chat behaviour. This branch also touches main.py and .env.example, which #67 (docs) touches too — whichever merges second, I'll resolve.

Summary by CodeRabbit

  • New Features

    • Added live gold-price-based nisab calculations with caching and configurable fallback values.
    • Enhanced the zakat endpoint with nisab details, balance calculations, and optional caller overrides.
    • Added zakat support in chat, including public-key extraction and secret-key protection.
    • Documented new configuration options and zakat API behavior.
  • Bug Fixes

    • Improved handling of unavailable or implausible gold-price data through safe fallback behavior.
  • Tests

    • Added comprehensive coverage for zakat calculations, nisab retrieval, Stellar balances, validation, and chat integration.

…erage

Zakat v1 shipped a working /zakat endpoint that the assistant itself could
not use, with a nisab frozen in an env var and no automated tests.

Live nisab (nisab.py)
The nisab is the value of 85g of gold, so it moves with the gold market and
a hardcoded figure drifts wrong in both directions. It is now derived from a
spot price via gold-api.com, falling back to CoinGecko's PAX Gold (redeemable
one-for-one for a troy ounce of allocated gold) and then to ZAKAT_NISAB_USD.
Neither source needs a key. Prices are cached for NISAB_CACHE_TTL_SECONDS,
and a price outside a plausible range is refused rather than used, so a
source that changes its units degrades instead of producing a nisab off by
an order of magnitude. Every quote reports whether it is live and where it
came from, so a fallback is never mistaken for a market figure.

Chat integration
A zakat question carrying a Stellar public key — in the prompt or in the
request's context field — now reads the real on-chain balance and hands the
model the actual figures, the nisab basis, and an instruction to close with
the existing scholar disclaimer verbatim. Without a key the assistant
explains the calculation and invites a public key. Detection is offline
(keywords plus a key-shaped match), so an ordinary message reaches neither
Horizon nor the price API, and a zakat answer is never written to the
response cache: it contains one user's real balance.

Read-only, and harder to misuse
Secret keys already failed validation. Two gaps are now closed: a message
that looks like it contains one is refused with a warning rather than used,
and the key is redacted from the prompt before it reaches the model provider
or the stored history — the module promises secret keys are never stored or
logged, and that has to survive a user volunteering one.

Also: the synchronous Horizon call now runs off the event loop rather than
blocking every other in-flight request, and zakat arithmetic is quantized to
USDC's 7 decimal places.

Tests (tests/test_zakat.py, 79 offline)
Zakat math above/below/exactly at nisab, that 2.5% applies to the whole
balance rather than the excess, quantization, key validation including
secret-key rejection, Horizon mocked for the trustline / no-trustline /
wrong-issuer / unknown-account paths, the response shape, live nisab with
both sources and every fallback, cache behaviour, chat detection, and
redaction. No live network calls.

Closes Deen-Bridge#22
@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Adds live gold-price nisab calculation with fallback and caching, refactors Stellar zakat computation into reusable helpers, integrates zakat context into chat with secret-key redaction, and adds extensive offline tests and CI coverage.

Changes

Zakat v2

Layer / File(s) Summary
Live nisab pricing and caching
nisab.py, semantic_cache.py, .env.example, tests/test_zakat.py
Calculates the 85g gold nisab from validated primary or fallback price sources, caches successful live quotes with configurable TTL, and falls back to the configured default or caller override.
Shared Stellar zakat calculation
stellar.py, README.md, tests/test_zakat.py
Centralizes public-key validation, USDC balance lookup, quantized zakat calculation, nisab-aware response construction, and /zakat routing.
Chat zakat detection and grounding
stellar.py, README.md, tests/test_zakat.py
Detects zakat questions, extracts public keys, rejects and redacts secret keys, and produces structured zakat data and grounding prompt content.
Chat endpoint integration
main.py, .github/workflows/ci.yml, tests/test_zakat.py
Adds zakat retrieval to /chat, excludes zakat turns from semantic caching, propagates redacted inputs, exposes ChatResponse.zakat, and runs the new test suite in CI.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

Suggested reviewers: zeemscript, dayz-tech-co

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 29.41% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: chat zakat integration, live nisab pricing, and added tests.
Linked Issues check ✅ Passed The PR implements chat zakat wiring, live nisab pricing with fallback/caching, test coverage, and public-key-only handling.
Out of Scope Changes check ✅ Passed The changes stay focused on zakat chat wiring, nisab pricing, CI, docs, cache TTL handling, and tests.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
stellar.py (1)

219-244: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Minor false-positive in keyword detection: "alms" matches "psalms".

is_zakat_question does a plain substring scan, so a prompt like "explain the psalms" trips the zakat path (because "alms""psalms"). It's low-harm — the worst case is an unsolicited NO_KEY_NOTE explanation — but if you want to tighten it, a word-boundary regex over the keyword set avoids the surprise. Entirely optional.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@stellar.py` around lines 219 - 244, Update is_zakat_question to match
ZAKAT_KEYWORDS as whole words rather than using substring checks, preventing
“alms” from matching words such as “psalms” while preserving detection of
standalone keywords. Reuse the existing keyword set and keep the current
empty-text handling.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@stellar.py`:
- Around line 219-244: Update is_zakat_question to match ZAKAT_KEYWORDS as whole
words rather than using substring checks, preventing “alms” from matching words
such as “psalms” while preserving detection of standalone keywords. Reuse the
existing keyword set and keep the current empty-text handling.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 8aae2354-8e26-43e4-8ea5-af4607eac63f

📥 Commits

Reviewing files that changed from the base of the PR and between 06981f2 and e83d48e.

📒 Files selected for processing (8)
  • .env.example
  • .github/workflows/ci.yml
  • README.md
  • main.py
  • nisab.py
  • semantic_cache.py
  • stellar.py
  • tests/test_zakat.py

@zeemscript
zeemscript merged commit aba4995 into Deen-Bridge:dev Jul 24, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants