Skip to content

SquadTrust/squadtrustbackend

Repository files navigation

SquadTrust Backend

Owners: Abideen (API + Escrow + Edge Cases) · Iseoluwa (AI Layer + Trust Score + Research)
Stack: FastAPI · PostgreSQL 15 · SQLAlchemy 2.0 · Alembic · httpx · Anthropic Claude API


Your 7-Day Build Plan

Tonight — Day 0

Abideen — Prompt 0.1: Squad Sandbox Connectivity Test

Time: 45 minutes · Depends on: Your sandbox_sk_ key (get it from Squad merchant dashboard → API & Webhooks → Sandbox)

Key distinction: You have two keys in the dashboard.

  • Test Secret Key (sandbox_sk_...) → backend .env only. This is what the smoke test needs.
  • Test Public Key (sandbox_pk_...) → client-side only (Squad checkout widget). Do not put this in the backend.
  • Never commit either to git. .env is already in .gitignore in this repo.

KYC warning: If your merchant account shows "Missing KYC details", Test 3 (Dynamic VA) will likely fail with a KYC/profiling error. Complete sandbox KYC before running, or run anyway and treat the error as a finding. Dynamic VA is required for Day 2 escrow — surface this blocker tonight if it exists.

Your sandbox merchant ID: SBWCKYR7RP — keep this handy; Squad support and the Soft POS SDK both need it.

Paste this into Claude Code:

You are setting up a Squad API sandbox smoke test for a hackathon project called
SquadTrust. The goal is to verify that our sandbox credentials work end-to-end
before our team commits to a week of building.

CONTEXT YOU MUST KNOW BEFORE WRITING ANY CODE:

1. Base URL for sandbox: https://sandbox-api-d.squadco.com
2. Auth header format: "Authorization: Bearer <SQUAD_SECRET_KEY>"
3. Sandbox keys are prefixed "sandbox_sk_". Production keys start with "sk_"
   and will fail silently with 401s if used against sandbox. ALWAYS validate
   the prefix at startup.
4. All amounts to Squad APIs are in Kobo (Naira × 100). ₦500 = 50000 Kobo.
5. transaction_reference must be globally unique across all merchants. Use
   uuid4 hex. Never reuse a reference, even on retry.
6. Squad documentation: docs.squadco.com — refer to it when payload field
   shapes are unclear. Print the full response body whenever Squad rejects
   a request so we can see exactly what they expect.

DELIVERABLE:

A single Python file `smoke_test.py` (no framework — just `requests`,
`python-dotenv`, and stdlib). It must run with one command:
  python smoke_test.py

REQUIREMENTS:

1. Load SQUAD_SECRET_KEY from a .env file in the same directory.
   - If the file doesn't exist, exit with: "Create a .env file with
     SQUAD_SECRET_KEY=sandbox_sk_..."
   - If the key doesn't start with "sandbox_sk_", exit with:
     "ERROR: Key must start with 'sandbox_sk_'. Production keys will fail
     against the sandbox endpoint."

2. Create a SquadCaller helper class with one method:
   call(method, path, body=None) -> (status_code, json_response)
   It should:
   - Build the full URL from the sandbox base + path
   - Set Authorization: Bearer <key> and Content-Type: application/json
   - Log to stdout: the method, full URL, and body (pretty-printed JSON)
     BUT redact the Authorization header in any logging
   - On response: print status code and pretty-printed response body
   - Return (status_code, parsed_json) — do not raise on non-2xx; let the
     test function decide how to interpret

