Case study · Private credit · Public SEC data

What other lenders charge and how they value the same loans, with the filing behind every number.

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.

Try the demo Code Real public SEC filings, a fixed copy taken 2026-09-06. No AI language model. Project codename: Parallax.
42,830loan rows from 64 filings of 8 BDCs
48 of 48money values read correctly in a check sample
0.9855share of matched borrower names that are correct (68 of 69)
3 of 15flagged borrowers went non-accrual next quarter (2025-12-31), against 22 of 1,423 overall
For
A lender to mid-sized private companies, such as a private credit fund
Users
You, pricing a new loan or reviewing existing ones, and your head of lending, who pays
Data
8 BDCs, 8 quarters, 2,348 matched borrowers
Built with
Python, Streamlit, an AI agent team (PM, engineer, evaluator)
TechniquesData engSEC EDGAR ingestion: 64 filingsData engHTML table parsing across 8 layoutsData engEntity resolution: blocking, fuzzy matching, union-findData engNeo4j borrower and lender graphData scienceXGBoost vs a simple baseline, held-out AUCData sciencePrecision@15 backtestData scienceWilson confidence intervalsEvalMLflow experiment trackingAIMulti-agent build loop: PM, engineer, evaluator
01 · VISION

Every margin at committee cites what other lenders charge.

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.

02 · THE PROBLEM

Six things go wrong today.

1 · No single table

Valuations and margins sit in 64 filings with 8 table layouts, so you can't compare lenders.

2 · Names don't match

Each lender spells the same borrower differently, so their valuations can't sit side by side.

3 · Your margin has no source

A margin without its lenders and rows can't be defended at committee.

4 · One lender looks like a market

Each BDC names industries its own way, so most industry groups hold one lender's loans.

5 · Your review time goes unranked

With time for 15 borrowers, you have no list sorted by how other lenders value them.

6 · What changed is hidden

Valuation moves and new names need last quarter's filing open next to this one.

03 · THE SOLUTION

Code reads the filings. You price and review from the rows.

  1. Every loan list is read into one table. 64 annual and quarterly reports of 8 BDCs from SEC EDGAR: margin, amount owed, cost, fair value, non-accrual flag and SEC filing ID per row.
  2. Borrower names are matched across lenders. Names scoring 93 or more are linked: 2,348 borrowers.
  3. Price a loan. Pick industry, loan type and size. You see the lender count first, then quartiles, a lender table and source rows with SEC links. A warning shows when every loan comes from one lender.
  4. Review the right 15. Pick a quarter and how many borrowers you can review. You get the lowest valuations first, what changed, each lender's valuation, and a hindsight check of which flagged borrowers went non-accrual.
04 · HYPOTHESES AND IMPACT

One job, one bet, one number each.

Your jobHypothesisMetric and target, then resultExpected 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 48Comparisons 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 unobservedFaster 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 unobservedFewer 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,415Review 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 unobservedMore 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.

05 · THE VALUE

What it's worth, and what it asks of you.

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.

You, pricing a new loan
You get
"My margin sits at the median of 90 first-lien insurance loans across 4 BDCs. Here are the rows."
Will you believe it?
Medium. Rows and filings can be checked. Many industries have few loans: Software first lien has 5 loans from 2 lenders.
How fast
Low delay. The answer shows on selection. Latest quarter in the copy is 2026-06-30.
What it takes
Medium. You guess which industry name in the filings fits the borrower. Industry names are never merged.
What you give up
Low. Public data only. No full yield: fees and upfront discounts (OID) aren't in the filings.
You, reviewing loans
You get
"My 15 borrowers this quarter are the ones lenders value lowest, with what moved and the filing one click away."
Will you believe it?
Low to medium. Looking back, 3 of 15 (2025-12-31) and 1 of 15 (2026-03-31) went non-accrual the next quarter. Wide uncertainty ranges.
How fast
High delay. The next filing shows whether the list was right: one quarter later.
What it takes
Low. One sorted list, sized to your time.
What you give up
Medium. It covers only borrowers of 8 BDCs, not your own loans. No machine-learning score.
Your head of lending
They get
"Margins at committee cite what other lenders charge, and review time goes to the lowest valuations."
Will they believe it?
Low. No user tests. Release checks failed (D-018).
How fast
High delay. Effects on pricing and money recovered show over several quarters.
What it takes
Medium. Someone matches the table to the fund's own loans and keeps the filing reader working each quarter.
What they give up
Low. Public data, no data vendor to depend on, $0 to run.

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.

06 · LIMITATIONS

What this doesn't prove yet.

07 · SCOPE AND NEXT STEPS

What it won't do, and what comes next.

