Metadata-Version: 2.4
Name: dfx-intel
Version: 0.1.1
Summary: Evidence-backed private markets research for agents: DFX Intelligence over MCP and A2A, standard library only.
Author: Deal Flow Xchange Inc.
License-Expression: MIT
Project-URL: Homepage, https://dfxintel.com/agents
Project-URL: Documentation, https://dfxintel.com/agents/docs
Project-URL: Plans, https://dfxintel.com/data-factory/plans
Project-URL: Repository, https://github.com/Capital-W-Holdings/us-property-parcel-real-estate-debt
Project-URL: Examples, https://github.com/Capital-W-Holdings/us-property-parcel-real-estate-debt/tree/main/examples/sdk
Project-URL: Issues, https://github.com/Capital-W-Holdings/us-property-parcel-real-estate-debt/issues
Keywords: private markets,private credit,mcp,a2a,agents,research
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Office/Business :: Financial :: Investment
Classifier: Intended Audience :: Developers
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: langchain
Requires-Dist: langchain-core>=1.6; extra == "langchain"
Provides-Extra: openai-agents
Requires-Dist: openai-agents>=0.23; extra == "openai-agents"
Provides-Extra: test
Requires-Dist: pytest>=8; extra == "test"
Dynamic: license-file

# dfx-intel (Python)

A small client for DFX Intelligence, an evidence-backed research service on private markets: private credit, private
equity, venture, family offices, registered investment advisers, public pension allocators and US commercial real
estate. It is written for agents. Every answer carries its claims, the URLs of the documents those claims come from,
what is uncertain, and what lies outside coverage.

The core uses only the standard library (Python 3.9+). The framework adapters are optional extras.

```
pip install dfx-intel
```

## Quick start

```python
from dfx_intel import research

r = research("Which BDCs lend to Kaseya, and which of them are current lenders?")
print(r.state, r.answer_state)      # OK OK
print(r.answer)                     # one or two sentences, built only from cited claims
for c in r.claims:
    print(c.claim_id, c.predicate, c.claim_type)   # VERIFIED_FACT, REPORTED_FACT, DERIVED, INFERENCE ...
for url in r.evidence_urls:
    print(url)                      # the SEC filing, IAPD report or plan disclosure behind the claims
print(r.uncertainty, r.coverage_limits)
if r.has_more:
    page2 = research(continuation=r.continuation)
```

Public, runnable examples (anonymous research, citations, errors, pagination, a member key from the environment
and a short tutorial), each with the output of a live run, are at https://github.com/Capital-W-Holdings/us-property-parcel-real-estate-debt/tree/main/examples/sdk.

`examples/` in the SDK source tree holds further scripts. Each one ran once (against production, except the two receipt readers, which run offline), and its real output sits next to it
(`.out.txt`, plus `.out.json` for the raw payload). The UTC time of the run is in `.ran_at`.

| example | investigation |
|:-|:-|
| 01_lenders_to_portfolio_company.py | Lenders: which BDCs lend to Kaseya (an Insight Partners company), then the next page through the continuation |
| 02_advisor_moves.py | People: which advisors joined Rockefeller Capital Management in the last 90 days, from IAPD |
| 03_allocator_commitments.py | Allocator: which funds CalPERS committed to in 2025, and for how much (see known issues below) |
| 04_verify_and_resolve.py | Verify "Blackstone Private Credit Fund lends to Auctane", then resolve "Thoma Bravo" to DFX ids |
| 05_not_covered.py | NAV per share of Ares Capital, which DFX does not read (the limit is stated in the answer) |
| 06_delegate_a2a.py | The same lender question delegated over A2A (message/send, then tasks/get) |
| 07_read_receipt.py | Read a research receipt (dfx.receipt/2): facts vs inferences, relationships, freshness, and the exact follow-up calls; runs offline on the bundled receipt (see ../RECEIPTS.md) |
| 08_receipt_as_documents.py | A receipt as retrieval documents (LangChain Documents when installed), marker and source in metadata |
| 09_sponsor_portfolio_credit.py | Sponsor and financing: which BDCs lend to Thoma Bravo portfolio companies, and how much they hold |
| 10_ownership_and_lenders.py | Ownership and lenders of one company (Hyland Software), and how to tell from `plan.operations` which half was answered |
| 11_fund_lps.py | Fund relationships: which public pension plans committed to Vista Equity Partners funds, and for how much |
| 12_langchain_tool.py | The LangChain StructuredTools, invoked as a ToolNode would (no model key needed) |
| 13_openai_agents_tool.py | The OpenAI Agents SDK function tools, invoked through `on_invoke_tool` (no model key needed) |
| 14_claude_agent_sdk.py | The Claude Agent SDK options (`mcp_servers`, `allowed_tools`); runs a query only with `DFX_RUN_AGENT=1` |