3. Run these four tests in sequence. Each test prints a header like
   "=== TEST 1: Payment Gateway Initiate ===" and ends with either
   "✅ PASS" or "❌ FAIL: <reason>".

   TEST 1 — Payment Gateway initiate:
     POST /transaction/initiate
     Body:
       email: "test@squadtrust.io"
       amount: 50000  (this is ₦500)
       currency: "NGN"
       initiate_type: "inline"
       transaction_ref: f"smoke_{uuid4().hex}"
       callback_url: "https://squadtrust.io/callback"
       customer_name: "Smoke Test Buyer"
     Pass condition: status 200 AND response contains a checkout_url
     (the exact JSON path may be data.checkout_url — print the full body
     so we can see)

   TEST 2 — Verify the transaction from Test 1:
     GET /transaction/verify/<transaction_ref from Test 1>
     Pass condition: status 200. The transaction will be in "Pending"
     status (nobody actually paid) — that's fine, we're just confirming
     the endpoint is reachable and returns the ref.

   TEST 3 — Create a Dynamic Virtual Account pool:
     POST /virtual-account/create-dynamic-virtual-account
     Refer to docs.squadco.com for the exact payload. If the docs are
     ambiguous, start with a minimal body like {"customer_identifier": ...}
     and add fields based on what Squad's error responses ask for. Print
     every attempt and its response so the user can see the negotiation.
     Pass condition: status 200 with a usable response.
     If this returns "merchant not yet profiled" or similar — that's still
     useful information. Mark as FAIL but note: "Sandbox merchant may need
     activation via help@squadco.com".

   TEST 4 — Account lookup:
     POST /payout/account/lookup
     Body:
       bank_code: "058"   (GTBank's NIP code)
       account_number: "0123456789"   (clearly fake — we just want to
                                       see what error Squad returns)
     Pass condition: status 200 OR a meaningful 4xx with a clear error
     message about the account not being found. Print everything either
     way.

4. After all 4 tests, print a summary:
   =====================================
   SMOKE TEST SUMMARY
   =====================================
   Test 1 (Payment Initiate):    PASS/FAIL
   Test 2 (Payment Verify):      PASS/FAIL
   Test 3 (Dynamic VA Pool):     PASS/FAIL
   Test 4 (Account Lookup):      PASS/FAIL

5. Error handling:
   - If ANY request returns 401: print in red (use ANSI codes):
     "AUTH FAILED — verify the key starts with sandbox_sk_ and is active.
     Check Squad merchant dashboard → API & Webhooks → Sandbox keys."
     Then exit immediately — no point running further tests.
   - If a request raises (network error, JSON parse error): catch it,
     mark that test FAIL with the exception message, and continue to the
     next test.
   - Never let one test's failure crash the whole script.

6. After running, write a SMOKE_TEST_RESULTS.md file in the same directory
   with this structure:

   # Squad Sandbox Smoke Test Results
   Run at: <ISO timestamp>
   Key prefix: sandbox_sk_*** (last 4 chars: <last 4 of key>)

   ## Endpoint Status
   | Endpoint | Status | Notes |
   |---|---|---|
   | POST /transaction/initiate | PASS/FAIL | <one line> |
   | GET /transaction/verify | PASS/FAIL | <one line> |
   | POST /virtual-account/create-dynamic-virtual-account | PASS/FAIL | <one line> |
   | POST /payout/account/lookup | PASS/FAIL | <one line> |

   ## Action Items
   <bulleted list of any follow-ups needed — e.g. "Email help@squadco.com
   to activate Dynamic VA on sandbox merchant profile">

   ## Raw Output
   <pasted full stdout from the run>

7. Create a .env.example file with:
     SQUAD_SECRET_KEY=sandbox_sk_replace_me_with_real_key

8. Create a minimal README.md:
     # Squad Sandbox Smoke Test
     1. Copy .env.example to .env and paste your sandbox key
     2. pip install requests python-dotenv
     3. python smoke_test.py
     4. Review SMOKE_TEST_RESULTS.md

DO NOT:
- Build any abstractions beyond SquadCaller
- Add a CLI argument parser, config system, or logging library
- Add retry logic — we want to see raw failures
- Mock anything — every call must hit the real sandbox
- Skip a test if a previous one failed — run all four (except on 401)

DONE WHEN:
- `python smoke_test.py` runs to completion
- SMOKE_TEST_RESULTS.md exists and is filled in
- The user can see at a glance which Squad endpoints work tonight and which
  need help@squadco.com follow-up before Day 1
- Total runtime under 30 seconds

Done when: SMOKE_TEST_RESULTS.md committed with clear pass/fail per endpoint. If Test 3 (Dynamic VA) fails with KYC error, note it in the action items — email help@squadco.com first thing Day 1.


Day 1

Abideen — Prompt 1.1: Backend Scaffold + Database Schema

Depends on: 0.1 passed

Paste this into Claude Code:

You are building the backend for SquadTrust, an AI-driven merchant orchestration
platform for the Squad Hackathon 3.0. Scaffold a FastAPI service.

Repo: squadtrust-api (already exists, empty)
Stack: FastAPI, SQLAlchemy 2.0, Alembic, Postgres 15, Pydantic v2, uvicorn

1. Project structure:
   app/
     main.py              — FastAPI app, /health endpoint
     config.py            — Settings via pydantic-settings, loads .env
     db.py                — SQLAlchemy engine + session dependency
     models/              — SQLAlchemy models
       merchant.py
       customer.py
       escrow_transaction.py
       softpos_transaction.py
       trust_score.py
       chat_log.py
     schemas/             — Pydantic request/response models (mirror /models)
     routers/             — FastAPI routers (empty stubs for now)
       merchants.py
       escrow.py
       softpos.py
       trust.py
       loans.py
       webhooks.py
     services/            — business logic (empty for now)
     squad/
       client.py          — Squad API client (see below)
       exceptions.py
   alembic/               — migrations
   tests/
   docker-compose.yml     — Postgres + the API
   .env.example
   README.md

2. Database schema (write Alembic migration to create all tables):
   merchants: id (uuid pk), business_name, phone, email, bvn (nullable),
     gtbank_account (nullable), trust_score (int, default 0), created_at
   customers: id (uuid pk), phone, name, email, created_at
   escrow_transactions: id (uuid pk), merchant_id (fk), customer_id (fk),
     transaction_ref (unique), amount_kobo (bigint), virtual_account_number,
     status (enum: pending, funded, ai_verifying, released, refunded, expired),
     delivery_confirmed_at (nullable), released_at (nullable),
     refund_reason (nullable), created_at
   softpos_transactions: id (uuid pk), merchant_id (fk), transaction_ref (unique),
     amount_kobo, status (enum: pending, success, failed), card_last4 (nullable),
     created_at
   chat_logs: id (uuid pk), escrow_transaction_id (fk), sender (enum: buyer,
     merchant, system), message (text), created_at
   trust_score_history: id (uuid pk), merchant_id (fk), score (int), components
     (jsonb: {fulfillment_rate, velocity, refund_rate, softpos_volume, chat_sentiment}),
     calculated_at

3. Squad client (app/squad/client.py):
   class SquadClient with these methods, all using httpx.AsyncClient:
     - initiate_payment(email, amount_kobo, transaction_ref, callback_url,
                        customer_name) -> dict
     - verify_payment(transaction_ref) -> dict
     - create_dynamic_va_pool(count) -> dict
     - initiate_dynamic_va(transaction_ref, amount_kobo, duration_seconds,
                           customer_name) -> dict
     - lookup_account(bank_code, account_number) -> dict
     - transfer(transaction_ref, amount_kobo, bank_code, account_number,
                account_name, remark) -> dict
     - requery_transfer(transaction_ref) -> dict
     - verify_softpos(transaction_ref) -> dict
   Rules baked in:
     - Base URL from settings.SQUAD_BASE_URL (sandbox by default)
     - Authorization: Bearer <settings.SQUAD_SECRET_KEY>
     - Validate SQUAD_SECRET_KEY starts with "sandbox_sk_" at startup and log a
       warning if not
     - All amount inputs are integer Kobo (no float Naira anywhere)
     - Auto-generate transaction_ref via uuid4 if caller passes None
     - Catch httpx errors and raise SquadAPIError with status_code + response body
     - Log every request/response (redacted) at DEBUG

4. /health endpoint returns {status: "ok", squad_sandbox: <bool>, db: <bool>}

5. docker-compose.yml runs postgres:15 + the API. README has a quickstart:
   `cp .env.example .env && docker compose up`

Done when:
- `docker compose up` boots the API and Postgres cleanly
- `curl http://localhost:8000/health` returns all green
- Alembic migration applies without errors
- A pytest in tests/test_squad_client.py mocks one SquadClient call and passes

Done when: docker compose up + curl /health returns all green. Migration applies clean. One test passes.

Iseoluwa — Prompt 1.4: User Research Synthesis

Time: 1 hour · Runs in parallel with 1.1

Paste this into Claude Code, replacing the placeholder at the bottom with your raw merchant interview notes:

You're preparing the user research evidence for the SquadTrust hackathon pitch.
Our team has spoken to merchants. Help us structure what we have into
quotable, pitch-ready material.

I will paste raw interview notes from our merchant conversations below. Your job:

1. Extract 3-5 direct quotes that illustrate the trust deficit pain point
   (delivery scams, buyer trust issues, payment-on-delivery problems).
2. Extract 2-3 quotes about credit/capital pain (inability to get loans,
   informal moneylenders, stock financing).
3. Extract 1-2 quotes about branch congestion or POS hardware cost.
4. For each quote, attach: speaker first name + business type (e.g., "Aisha,
   Instagram fashion vendor, Lekki").
5. Output: a single MERCHANT_RESEARCH.md file with three sections:
   - The Trust Deficit (with quotes)
   - The Credit Gap (with quotes)
   - The Branch Problem (with quotes)
6. Pull out 3-5 numerical signals if the merchants mentioned any (% of
   orders disputed, time to resolution, etc.) for the pitch deck stats slide.

[PASTE YOUR INTERVIEW NOTES HERE]

Done when: MERCHANT_RESEARCH.md is in the squadtrust-api repo under /docs/.
This file will be referenced in the pitch deck slide 9 (Research & Validation).

Done when: docs/MERCHANT_RESEARCH.md committed with three sections and at least 5 usable quotes.


Day 2

Abideen — Prompt 2.1: Escrow Service & API

Depends on: 1.1 done

Paste this into Claude Code:

Build the Escrow service for SquadTrust. This is the highest-stakes feature —
it's our Squad API integration showcase.

Implement these endpoints in app/routers/escrow.py and app/services/escrow.py:

POST /escrow/create
  Auth: merchant session (placeholder — just require X-Merchant-Id header for now)
  Body: { customer_phone, customer_name, amount_naira, product_description,
          delivery_method (enum: dispatch, pickup, courier) }
  Logic:
    1. Generate transaction_ref = f"esc_{uuid4().hex}"
    2. Convert amount_naira to amount_kobo (× 100, integer)
    3. Call squad.initiate_dynamic_va(transaction_ref, amount_kobo,
       duration_seconds=86400, customer_name=customer_phone)
    4. Insert escrow_transactions row with status="pending", store the
       virtual_account_number from Squad's response
    5. Generate a public payment_url:
       f"{settings.FRONTEND_URL}/pay/{transaction_ref}"
    6. Return: { transaction_ref, virtual_account_number, payment_url,
                amount_kobo, amount_naira, expires_at }

GET /escrow/{transaction_ref}
  Public — buyer hits this to view the payment page
  Returns: { merchant_business_name, amount_naira, product_description,
             virtual_account_number, bank_name: "Squad/GTBank",
             status, expires_at }

POST /webhooks/squad/dynamic-va
  Public — Squad will POST here when a buyer funds the VA
  Verify the webhook signature (SHA-512 in x-squad-encrypted-body)
  On SUCCESS event:
    - Find escrow_transactions row by transaction_ref
    - Update status="funded"
    - Trigger AI verification (Day 3 will wire this; for now just log it)
  On MISMATCH or EXPIRED:
    - Update status accordingly
  Always respond HTTP 200 even on errors (Squad expects this)

GET /escrow (merchant view)
  Returns merchant's escrow transactions with pagination

POST /escrow/{transaction_ref}/confirm-delivery
  Merchant manually confirms delivery (used as fallback if AI verification
  fails or is too slow — Day 3 will add the AI path)
  Logic:
    1. Verify the escrow row belongs to this merchant
    2. Status must be "funded" — reject otherwise
    3. Call squad.transfer(...) to merchant's gtbank_account
    4. On success: status="released", released_at=now
    5. Trigger trust score recalculation (Day 4 will wire)

Write integration tests in tests/test_escrow.py:
  - test_create_escrow_returns_va_number (mock SquadClient)
  - test_webhook_funded_updates_status
  - test_confirm_delivery_triggers_transfer
  - test_duplicate_transaction_ref_rejected

Critical: when calling squad.transfer, ALWAYS run squad.lookup_account first
and use the returned account_name in the transfer payload. Squad rejects
transfers where the name doesn't match.

Done when:
- All endpoints work against the actual sandbox (manually test with
  curl or Postman)
- One full happy path runs: create escrow → simulate payment via
  /virtual-account/simulate/payment → webhook fires → manual confirm-delivery
  → transfer succeeds in sandbox
- Tests pass
- Record a screen-capture GIF of the happy path for the demo backup

Done when: Full happy-path runs in sandbox. Tests pass. GIF recorded.


Day 3

Iseoluwa — Prompt 3.1: AI Delivery Verification Service

Depends on: 2.1 done

Paste this into Claude Code:

Build the AI delivery verification layer for SquadTrust escrow transactions.
This is the centerpiece of our "AI Automation" pillar for Challenge 02.

Stack: app/services/ai_verifier.py, using OpenAI or Anthropic API
(use Claude Sonnet 4 via Anthropic API — model: claude-sonnet-4-20250514).

The service has one main function:
  async def verify_delivery(escrow_transaction_id) -> VerificationResult

VerificationResult is a Pydantic model:
  - verdict: "approve_release" | "reject_release" | "needs_human_review"
  - confidence: float (0-1)
  - reasoning: str (the LLM's explanation, in plain English)
  - red_flags: list[str]
  - signals_used: dict (what data fed the decision)

Inputs gathered for each verification:
1. Escrow transaction: amount, age in hours, merchant trust score
2. Chat logs (from chat_logs table) — all messages between buyer and merchant
3. Mock 3PL data: for the hackathon, simulate logistics status with a
   `LogisticsSimulator` class that returns one of:
   - {"status": "delivered", "signed_by": "<name>", "timestamp": ...}
   - {"status": "in_transit", "last_location": "...", "eta": "..."}
   - {"status": "no_data"}
   The simulator picks one based on the escrow_transaction_id hash so the
   same ID always returns the same result — makes the demo reproducible.
   Real-world note in comments: "In production this calls 3PL APIs
   (Sendbox, Kwik, GIG)."

Prompt template for the LLM (in app/services/prompts/delivery_verification.py):

  You are an AI dispute analyst for SquadTrust, a Nigerian social-commerce
  escrow platform. Your job is to decide whether to release held funds to a
  merchant based on evidence of delivery.

  TRANSACTION:
  - Amount: ₦{amount_naira}
  - Time since payment: {hours_since_funded} hours
  - Merchant trust score: {merchant_trust_score}/1000

  LOGISTICS DATA:
  {logistics_json}

  BUYER-MERCHANT CHAT (most recent first):
  {chat_log_formatted}

  Decide one of:
  - approve_release: clear delivery signal and buyer satisfaction
  - reject_release: clear evidence of non-delivery, scam, or buyer dispute
  - needs_human_review: ambiguous evidence

  Output JSON: { "verdict": "...", "confidence": 0.0-1.0,
                 "reasoning": "...", "red_flags": [...] }

  Be conservative — when in doubt, choose needs_human_review.

Sentiment analysis: also compute a buyer_sentiment_score (-1 to +1) from the
chat log using either a second LLM call OR a lightweight library
(vaderSentiment is fine for English; for code-switched Pidgin/Yoruba, ask
the LLM directly: "Rate buyer satisfaction from -1 to +1").

Trigger points:
- Webhook handler in 2.1 calls verify_delivery() in a background task when
  status flips to "funded" AND chat_logs has 2+ messages AND age > 1 hour
- Manual trigger endpoint: POST /escrow/{ref}/verify-now (for demo)

When verdict == "approve_release" AND confidence > 0.75:
  - Auto-call the same release logic as confirm-delivery (squad.transfer)
  - Mark released_by = "ai"
When verdict == "reject_release" AND confidence > 0.75:
  - Mark status="refund_pending" — Squad's Dynamic VA auto-refunds expired
    unmatched payments, so we just stop the transfer
Otherwise:
  - Mark status="needs_review" — merchant sees a "Pending AI review" badge
    and can manually confirm

Persist every verification run to a new table:
  ai_verifications: id, escrow_transaction_id, verdict, confidence, reasoning,
                    red_flags (jsonb), raw_llm_response (text), created_at
Add an Alembic migration.

Done when:
- POST /escrow/{ref}/verify-now returns a structured verdict in < 10 seconds
- Three test escrow transactions (one clearly delivered, one clearly disputed,
  one ambiguous) produce the expected verdicts
- The reasoning text is genuinely useful — it cites specific chat messages
  and logistics signals

Done when: Three test transactions produce expected verdicts. /verify-now responds in < 10 seconds.

Iseoluwa — Prompt 3.2: Trust Score Engine

Runs in parallel with 3.1 or immediately after

Paste this into Claude Code:

Build the Trust Score engine for SquadTrust. This is the alternative credit
history that unlocks loans. It's our Challenge 02 differentiator.

Implement app/services/trust_score.py.

Trust Score is an integer 0-1000, computed from weighted components.
This is a heuristic engine — not ML. The judges will respect honesty here;
do NOT pretend it's a trained model.

Components (weights sum to 1.0):

1. Fulfillment Rate (weight: 0.35)
   = released_transactions / total_completed_transactions
   - Only count transactions older than 24 hours
   - 100% fulfillment → 350 points

2. Transaction Velocity (weight: 0.20)
   = log-scaled count of transactions in last 30 days
   - 0 transactions → 0 points
   - 1 transaction → 50 points
   - 10 transactions → 130 points
   - 50+ transactions → 200 points (capped)

3. Refund Rate (weight: 0.15)
   = 1 - (refunded_transactions / total_transactions)
   - 0% refunds → 150 points
   - 20%+ refunds → 0 points

4. Soft POS Volume (weight: 0.15)
   = log-scaled total Naira processed via Soft POS in last 30 days
   - 0 → 0 points, ₦100k → 75 points, ₦1m+ → 150 points

5. Chat Sentiment Average (weight: 0.10)
   = average buyer_sentiment_score across last 20 chat logs
   - Normalize from [-1, +1] to [0, 100]

6. Account Age (weight: 0.05)
   = days since merchant registration
   - 0 days → 0, 30 days → 25, 90+ days → 50

Function signature:
  async def compute_trust_score(merchant_id: UUID) -> TrustScoreResult

TrustScoreResult:
  - total_score: int
  - components: dict (each component's contribution)
  - rank: str ("New", "Building Trust", "Trusted", "Trusted+", "SquadTrust Elite")
    Bands: <200, 200-499, 500-749, 750-899, 900+
  - loan_eligibility_naira: int
  - explanation: str (LLM-generated, plain-English summary)

Loan eligibility:
  - score < 500: ₦0
  - 500-699: ₦25,000
  - 700-849: ₦100,000
  - 850-949: ₦500,000
  - 950+: ₦2,000,000

Explanation: pass components + merchant context to Claude and ask for a
2-sentence plain-English summary that a Lagos market trader would understand.
Example tone: "Aisha, your fulfillment is strong (96%) and your sales volume
is growing fast. Keep your refund rate below 5% to unlock the next loan tier."

Recompute trust score:
- Every time an escrow transaction releases or refunds
- Every Soft POS transaction success
- Nightly: via /admin/recompute-all-scores endpoint

Persist every computation to trust_score_history.

Done when:
- Merchant with 0 transactions → score=0, rank="New"
- Merchant with 10 successful escrows + 5 Soft POS → score 600-750 range
- Merchant with high refund rate → low score, clear explanation
- POST /admin/recompute-all-scores recomputes for every merchant
- GET /merchants/{id}/trust-score returns latest score with full breakdown

Done when: Three merchant profiles produce scores in the expected ranges. Explanation text is plain English a trader would understand.

Abideen — Prompt 3.3: Chat Log Seeder + Loan Eligibility Endpoint

Depends on: 3.1 and 3.2 done

Paste this into Claude Code:

Two backend additions to support the demo:

1. Chat log seeder (scripts/seed_demo.py):
   Create a script that wipes the dev database and seeds:
   - 3 merchants:
     a) Aisha Mohammed — Instagram fashion vendor, Lagos. 14 days old.
        12 escrow transactions, 11 successful, 1 refunded. 5 Soft POS sales.
        Expected score: ~720.
     b) Tunde Adekunle — electronics, brand new. 0 transactions.
        Expected score: 0.
     c) Chioma Okafor — food delivery, 60 days old. 30 escrow transactions,
        28 successful, 2 refunded. 22 Soft POS sales. Expected score: ~880.

   - For each escrow transaction, seed 2-4 chat messages between buyer and
     merchant that match the outcome:
     - Successful: buyer thanks merchant for delivery
     - Refunded: buyer complains item was wrong / never arrived
   - 3 distinct chat log scenarios for the LIVE demo escrow (don't release
     these on seed — keep them as fixtures):
     i)  "Clear delivery confirmed" — buyer says "Received, thanks!" and
         logistics shows delivered status
     ii) "Clear dispute" — buyer says "This is not what I ordered, refund!"
         and logistics shows delivered but to wrong address
     iii) "Ambiguous" — buyer says "Where is my package?" no logistics data
   These get loaded into the live demo escrow's chat log via a
   /demo/load-scenario/{n} endpoint.