Non-goals
  • A warning before the valuation falls. The model lost to the simple rule; its scores are hidden.
  • Full pricing. Fees, upfront discounts and early-repayment terms aren't in the loan lists.
  • Wider coverage. 8 BDCs, 8 quarters. No banks, private funds or buyout-firm owner names.
  • A merged industry list, by choice: it would invent a statistic no filing reports.
  • Login, scheduling, cloud hosting, investment advice.
Next
  1. Fix the failed release checks before any adoption talk.
  2. Two pricers, 5 practice committee questions, broker notes vs the screen.
  3. Your own review list vs this list on the same quarter, judged by the next filing.
  4. A new hand-labelled sample for name matching, threshold fixed before labelling.
  5. More non-accrual cases (60 pooled) before another model attempt.
RESOURCES

Go deeper.

Try the demo, step by step three steps, in order
  1. Open the demo, or run it locally: pip install -r requirements.txt, then streamlit run app/two_stories.py from the showcase folder.
  2. Price a loan: pick 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.
  3. Review list: pick 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.

Main learnings coverage, reading check, name matching, both stories
  • Coverage: 8 BDCs, 8 quarter ends from 2024-09-30 to 2026-06-30, 64 filings (16 10-K, 48 10-Q), 42,830 loan rows, 2,348 matched borrowers. Source: tools.coverage() on showcase/data. Supports 1.
  • Reading check: 48 of 48 correct on amount owed, cost and fair value, Wilson 95% confidence interval [0.9259, 1.0000] each. The person who wrote the reader drew the sample, from the same data. Source: eval/parse_audit_results.json. Supports 1.
  • Name matching: pair precision 0.9855 (68 of 69), CI [0.9224, 0.9974]; recall (share of true matches found) 0.9315 on 73 labelled true pairs; group precision 0.85 (17 of 20), CI [0.6396, 0.9476]. Threshold 93 was chosen on the same sample. 187 borrowers held by 2+ BDCs at 2026-06-30. Source: eval/er_results.json. Supports 2.
  • Story 1, Software first lien: 5 loans, 2 lenders (GBDC 4, PSEC 1), median S+675. Too few to rely on. Source: get_comparables, asserted in tests/test_showcase.py. Supports 3, 4.
  • Story 1, Insurance first lien: 90 loans, 4 lenders, median S+475; ARCC supplies 52 of 90. Source: two_stories.lender_table. Supports 3, 4.
  • Story 1, Software and Services first lien: 193 loans, all from ARCC: one lender's loans. Source: get_comparables. Supports 4.
  • Story 2, 2026-03-31: 15 borrowers, 5 new since 2025-12-31; 1 of 15 went non-accrual next quarter, Wilson CI [0.01, 0.30]; overall rate 19 of 1,415. Source: two_stories.hindsight. Supports 5, 6.
  • Story 2, 2025-12-31: 3 of 15, CI [0.07, 0.45]; overall rate 22 of 1,423. Both quarters match the saved simple-rule precision@15 (0.200, 0.067) in eval/model_results.json, checked by a test. Supports 5.
  • Story 2, all 7 quarters: listed borrowers with a known outcome that went non-accrual range from 0 to 4. Three quarters show 0. Weak support for 5.
  • Tests: 12 passed (7 new for the two-story app, 5 existing), including checks that the app loads with no error (AppTest). The local server health check returned 200.
Hypotheses in full before and after, and the test for each

1 · NO SINGLE TABLE

Before -> after
64 filings read one by one -> one table of 42,830 loans, each row with its SEC filing ID
Hypothesis
If every loan list is read into one format, then comparisons and review lists work across 8 lenders, because the filings already hold each loan's valuation and margin.
Metric
Accuracy of the money columns on a fixed check sample. Up. Starting point: 48 of 48 correct on amount owed, cost and fair value. Floor: below 0.90 on any money column means drop the table (README rule).
Test
Draw a new sample each quarter and check rows against the filing text.

2 · NAMES DON'T MATCH

Before -> after
each lender's spelling -> one borrower ID across lenders
Hypothesis
If names are matched with measured accuracy, then one borrower's valuations from several lenders sit side by side, because a wrong match would show a fake disagreement.
Metric
Share of matched pairs that are correct (precision), at the chosen threshold. Up. Starting point 0.9855, n=69. Floor: precision of whole matched groups, starting point 0.85, n=20.
Test
New hand-labelled sample, threshold fixed before labelling. Below 0.95 means stop (README rule).

3 · COMPARISONS WITHOUT SOURCES

Before -> after
a quoted margin -> quartiles, lender table and source rows with SEC links
Hypothesis
If the margin comes with its lenders and rows, then the pricer answers the committee's "says who?" from the screen, because each row links to the filing.
Metric
Time to answer where a proposed margin comes from. Down. Starting point unobserved.
Test
Two pricers, 5 practice committee questions, broker notes vs the screen.

4 · ONE LENDER AS A MARKET

Before -> after
a group median read as the market -> lender count first, one-lender warning, similar industry names listed but not merged
Hypothesis
If the screen names which lender's loans drive the number, then fewer one-lender medians reach committee as "market", because the warning appears before the quartiles.
Metric
Share of cited comparisons resting on one lender. Down. Starting point unobserved.
Test
Same practice session: count medians cited without their lender count.

5 · UNSIZED REVIEW LIST

Before -> after
no ranked list -> list sized to review time, lowest lender valuation first
Hypothesis
If the reviewer checks the 15 lowest-valued borrowers, then more of them go non-accrual next quarter than a random 15, because a lender valuing a loan below cost is publicly admitting trouble.
Metric
Share of the 15 flagged that went non-accrual next quarter (precision@15). Up. Starting point: 3 of 15 (2025-12-31), 1 of 15 (2026-03-31), against overall rates of 22 of 1,423 and 19 of 1,415. The reviewer's own list: unobserved.
Test
Reviewer's list vs this list on the same quarter, result from the next filing.

6 · HIDDEN CHANGE

Before -> after
reopen last quarter's filing -> valuation change, NEW marker, each lender's valuation and filing link per row
Hypothesis
If each row shows what moved and links its filing, then the reviewer spends less time per borrower before escalating, because the change and source sit on one line.
Metric
Minutes per reviewed borrower. Down. Starting point unobserved.
Test
Time one reviewer on 15 borrowers with filings only vs with the list.

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.

All sub-problems the 11 found, what's tackled, and where in the code
  1. No single table. Valuations and margins sit in 64 web-page filings with 8 different table layouts.
  2. Names don't match. Each lender spells the same borrower differently, so their valuations can't be compared.
  3. Comparisons have no sources. A margin without its lenders and rows can't be defended at committee. (Story 1)
  4. One lender looks like a market. Each BDC names industries its own way, so most industry groups hold one lender's loans. (Story 1)
  5. Review list isn't sized or ordered. A reviewer with time for 15 borrowers has no ranked list. (Story 2)
  6. What changed is hidden. Valuation moves, lender disagreement and new names need a quarter-on-quarter comparison and the filing. (Story 2)
  7. Warning before the valuation falls. Predicting non-accrual earlier than the lowest valuation does.
  8. Full pricing. Fees, upfront discounts (OID) and early-repayment terms aren't in the loan lists.
  9. Coverage. Only 8 BDCs and 8 quarters. No banks, private funds or names of the buyout firms that own borrowers.
  10. Investor valuation check. Whether a fund manager's valuation agrees with other lenders to the same borrower.
  11. Industry mapping. One shared list of industry names across lenders.

This build tackles 1 to 6.

#StatusCode
1Tackledsrc/parallax/ingest.py, src/parallax/parse.py, src/parallax/soi/ -> showcase/data/tape_resolved.parquet
2Tackledsrc/parallax/resolve/, measured in eval/er_results.json
3Tackledshowcase/src/parallax/tools.py get_comparables; showcase/app/two_stories.py matched_positions, lender_table
4Tackledget_comparables related_labels_not_used, filer_mix; one-lender and few-loans warnings in two_stories.py story_price
5Tackledtools.py review_list (sort by fvc_min, capacity k); two_stories.py story_review
6Tackledreview_list fields d_fvc_1q, new_since_prev_period, holders, accessions; two_stories.py hindsight
7Attempted, not achievedsrc/parallax/model.py, eval/model_results.json: the XGBoost model did not beat the simple rule
10Partial, not a storytools.py get_borrower; each lender's valuation shown inside story 2
8, 9, 11Not tacklednone
How it's built download, read, match names, build, two stories
  1. Download the annual (10-K) and quarterly (10-Q) reports of ARCC, OBDC, FSK, GBDC, MAIN, TSLX, HTGC and PSEC from SEC EDGAR. Save and index them: 64 filings.
  2. Find each filing's loan list (Schedule of Investments) and read it. Result: 42,830 loan rows with margin, amount owed, cost, fair value, non-accrual flag and SEC filing ID.
  3. Match borrower names: clean them, group likely pairs, score how alike they are, and link names scoring 93 or more. Result: 2,348 borrowers.
  4. Build one row per borrower per quarter: lowest lender valuation (fvc_min), change since last quarter, lender disagreement, and whether it went non-accrual next quarter.
  5. Story 1: pick industry, loan type and size. The tool takes exact industry-name matches from each lender's latest quarter. It shows quartiles, a lender table and source rows with SEC links.
  6. Story 2: pick a quarter and how many borrowers you can review. The tool sorts by 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.

How success is tracked early and late signals
MetricTypeTests hypothesisHow measured
Money-column accuracy per new quarterEarly signal1Fixed random sample checked against filing text
Name-matching precision, pairs and groups, on a new sampleEarly signal2Hand-labelled pairs at the fixed threshold, Wilson confidence interval
Time to answer a committee question on sourcesEarly signal3Timed practice committee, 2 pricers
Share of cited medians resting on one lenderEarly signal4Count in practice committee notes
Minutes per reviewed borrowerEarly signal6Timed review session
Precision@15 vs overall rate, per quarterLate signal5Next filing's non-accrual flags, Wilson confidence interval, already in the demo
Committee margins later re-pricedLate signal3, 4Unobserved: needs the lender's own deal records
Scope in scope and out of scope
In scopeNot 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.
Trade-offs what was chosen, over what, and the cost
ChoseOverGainCost
Rank by lowest observed valuationXGBoost scoreNo unverified score; model not better on held-out dataDoesn't learn from other data columns
Exact industry names from filings, similar ones listedMerged industry listNo invented market statisticFew loans per group; user picks the industry name
Fixed public copyLive data feed$0, works offline, same result every runData ages each quarter
Hindsight check beside the listPrecision claim in textBelief rests on visible outcomesShows quarters with 0 hits
Two stories in the demoNine-tab walkthroughA visitor finishes in minutesGraph, ML and agent work sit in the full app only
Terms on this page plain definitions
  • BDC: a stock-listed fund that lends to private companies.
  • Valuation (the "mark"): the lender's own estimate of a loan's worth, as a share of what it paid.
  • Interest margin (spread): what a lender charges above a benchmark rate. S+475 means the benchmark plus 475 hundredths of a percent.
  • First lien: a loan that is repaid first if the borrower fails.
  • Non-accrual: the lender stops counting a loan's interest as income, because payment is in doubt.
  • Name matching: linking one borrower that each lender spells differently.
  • Hit rate (precision@15): of 15 flagged borrowers, the share that went non-accrual the next quarter.
  • AUC: how well a score ranks future problem loans ahead of the rest. Higher is better.
Product requirements the Team Kickoff PRD, v2 after a 7-persona review

Parallax: private-credit loan tape and early warning

0) Meta

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

1) Problem and hypothesis

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.


2) Scope and non-goals

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.


3) Expected impact

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.


4) AI behaviour contract (M7 agent)

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.


5) Success

Success is team/criteria.json. No number here that isn't in the brief.

  • Gated: the 28 ids marked gated: true, M1.1 to M8.4 and D1 to D3. They are the brief's acceptance tests.
  • Product, non-gated: P1 (underwriter), P2 (monitoring analyst), P3 (allocator), one per JTBD. A failure is a README finding with a written reason, not a block.
  • Passing bar: every gated criterion PASS on observed output.

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.


6) Risks

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

7) Open questions

  • [ ] Are the three JTBDs real for practitioners Alex knows? Owner: Alex, after M8: two 20-minute task sessions, "price this deal" on screen 1 and "pick your 15" on screen 2.
  • [ ] Which 8 quarters, given PSEC (June) and GBDC (September) fiscal year-ends? Resolved at M1: the 8 most recent period-ends per filer.
  • [ ] "Size" for comparables: principal, cost or fair value? Resolved at M7; PM default is principal.
  • [ ] Does the non-accrual LLM fallback fit the Gemini free tier? Resolved at M2, call count logged.
  • [ ] LangSmith or MLflow tracing? Resolved at M7 by whichever key is in .env.

Annex A: napkin sketch

Told through Priya, an underwriter at a mid-market direct lender, pricing a first-lien term loan to a healthcare-services borrower.

Screen 1: Price a deal

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 │ │ ... │ └──────────────────────────────────────────────────────────────┘

Screen 2: This quarter's review list

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 ▾] │ │ ... │ └──────────────────────────────────────────────────────────────┘


Appendix

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
Related project and links BDC Footnote Tape, demo, code

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.

How it was built with PM OS the AI agent team loop, step by step

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.

StageArtifactRepeatable with
OpportunityPROJECT_BRIEF.md: 3 personas and their jobs. No user interviews/opportunity-analysis
PRDoutputs/prds/parallax-team-kickoff.md v2, after a 7-persona review panel/prd-draft, /prd-review-panel
Criteriateam/criteria.json v9: 35 criteria, 32 of them must pass to move on/goal-gate
Build loopCycles 0 to 11: PM ask, Eng report and evaluator score each cycle/prd-graph (same maker, reviewer, evaluator, router loop)
DecisionsD-001 to D-018 verdicts, incl. the D-018 switch to a showcase/decision-doc
ShowcaseThis 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.

PM OS skills used

/decision-doc /goal-gate /name-audit /opportunity-analysis /portfolio-case-study /prd-draft /prd-graph /prd-review-panel evaluator reviewer router