You price or review loans at a lender to mid-sized private companies. You can't see what other lenders charge, so your margin goes to the approval committee on hearsay. BDC Mark Tape puts the loans that BDCs (stock-listed funds that lend to private companies) publish into one table you can check.
BDCs already publish every loan they hold, with its margin and valuation. It sits in 64 filings with 8 table layouts. The vision: one table of those loans, matched by borrower, so your margin and your review list rest on rows anyone can check.
Valuations and margins sit in 64 filings with 8 table layouts, so you can't compare lenders.
Each lender spells the same borrower differently, so their valuations can't sit side by side.
A margin without its lenders and rows can't be defended at committee.
Each BDC names industries its own way, so most industry groups hold one lender's loans.
With time for 15 borrowers, you have no list sorted by how other lenders value them.
Valuation moves and new names need last quarter's filing open next to this one.
| Your job | Hypothesis | Metric and target, then result | Expected impact |
|---|---|---|---|
| When I compare lenders, I want every loan list in one format, so I don't read 64 filings. | If every loan list is read into one format, comparisons and review lists work across 8 lenders, because the filings already hold each loan's valuation and margin. | Money-column accuracy on a check sample, floor 0.90: 48 of 48 | Comparisons across 8 lenders in one place |
| When I look at a borrower, I want each lender's valuation side by side, so a disagreement is real. | If names are matched with measured accuracy, one borrower's valuations from several lenders sit together, because a wrong match would show a fake disagreement. | Pair precision, floor 0.95: 0.9855 (n=69). Group precision 0.85 (n=20) | 187 borrowers held by 2 or more BDCs to compare |
| When I take a margin to committee, I want its lenders and rows, so I can answer "says who?" | If the margin comes with its lenders and rows, you answer the committee from the screen, because each row links to the filing. | Time to answer where a proposed margin comes from: down. Baseline unobserved | Faster committee answers (unobserved) |
| When I read a median, I want to know whose loans drive it, so I don't call one lender a market. | If the screen names which lender's loans drive the number, fewer one-lender medians reach committee as "market", because the warning appears before the quartiles. | Share of cited comparisons resting on one lender: down. Baseline unobserved | Fewer margins priced on one lender's book (unobserved) |
| When I plan the quarter's review, I want a list sized to my time, so my hours go where other lenders see trouble. | If you check the 15 lowest-valued borrowers, more go non-accrual next quarter than a random 15, because a lender valuing a loan below cost is admitting trouble. | Share of the 15 that went non-accrual next quarter: up. 3 of 15 (2025-12-31) vs 22 of 1,423; 1 of 15 (2026-03-31) vs 19 of 1,415 | Review time spent on borrowers in trouble |
| When I review a borrower, I want what moved and the filing on one line, so I escalate faster. | If each row shows what moved and links its filing, you spend less time per borrower before escalating, because the change and source sit on one line. | Minutes per reviewed borrower: down. Baseline unobserved | More borrowers reviewed per quarter (unobserved) |
Floors are the README stop rules. The reading check sample was drawn by the person who wrote the reader, from the same data. No pricer or reviewer has used it.
Public reports hold 90 first-lien insurance loans from 4 lenders, at a median of S+475. Finding them by hand means reading 64 filings.
The weak link is belief, for all three. Pricing: show screen 1 to two pricers and count who'd bring it to committee. Reviewing: the demo shows the hindsight check for every quarter beside the list, so belief rests on results for any of 7 quarters. Head of lending: fix the failed release checks before any adoption talk.
eval/model_results.json.team/criteria.json v9: 27 PASS, 3 FAIL, 5 PENDING. The three product criteria (P1 to P3) are PENDING.LIMITATIONS.md.pip install -r requirements.txt, then streamlit run app/two_stories.py from the showcase folder.Software, first_lien. Check the number of loans and lenders before the median. Switch to Software and Services to see the warning that every loan comes from one lender.2025-12-31 and 15 names. Open one borrower to see each lender's valuation and filing. Then read the hindsight check below your list: which flagged borrowers went non-accrual.Data: real public SEC filings, a fixed copy taken 2026-09-06. No AI language model, no model score, no login. Nothing goes online except SEC links you choose to open. Costs $0 to run.
tools.coverage() on showcase/data. Supports 1.eval/parse_audit_results.json. Supports 1.eval/er_results.json. Supports 2.get_comparables, asserted in tests/test_showcase.py. Supports 3, 4.two_stories.lender_table. Supports 3, 4.get_comparables. Supports 4.two_stories.hindsight. Supports 5, 6.eval/model_results.json, checked by a test. Supports 5.Terms. Non-accrual: the lender stops counting the loan's interest as income. NEW: not on the prior quarter's list of the same length. It doesn't mean newly in trouble.
This build tackles 1 to 6.
| # | Status | Code |
|---|---|---|
| 1 | Tackled | src/parallax/ingest.py, src/parallax/parse.py, src/parallax/soi/ -> showcase/data/tape_resolved.parquet |
| 2 | Tackled | src/parallax/resolve/, measured in eval/er_results.json |
| 3 | Tackled | showcase/src/parallax/tools.py get_comparables; showcase/app/two_stories.py matched_positions, lender_table |
| 4 | Tackled | get_comparables related_labels_not_used, filer_mix; one-lender and few-loans warnings in two_stories.py story_price |
| 5 | Tackled | tools.py review_list (sort by fvc_min, capacity k); two_stories.py story_review |
| 6 | Tackled | review_list fields d_fvc_1q, new_since_prev_period, holders, accessions; two_stories.py hindsight |
| 7 | Attempted, not achieved | src/parallax/model.py, eval/model_results.json: the XGBoost model did not beat the simple rule |
| 10 | Partial, not a story | tools.py get_borrower; each lender's valuation shown inside story 2 |
| 8, 9, 11 | Not tackled | none |
fvc_min), change since last quarter, lender disagreement, and whether it went non-accrual next quarter.fvc_min and shows change, a NEW marker, each lender's valuation, filing links and the hindsight check.Valuation = total fair value / total cost, per lender, on loan rows only. Netting rows, equity and rows with zero cost are left out.
| Metric | Type | Tests hypothesis | How measured |
|---|---|---|---|
| Money-column accuracy per new quarter | Early signal | 1 | Fixed random sample checked against filing text |
| Name-matching precision, pairs and groups, on a new sample | Early signal | 2 | Hand-labelled pairs at the fixed threshold, Wilson confidence interval |
| Time to answer a committee question on sources | Early signal | 3 | Timed practice committee, 2 pricers |
| Share of cited medians resting on one lender | Early signal | 4 | Count in practice committee notes |
| Minutes per reviewed borrower | Early signal | 6 | Timed review session |
| Precision@15 vs overall rate, per quarter | Late signal | 5 | Next filing's non-accrual flags, Wilson confidence interval, already in the demo |
| Committee margins later re-priced | Late signal | 3, 4 | Unobserved: needs the lender's own deal records |
| In scope | Not in scope (incl. untackled sub-problems) |
|---|---|
| Read 64 filings from 8 BDCs (1) | 7, warning before the valuation falls: an XGBoost model was built and lost; its scores are hidden. Missing: more non-accrual cases (the README minimum is 60 pooled) and a verified saved copy of the scores. |
| Match borrowers across lenders (2) | 8, full pricing: fees, upfront discounts (OID) and early-repayment protection aren't in the loan lists. Missing: a data source that reports them. |
| Comparisons with lenders and rows (3) | 9, coverage: 8 BDCs, 8 quarters, no buyout-firm owner names, as set in the brief. Missing: more reporting lenders, and lenders that aren't BDCs. |
| Warnings for few loans, one lender and similar industry names (4) | 10, investor valuation check: partial. get_borrower returns each lender's valuation; there's no screen or story for fund investors. |
| Lowest-valuation list sized to review time (5) | 11, industry mapping: no shared industry list, by choice. Merging industry names would invent a statistic no filing reports. |
| Change, lender valuations, filing links, hindsight (6) | Also out: graph queries, the source-citing AI agent, MLflow views in this demo; login, scheduling, cloud hosting, investment advice. |
| Chose | Over | Gain | Cost |
|---|---|---|---|
| Rank by lowest observed valuation | XGBoost score | No unverified score; model not better on held-out data | Doesn't learn from other data columns |
| Exact industry names from filings, similar ones listed | Merged industry list | No invented market statistic | Few loans per group; user picks the industry name |
| Fixed public copy | Live data feed | $0, works offline, same result every run | Data ages each quarter |
| Hindsight check beside the list | Precision claim in text | Belief rests on visible outcomes | Shows quarters with 0 hits |
| Two stories in the demo | Nine-tab walkthrough | A visitor finishes in minutes | Graph, ML and agent work sit in the full app only |
| Field | Value |
|---|---|
| Feature | Parallax: loan tape, review-list model, cited agent, two-screen app |
| DRI (PM) | pm agent, for Alex (CPO, owner) |
| Stage | Team Kickoff |
| Last Updated | 2026-09-06 |
| Status | Draft, panel-reviewed |
| Links | PROJECT_BRIEF.md · team/criteria.json · outputs/prds/parallax-goals.md · outputs/prds/parallax-state.json |
Problem. Private-credit loans are priced and marked behind closed doors. The only public, position-level record is the Schedule of Investments each listed BDC files quarterly, and nobody has stitched those into one tape where the same borrower is visible across lenders.
Hypothesis. If we parse the SOI from 8 BDCs over 8 quarters, resolve borrowers across filers with measured precision, and put a model and a cited agent on top, then an underwriter can defend a spread with BDC-observed comparables (what 8 lenders hold, not "the market"), a monitoring analyst can pick 15 names with a stated hit rate, and an allocator can compare a manager's mark with other holders', because a lender's fair-value-to-cost mark is its own public admission of stress, one holder often marks a borrower down before the others do, and that disagreement only exists once entities are resolved.
The allocator gets no screen: the mark-dispersion query (M4) and get_borrower (M7) serve that job, checked by P3.
Supporting evidence. The brief, nothing else. No user interviews yet; the jobs below stay hypotheses until a practitioner confirms them (open question 1).
Jobs to be done (verbatim from the brief):
| Persona | Job |
|---|---|
| Deal underwriter | When pricing a deal, know what the market is actually clearing at for a comparable borrower, to defend a spread to the investment committee. |
| Monitoring analyst | With 200 borrowers and capacity to review 15, pick the right 15, early enough to preserve recovery. |
| Allocator (free output) | Is my manager's mark consistent with how other lenders mark the same borrower? |
Strategic fit. The brief claims an observable private-credit tape doesn't exist in public form. Every input is a public SEC filing, so the data isn't the moat. The moat is the measured pipeline: parse audit, entity resolution with precision and recall, a model at the operating point a real analyst has (15 reviews a quarter), an agent that cites or refuses. One spine serves three personas, and each milestone is a separate checkable claim, so a failure anywhere is a finding, not a wasted week.
In scope: M1 to M8 as written in the brief, on the suggested tickers ARCC, OBDC, FSK, GBDC, MAIN, TSLX, HTGC, PSEC, 8 quarters of 10-K and 10-Q.
Non-goals (from the brief, each with the reason):
- Auth, accounts, multi-tenancy. One user, run locally. Nothing to protect.
- Scheduler or automated refresh. The cache is the data version; refresh is make data.
- More than 8 BDCs. Coverage is a stated limitation; parse quality per filer beats breadth in two days.
- Any fine-tuned model. Can't be evaluated with this little labelled data.
- Deployment beyond docker compose up. Nobody is on call for this.
- Sponsor identity. Not in BDC filings. Future enrichment.
Trade-offs accepted: heuristic non-accrual detection with an LLM fallback and a non_accrual_source column; an eight-filer universe that biases comparables toward upper-middle-market direct lending, which the coverage banner says.
If this ships, an underwriter gets a spread distribution with n and the rows behind it instead of a broker's anecdote, and a monitoring analyst gets 15 names with reasons and a measured precision instead of a gut list. If it doesn't, those decisions stay undefended. We don't know the size of the impact. To find out: put screen 1 in front of two underwriters after M8 and count who'd bring it to committee.
| Dimension | Specification |
|---|---|
| Primary task | retrieve-and-answer over the tape, the model and the graph |
| Inputs | four tools: get_comparables, get_borrower, sponsor_or_colender_exposure, review_list; no free web access |
| Constraints | every numeric claim carries the accession number it came from; answers state "data as of" |
| Disallowed | inventing a number, a borrower, a sponsor, or an accession; answering outside the tape's coverage |
| Model | Gemini via langchain-google-genai, existing key, no fine-tune |
| Tracing | LangSmith if available, else MLflow tracing; every inference inspectable |
The rule: cite or refuse. No accession, no number. The agent says it doesn't know and why. Refusing is a correct answer; guessing is a failure.
What counts as a citation (M7.2): a single position cites its accession; an aggregate cites the accession list of the rows it used, with n; a model output cites the MLflow run id plus the accessions of the input rows. Anything else is uncited.
Behaviour examples (values illustrative, accessions as placeholders):
| Scenario | User input | Expected output |
|---|---|---|
| Cited answer | "What are first-lien healthcare loans between $20M and $50M clearing at?" | "Across n=23 positions in 6 filings, median S+575 bps, IQR 525 to 650. Data as of 2025-06-30. Sources: <accession ARCC 10-Q>, <accession FSK 10-Q>, ... (full list in the rows)." |
| Refusal | "Which sponsor backs Borrower X?" | "I can't answer that. Sponsor identity isn't in BDC filings, so there's no accession to cite." |
| Refusal | "What's the spread on Borrower Y?" (not in the tape) | "No position for Borrower Y across the 8 BDCs and 8 quarters covered (data as of 2025-06-30). I won't estimate it." |
eval/agent_eval.jsonl (20 questions, M7.3) scores accuracy and citation-groundedness and includes refusal cases.
Success is team/criteria.json. No number here that isn't in the brief.
gated: true, M1.1 to M8.4 and D1 to D3. They are the brief's acceptance tests.Guardrails: M3.2 precision >= 0.95 is a floor; recall is reported whatever it is (M3.3). If the model doesn't beat the baseline, we keep the result and say so (M5.3).
Kill criteria (the README's question 5, decided now): - The tape is abandoned if M2.2 (parse audit >= 90%) still fails after the protocol's three loops, or a filer's SOI can't be parsed and no replacement filer gets the cache back to M1.1. - The cross-filer story is dropped, not the project, if M3.2 (precision >= 0.95) is unreachable after three loops: dispersion features and P3 go, and the README says the tape is single-filer. - A model that doesn't beat the baseline is not a kill condition. The baseline ships and M5.3 says why.
| Risk | Detection | Fallback |
|---|---|---|
| SEC blocks or throttles | 403/429 in M1 logs | correct User-Agent with contact, <= 8 req/s, cache everything, never re-download |
| A filer's SOI table isn't found by the header heuristic | per-filer row count of zero in M2 | per-filer header aliases; if still zero, PIVOT with the filer named |
| Non-accrual heuristic misses footnote legends | non_accrual_source mix skews to none |
LLM pass over the footnote block, logged per row |
| Few borrowers held by >= 2 BDCs | count reported after M3 | dispersion features and P3 degrade; README says so |
| Model doesn't beat the baseline | M5.2 table | ship the baseline; M5.3 says why |
| Few positives in the held-out quarters, so precision@15 is noisy | n positives in M5.2 | show n beside the number on screen 2; no delta claimed inside the noise |
.env.Told through Priya, an underwriter at a mid-market direct lender, pricing a first-lien term loan to a healthcare-services borrower.
JTBD. When Priya prices a deal, she wants the observed spread distribution for comparable positions, so she can defend her spread to committee. Primary CTA. "Show comparables"
┌──────────────────────────────────────────────────────────────┐
│ Coverage: 8 BDCs · 2023-Q3 to 2025-Q2 · 1,2xx borrowers · as of 2025-06-30 │
├──────────────────────────────────────────────────────────────┤
│ Industry [Healthcare services ▾] Size [$20M–$50M ▾] Instrument [First lien ▾] │
│ [ Show comparables ] │
├──────────────────────────────────────────────────────────────┤
│ n = 23 positions · 6 filings · median S+575 · IQR 525–650 │
│ ▁▂▅█▆▃▁ spread_bps histogram │
├──────────────────────────────────────────────────────────────┤
│ borrower · filer · period · spread · principal · FV/cost · accession │
│ ... │
└──────────────────────────────────────────────────────────────┘
JTBD. When Priya's monitoring colleague opens the quarter, she wants the 15 names most likely to go on non-accrual next quarter, with reasons, so she spends her 15 reviews well. Primary CTA. "Open filing" (per holder)
┌──────────────────────────────────────────────────────────────┐
│ Coverage banner (same as screen 1) │
├──────────────────────────────────────────────────────────────┤
│ Review list · period 2025-Q2 · top 15 · precision@15 on held-out: see eval │
├──────────────────────────────────────────────────────────────┤
│ # · borrower · score · why (FV/cost 0.82 ↓2q; PIK; 3 holders) · dispersion · filings │
│ 1 ... [Open filing ▾] │
│ ... │
└──────────────────────────────────────────────────────────────┘
Changelog
| Date | Change | Who |
|---|---|---|
| 2026-09-06 | First draft from the brief | pm |
| 2026-09-06 | v2 after the seven-persona panel: citation rule for aggregates and model outputs (B1), kill criteria (B2), hypothesis narrowed to BDC-observed comparables and allocator path stated (B3), n-positives risk, session format for open question 1 | pm |
BDC Footnotes extraction is the earlier project this build reuses: its code that finds the loan list in a filing, and its ideas for reading non-accrual footnotes (credited in src/parallax/soi/section.py and src/parallax/soi/non_accrual.py). See BDC Footnote Tape.
Built by a two-role AI agent team (PM and AI Eng) plus an independent evaluator, run by Alex as owner. Each cycle: the PM asks, Eng builds, the evaluator re-runs every check, the PM gives a verdict (ADVANCE, LOOP, PIVOT). Source: team/PROTOCOL.md, team/board.md.
| Stage | Artifact | Repeatable with |
|---|---|---|
| Opportunity | PROJECT_BRIEF.md: 3 personas and their jobs. No user interviews | /opportunity-analysis |
| PRD | outputs/prds/parallax-team-kickoff.md v2, after a 7-persona review panel | /prd-draft, /prd-review-panel |
| Criteria | team/criteria.json v9: 35 criteria, 32 of them must pass to move on | /goal-gate |
| Build loop | Cycles 0 to 11: PM ask, Eng report and evaluator score each cycle | /prd-graph (same maker, reviewer, evaluator, router loop) |
| Decisions | D-001 to D-018 verdicts, incl. the D-018 switch to a showcase | /decision-doc |
| Showcase | This page, name audit, two-story demo | /portfolio-case-study, /name-audit |
Skills are mapped from the artifact each stage produced. They show how each stage repeats with PM OS; the original working sessions were not logged.
/decision-doc /goal-gate /name-audit /opportunity-analysis /portfolio-case-study /prd-draft /prd-graph /prd-review-panel evaluator reviewer router