Every example except 07 and 08 (which read the bundled receipt offline) runs anonymously against production with no paid API. The adapter examples run an agent only when you
set a model key yourself.

### Checked against the primary documents (2026-10-12)

Before these outputs were published, figures in them were read against the documents they cite:

| example | claim in the output | primary document | result |
|:-|:-|:-|:-|
| 01 | Blackstone Private Credit Fund holds Kaseya debt, principal $161.7M, fair value $98.5M at 2026-06-30 | BCRED 10-Q for 2026-06-30, schedule of investments (two Kaseya term loans: $36.7M at $22.0M and $125.0M at $76.4M, in thousands as printed) | matches |
| 02 | An advisor registered with Rockefeller Capital Management from 2026-09-30, after Truist Advisory Services (ended 2026-10-01) | That advisor's IAPD report (IA registered since 09/30/2026; Truist 04/18/2022 to 10/01/2026) | matches |
| 09, 10 | Hyland Software is a Thoma Bravo portfolio company | Golub Capital's 2023 release on Hyland's $3.4B unitranche ("a portfolio company of Thoma Bravo since 2007"). The URL DFX cites for it now answers 404; the release is at golubcapital.com/?p=2714 | matches, citation link dead |
| 09 | Thoma Bravo invested in Grayshift | Peak Equity, 2022-07-07: "Thoma Bravo ... announced the completion of its strategic investment in Grayshift, LLC" | matches |
| 10 | Blue Owl Technology Finance Corp. holds Hyland Software debt, principal $147.5M, fair value $142.9M at 2026-06-30 | ORTF 10-Q for 2026-06-30 (first lien loan $147,542K principal at $143,116K; undrawn revolver at $(215)K) | matches |
| 11 | Minnesota State Board of Investment committed $200M to Vista Equity Partners Perennial, vintage 2020 | MSBI private markets asset listing, as of 2026-06-30 | matches |
| 11 | New Jersey committed $300M to Vista Equity Partners Fund VII (2018), $200M to Vista Co-Invest Fund 2017-3, Fund VI and Fund IV | NJ Division of Investment Directors Report, period ending 2026-05-31 | matches |

### Known issues seen in these outputs

They are left in the outputs, unedited, because the outputs are real. Each was reported to the DFX research team.

- An Auctane run (not shipped as an example) booked a joint venture's loan to its parent BDC: Blackstone Private
  Credit Fund's Auctane position was given as $889.4M at fair value, while BCRED's own schedule shows $832.0M and the
  other $57.3M is held by Emerald JV LP.
- 03: one fund name carries a row number from the source table ("29. Blackwell Capital Partners II"), and one
  evidence URL is a Census Bureau survey documentation page, not a CalPERS document.
- A two-part question can come back with only one part answered and no sentence saying so. Example 10 shows how to
  read `raw["plan"]["operations"]` to see which lookup returned NO_MATCH.
- `verify` for an anonymous caller returns SUPPORTED with every source URL LOCKED, so the caller cannot open the
  document. The amounts and dates still come back.