2. Loan eligibility endpoint:
   GET /merchants/me/loan-eligibility
   Returns:
     {
       trust_score: int,
       eligible: bool,
       max_amount_naira: int,
       interest_rate_monthly: float (hardcode 2.5%),
       recommended_tenor_days: int (30/60/90 based on score),
       explanation: str
     }

   POST /merchants/me/apply-loan
   Body: { amount_naira, tenor_days }
   Logic:
     - Verify amount <= max_amount_naira
     - Create a loan record (new table: loans — keep it minimal:
       id, merchant_id, amount_naira, tenor_days, status (enum: approved,
       disbursed, repaid, defaulted), approved_at, disbursed_at)
     - Call squad.transfer() to merchant's gtbank_account (for the demo,
       just log it — don't actually transfer real sandbox money to avoid
       sandbox balance issues)
     - Mark status="approved"
     - Return: { loan_id, status, message: "Approved. Funds will arrive in
                 your GTBank account within 5 minutes." }

Done when:
- `python scripts/seed_demo.py` reliably produces the three merchants with
  expected scores
- Loan eligibility for Aisha returns ~₦100k, for Chioma returns ~₦500k,
  for Tunde returns ₦0
- /demo/load-scenario/{n} swaps the chat log for the live demo escrow

Done when: seed_demo.py runs clean. Three merchants' loan eligibility numbers match targets.


Day 4

Abideen — Soft POS Backend Endpoints

Depends on: Abimbola confirms what the mobile app needs (coordinate with him)

Add these four endpoints to support the mobile Soft POS flow (Abimbola will call them from the app):

  • POST /softpos/initiate — creates softpos_transactions row, status=pending
  • POST /softpos/confirm — updates to status=success, sets card_last4, triggers trust score recompute. Must be idempotent — use transaction_ref as idempotency key; ignore duplicate confirms for refs already in success status.
  • POST /softpos/fail — updates to status=failed with error_code
  • GET /softpos — list merchant's Soft POS transactions

Day 5

Abideen — Prompt 5.2: Demo Control Panel (Backend)

Day: 5

Paste this into Claude Code:

Build a hidden /admin/demo-control page in the web app for the demo operator.
This isn't for judges — it's a safety panel for the team running the demo.

Features:
- Switch active merchant (Aisha / Tunde / Chioma) — sets a cookie used by
  all API calls
- Trigger fake escrow funding: POST /demo/fund-escrow/{ref} hits the
  /virtual-account/simulate/payment Squad endpoint
- Load chat scenario: buttons for scenario 1/2/3 (from 3.3)
- Force AI verification: POST /escrow/{ref}/verify-now
- Reset demo state: re-runs seed_demo.py

Build with shadcn/ui. Auth-gate it with a hardcoded password from .env
(DEMO_OPERATOR_PASSWORD). Make every action have a clear loading state and
a success toast.

Done when: every step of the rehearsed demo flow can be triggered from this
one page without touching the terminal.

Note: The UI for this page lives in the web repo (squadtrustweb). You're responsible for the backend endpoints it calls. Coordinate with Fiopefoluwa on what the frontend needs.


Day 6

Abideen — Prompt 6.1: Edge Case Hardening

Paste this into Claude Code:

Harden the SquadTrust backend against demo-day failure modes. Add tests
and fixes for each of these cases:

1. Squad sandbox returns 424 timeout on Transfer
   → Already have requery_transfer. Wire it into a retry decorator that
     re-queries up to 3 times with 2s/5s/10s backoff before failing.

2. Duplicate transaction_reference rejected by Squad
   → On HTTP 400 mentioning duplicate ref, regenerate ref with a different
     uuid and retry once. Log the original ref for debugging.

3. Webhook signature verification fails
   → Currently we accept anyway for the demo. Add a config flag
     WEBHOOK_STRICT_MODE (default false in dev). When true, reject invalid
     signatures with 403. Document this choice in README.

4. LLM API timeout
   → ai_verifier must time out at 8 seconds. On timeout, return verdict
     "needs_human_review" with reasoning "AI verification timed out — manual
     review required." Never block the escrow flow on AI being slow.

5. Postgres connection drop
   → Add a /health endpoint that actually pings the DB. The Next.js
     dashboard already shows a connection error banner if /health fails.

6. Mobile app offline at the moment of NFC tap
   → Soft POS SDK handles this client-side, but our /softpos/confirm endpoint
     must be idempotent — the mobile app retries on reconnection, and we should
     not double-credit. Use transaction_ref as the idempotency key.

For each case, add an integration test in tests/test_edge_cases.py.

Done when: all 6 tests pass and DEMO_RUNBOOK.md's "Failure recovery" section
references the specific behavior for each case.

Done when: All 6 edge case tests pass.


Day 7

Both — Prompt 7.1: Repo Cleanup

Final README polish, then make this repo public. The README must allow a judge to clone and boot the API with three commands. Run the quickstart yourself on a clean machine to verify.


Environment Variables

Create a .env from .env.example:

Variable Required Notes
SQUAD_SECRET_KEY Yes Must start with sandbox_sk_
SQUAD_BASE_URL No Default: https://sandbox-api-d.squadco.com
DATABASE_URL Yes PostgreSQL connection string
FRONTEND_URL Yes Used to build escrow payment_url
ANTHROPIC_API_KEY Yes (Day 3+) For AI verifier + trust score explanation
WEBHOOK_STRICT_MODE No false in dev; true rejects invalid signatures
DEMO_OPERATOR_PASSWORD Yes (Day 5+) Auth for demo control panel

Quickstart

cp .env.example .env
# fill in your sandbox_sk_ key and other vars
docker compose up
curl http://localhost:8000/health

Run all tests: docker compose exec api pytest
Run single test: docker compose exec api pytest tests/test_escrow.py::test_create_escrow_returns_va_number -v
Seed demo data: docker compose exec api python scripts/seed_demo.py
Apply migrations: docker compose exec api alembic upgrade head

About

The Autonomous Commerce & Credit Engine

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages