Owners: Abideen (API + Escrow + Edge Cases) · Iseoluwa (AI Layer + Trust Score + Research)
Stack: FastAPI · PostgreSQL 15 · SQLAlchemy 2.0 · Alembic · httpx · Anthropic Claude API
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.envonly. 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.
.envis already in.gitignorein 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.
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.
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.
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.
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.
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.
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.
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— createssoftpos_transactionsrow,status=pendingPOST /softpos/confirm— updates tostatus=success, setscard_last4, triggers trust score recompute. Must be idempotent — usetransaction_refas idempotency key; ignore duplicate confirms for refs already insuccessstatus.POST /softpos/fail— updates tostatus=failedwitherror_codeGET /softpos— list merchant's Soft POS transactions
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.
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.
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.
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 |
cp .env.example .env
# fill in your sandbox_sk_ key and other vars
docker compose up
curl http://localhost:8000/healthRun 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