## API

| call | transport | returns |
|:-|:-|:-|
| `research(question, *, continuation=None, detail=None, depth=None, entity=None, workflow=None, as_of=None, since=None)` | MCP `tools/call research` on `/mcp/core` | `ResearchResult` |
| `verify(claim, **structured)` | MCP `tools/call verify` | `VerifyResult`: SUPPORTED, PARTIALLY_SUPPORTED, CONTRADICTED or UNKNOWN |
| `resolve(name, *, domain=None, entity_type=None)` | MCP `tools/call resolve_name` | `ResolveResult`: DFX ids, with `total_lower_bound` meaning "at least N" |
| `delegate(question, *, arguments=None, context_id=None, poll_interval=2.0, max_wait=120.0)` | A2A 0.3 `message/send` on `/a2a`, then `tasks/get` until final | `DelegateResult`: the task, with `.result` as a `ResearchResult` |

The module level functions share one `Client()`. To set options, build your own client:

```python
from dfx_intel import Client
dfx = Client(agent_name="acme-research-agent", user_agent_suffix="acme/1.2")
```

Every result keeps the server's payload, unchanged, under `.raw`. Fields lifted onto `ResearchResult`:

- `state` and `answer_state`
- `answer` and `basis`
- `claims`
- `evidence_urls`
- `uncertainty` and `coverage_limits`
- `withheld`
- `continuation` and `page`
- `next`: follow-up calls with exact arguments
- `receipt_url`: passed through when the server provides one, None otherwise
- `interaction_id`
- `retry_after_seconds` and `error`

## States, not exceptions

`NO_MATCH`, `NOT_COVERED`, `UNKNOWN`, `RATE_LIMITED` and `INVALID_ARGUMENT` arrive as `result.state`. They do not
raise. Each tells the agent something:

- `NO_MATCH`: the name resolved, but nothing matches. Near misses are in `.raw["candidates"]`, and they are never
  the subject.
- `UNKNOWN`: the subject could not be resolved, or there is no evidence either way. Absence of evidence is never a
  contradiction.
- `NOT_COVERED`: DFX does not read that kind of record. An answer can also be `OK` and still list a `NOT_COVERED`
  item in `coverage_limits`.
- `RATE_LIMITED`: `retry_after_seconds` says when to try again.
- `TEMPORARILY_UNAVAILABLE`: the exchange is at capacity or not answering (HTTP 503). Nothing ran and nothing was
  charged; `retry_after_seconds` is set when the exchange sent one.
- `API_KEY_INVALID`, `NOT_IN_PLAN`, `ENTITLEMENT_REQUIRED`: the key is not recognised, or the plan does not cover the
  call. `.raw` carries `how_to_fix` or the plans URL.
- `HTTP_ERROR`: any other non-2xx answer; `.error` carries the server's description (for example the member door's
  401 "sign in with your DFX account").

Only transport failures raise `TransportError` (DNS, TLS, an unreadable body, or no answer within `timeout`, 90 s by
default). An A2A endpoint that answers 404
gives a `DelegateResult` whose `.raw["state"]` is `NOT_DEPLOYED`.

## Free and member access

**Anonymous (free, no key, no sign-up).** All the read tools work. Per DFX's published policy:

- A list names its first 5 rows in full and gives only a count of the rest (`withheld`, `locked`).
- A record names its subject and the first 3 related names per section.
- Contact values and decision-maker names are never returned.
- Public filer identity and dated public-record facts are served. Resolved relationships come as counts. Derived
  scores are not served.

Anonymous use is limited per network address, not per agent: 20 calls a minute and 300 an hour, and the search and
changes tools (research is one of them) share 60 an hour. Callers behind one office or cloud address share these
budgets (`budget.identity_basis` `network_prefix` in the RATE_LIMITED envelope). `agent_name` (X-Agent-Name) labels
your agent; it does not give it a separate budget. A refused call is not charged and carries `retry_after_seconds`.

