A deterministic, evidence-aware affordability engine that answers "Can I afford this?": when to pay, how much, and on what plan.
Built for the HackerRank Orchestrate 24-hour hackathon (September 2026).
For each purchase request, the engine rebuilds the user's real cash position from messy transaction history. It reads amounts from receipt images and interprets multilingual messages, then forecasts 90 days ahead. From that forecast it picks the safest plan the user will accept: pay in full, pay partially, use installments, wait, or don't proceed.
$ python3 code/main.py
wrote output.csv (250 rows) # ~0.5s, zero model calls, zero dependencies
- Deterministic core, AI at the edges. Six of the seven output columns are arithmetic or enum choices with one correct answer, so the core is a cash-flow simulator, not an LLM guessing numbers. AI only reads receipt images and interprets free text. It outputs typed data, and the simulator uses that data.
- One quantity answers everything. A payment on day D shifts the whole forecast curve
down from D onward. The largest safe payment today is therefore the headroom at the curve's trough:
amount_safe_to_pay = clamp(trough_90d − minimum_balance, 0, requested_amount). - Resolves hidden traps in the data. It handles cancelled authorisations, failed debits with retries, duplicate pending charges, unsettled refunds, non-cash portfolio valuations, blank amounts that must be read from images, and foreign-currency events converted at the rate for their date.
- Injection-resistant by construction. The message corpus includes a real advance-fee scam in two languages. Message text never reaches a prompt that can act. Only a fixed set of 16 typed operations can change a forecast, so the instruction "pay the release fee" can't be expressed at all. The self-test checks this against corpus messages and adversarial probes.
- Measured, not tuned. A 75-day horizon scored higher on the labelled samples. I rejected it
because it contradicts the spec and hides an unexplained bias. Every tempting shortcut that
scored worse is recorded in
lab/FINDINGS.md.
Local scorer against the 25 published labelled examples:
| Column | Accuracy |
|---|---|
affordability_status |
80.0% |
recommended_payment_method |
84.0% |
payment_plan |
88.0% |
earliest_date_for_full_payment |
76.0% |
spending_changes_needed |
84.0% |
amount_safe_to_pay |
12/25 within 5% |
| Weighted overall | 71.6 / 100 |
Reference points: the naive baseline ("always affordable, pay in full today") scores 25.7, and the ground truth scored against itself gives exactly 100.0. The full 250-request output passes all ~30 output checks with 0 errors, and the self-test passes 17/17.
Total AI cost for the whole dataset: $1.28, all spent once on extracting evidence.
The production run makes 0 model calls (see usage_report.md).
No installation and no dependencies. Needs Python 3.8+.
git clone https://github.com/Safeer1877/buy-or-wait.git
cd buy-or-wait
# predict all 250 requests -> output.csv
python3 code/main.py
# score against the 25 labelled examples
python3 code/main.py --requests dataset/sample_requests.csv --out /tmp/samples.csv
python3 code/evaluation/score.py --pred /tmp/samples.csv --truth dataset/sample_requests.csv
# check the output format and business rules
python3 code/evaluation/validate.py --pred output.csv --dataset dataset
# test the evidence layer, including the prompt-injection defence
python3 code/evaluation/selftest.pyAdd --verbose to print the evidence applied to each request. --refresh-evidence
re-extracts receipt amounts with a vision model and needs ANTHROPIC_API_KEY. Without a key,
the engine uses the audited cache.
financial_events ─┐
profiles ─────────┤ ┌──────────────┐ ┌────────────┐ ┌─────────┐
exchange_rates ───┼─► reconstruct ───────►│ 90-day │────►│ planner │────►│ explain │──► output.csv
payment_options ──┘ (trap resolution, │ forecast │ │ (candidates│ └─────────┘
recurring streams) │ + trough │ │ + ranking)│
messages ─┐ ▲ └──────────────┘ └────────────┘
images ───┴─► evidence ────┘
(typed, whitelisted operations)
| Module | Responsibility |
|---|---|
bow/loader.py |
Dataset joins and dated FX conversion (fails loudly instead of converting at the wrong rate) |
bow/evidence.py |
Turns images and messages into typed, whitelisted operations |
bow/reconstruct.py |
Trap resolution, recurring-stream detection, applying evidence |
bow/forecast.py |
90-day projection, trough, earliest safe date |
bow/planner.py |
Candidate plans, safety checks, spending-change search, ranking |
bow/explain.py |
Short explanations backed by the underlying figures |
The full technical write-up is in code/README.md. It covers each stage, design decisions and
the configuration flags that let every choice be re-measured.
code/ the solution
main.py entry point
bow/ engine modules
evaluation/ scorer, output validator, self-test, token-usage report
prompts/ image-extraction prompt and message-operation spec
cache/ audited amounts extracted from receipt images, with provenance
lab/ research record: findings, decisions, experiment harness, run history
dataset/ challenge input data (provided by HackerRank)
problem_statement.md full challenge specification (provided by HackerRank)
output.csv predictions for all 250 requests
I built the scoring harness and output validator before any solution code, so every change
was measured rather than guessed. The lab/ directory keeps that record:
lab/FINDINGS.md: rules reverse-engineered from the labelled data, each backed by sample evidencelab/DECISIONS.md: each design decision, with its reasoning and the alternatives I rejectedlab/runs/INDEX.md: the history of experiment scores
The problem statement and dataset are from the
HackerRank Orchestrate September 2026
hackathon and belong to HackerRank. They are included here only so the solution can be run and
reproduced. The MIT license covers the solution code in code/ and lab/.