**Member.** Set `DFX_API_KEY` to a `dfx_live_` account key, or pass `api_key=`. The key goes out as
`Authorization: Bearer <key>`. It unlocks the rows withheld from anonymous callers on the verticals your plan
covers. A member is metered on the membership's own allowance in retrieval units a day and a month, not in calls:
one research call uses 6 to 18 units depending on its depth. Allowances by plan: https://dfxintel.com/data-factory/plans

Withheld is not missing. When an answer says rows are withheld, they exist, and the answer counts them.

## Coverage limits (stated by the service itself)

- **Private credit** comes from BDCs' own schedules of investments in SEC filings. Banks, CLOs and private funds that
  file no schedule are invisible. Principal is the sum of BDC pieces, so it is a lower bound.
- **Advisor moves** use IAPD registration end and begin dates, not announcement dates. The book of business is not
  public.
- **Allocator commitments** are the plan's own printed disclosures. Deployed capital cannot be observed from them.
- Some rows cite only a dataset, or nothing. The answer says so (`uncertainty`, `citation_check`), and
  `evidence_urls` never invents a URL for them.

Each answer lists its own limits in `coverage_limits` and `do_not_conclude`. Read them before you rely on a negative.

## Framework adapters

| framework | status | how |
|:-|:-|:-|
| LangChain / LangGraph | shipped, tested offline against langchain-core 1.6.9, run live 2026-10-12 (example 10) | `pip install "dfx-intel[langchain]"`, then `from dfx_intel.adapters.langchain import dfx_tools` |
| OpenAI Agents SDK | shipped, tested offline against openai-agents 0.23.1, run live 2026-10-12 (example 11) | `pip install "dfx-intel[openai-agents]"`, then `from dfx_intel.adapters.openai_agents import dfx_tools` |
| Claude Agent SDK | config helper only (DFX is a remote MCP server) | `from dfx_intel.adapters.claude_agent_sdk import mcp_servers, allowed_tools` |
| CrewAI | snippet only | below |

Each adapter wraps the same two plain functions (`dfx_research(question, continuation="")` and `dfx_verify(claim)`).
Both return JSON that keeps the state, claims, evidence URLs, uncertainty and continuation.

Claude Agent SDK:

```python
from claude_agent_sdk import query, ClaudeAgentOptions
from dfx_intel.adapters.claude_agent_sdk import mcp_servers, allowed_tools

options = ClaudeAgentOptions(mcp_servers=mcp_servers(), allowed_tools=allowed_tools())
```

CrewAI. This is a snippet, not an adapter. CrewAI could not be installed in the environment where this package was
built (Python 3.14; its tiktoken dependency failed to build), so no tested adapter ships. CrewAI agents can use DFX
as a remote MCP server directly:

```python
from crewai import Agent
analyst = Agent(role="Private credit analyst", goal="Answer with cited sources", backstory="...",
                mcps=["https://exchange-production-9123.up.railway.app/mcp/core?c=crewai"])
```

## Endpoints and attribution

- MCP: `https://exchange-production-9123.up.railway.app/mcp/core?c=sdk-py`. A2A: `/a2a` on the same host. Override the
  host with `DFX_BASE_URL` or `base_url=`.
- Every request sends `User-Agent: dfx-intel-python/<version>`.
- Anonymous URLs carry `?c=sdk-py` so DFX can count calls made through this package. LangChain tools use
  `?c=langchain`. Pass `channel=None` to send no tag.
- A member request is never tagged: a request with a key, or a URL ending in `/member`, goes out bare.
- A2A tasks are held in the serving process's memory for up to 24 hours. A restart drops them, and `tasks/get` then
  answers task not found.

## Tests

```
python3 -m pytest -q sdk/python/tests
```

The tests are offline. They run against recorded production responses in `sdk/fixtures`, and each fixture names
where it came from.

## Licence

MIT. See `LICENSE`.
