investor-search

Name a country, get its verifiable investors: family offices, VCs, angels. Rejects advisers and lawyers. Every field is sourced, gaps stay empty. Nothing guessed. Stops on evidence…

Fahad Farooq

@chainleo

Install

$ openclaw skills install @chainleo/investor-search

Global Investor Search

Ask any agent for family offices in a country and it returns the number you asked for, not the number in the country. This skill replaces it with a rule, and records where every field came from and how well it was checked.

This file is the rules. Two of the reference files are read while working, not afterwards: open reference/lists.md before round 1 to add this market's word forms, and reference/environment.md when you write the output or resume from a paste. why.md is for when a rule looks arbitrary.

filewhen
SKILL.mdworking — every operative rule, nothing else
reference/lists.mdlegal forms, generic tails, place words, type words — extend per market
reference/why.mdthe measurements and the failures that produced each rule
reference/environment.mdwhere files go, the ledger format, delivering a file to the user

The six jobs, in order

0  Is it an investor?      reject the advisers who sell to them
1  Is the search finished? stop on six dry rounds, not on a number
2  Does it clear the bar?  name · not a dupe · source · headquarters
3  Is it one firm or two?  fold to compare — merge only with proof
4  What is it?             classify from evidence, never from the name alone
5  What is still missing?  fill the same row across rounds; empty stays empty

Before Job 0 — the file comes first

The memory of this skill is its files. Deduplication, the pending queue, the round history and the exhaustion gates all read from disk. An agent that cannot write still runs every judgement here — Job 0, the fold, the provenance rules, the language test, empty stays empty — it simply cannot remember making them.

Do not try to classify the environment. Test it, and report what actually happened. Persistence is not observable today. What is: whether a read finds something, and whether a write succeeds.

1 · Read before you search

Everything lives under one predictable path, because nothing tells an agent which files it already has — so the names are fixed here rather than chosen per run.

investor-search/index.md                 one line per market held
investor-search/<market>/investors.csv
                        /sources.csv
                        /investors-pending.csv
                        /investors-rejected.csv
                        /rounds.csv
                        /ledger.txt

Write relative paths and let the host place them. On hosts with a persistent working directory they land in it and are still there on a later run.

<market> is the English short name, lowercase, hyphens for spaces — poland, united-arab-emirates. The same market must produce the same folder every time, or the next run reads an empty path and starts from zero while the data sits one spelling away.

index.md — the one path that never varies

Everything else is found through it, so it is the only file guaranteed to be findable. Plain markdown, one line per market:

# Investor search — markets held

| market | scope | firms | pending | last run |
|---|---|---|---|---|
| poland | investment firms headquartered in Poland | 46 | 12 | 2026-03-14 |

scope is not decoration. Family offices headquartered in Poland and investors active in Poland are different populations, so a round history built for one does not describe the other. If the request's scope differs from the stored one, do not silently continue — say what is on file, say what was asked, and let the user choose: continue the stored scope, or start a second folder. A filter on the same population (…that invest in software) is not a scope change; it stays in the same folder. reference/environment.md has the naming rule for second folders, and for regions, multi-country and sub-national requests.

Write index.md before round 1 with the row you are about to work on, firms and pending left at the previous values or blank, and update it again at the end.

Read index.md first, then every file in the market folder — investors.csv, sources.csv, investors-pending.csv, investors-rejected.csv, rounds.csv. Read them before the first search, not after it.

what you findwhat to say and do
files for this market"Continuing a run of 14 March — 46 firms held." Load them, resume
index.md but not this marketa first run for this market; other markets stay untouched
nothinga first run. Say that too — silence reads as continuity
index.md lists the market but the folder is gonetreat as a first run, and say the index disagreed with the disk
a file is short, truncated or unreadableload what parses, name the file that did not, and shut Gate 0 — a partial round log that reads cleanly is the one input nothing else can check

Before round 1, open reference/lists.md and add this market's legal forms, generic tails and place words. Most field tests skipped this, and the fold failed on local names.

2 · Write in the first minute, not at the end

Create or update index.md before round 1. If writing is going to fail, learn it after one minute rather than after forty rounds of work you are about to lose.

  • Write succeeded → normal run. State where it went, in the first reply. Give the absolute path if the host reports one, and the relative path plus the working directory as you understand it otherwise — but never invent an absolute path. A file the user cannot find is a file that does not exist; a path that turns out to be wrong is worse.
  • Write refused, or there is no file tool → say so in one line, switch to chat output, and never claim exhaustion (Gate 0).

"I cannot write files here, so nothing will carry to a later run. I will print the CSV and a ledger into the chat instead."

A silent write failure is the worst outcome available (why.md). If a previous run's file does not come back, the run is a first run, whatever the write said last time.

3 · Resuming is not the same as trusting

  • Report the age of what you loaded. "The newest check on file is 94 days old." Old rows are evidence of a past state, not a claim about today, and checked is on every row precisely so this can be said.
  • Re-read before the final write. If it changed, do not merge. Two sessions can work the same market at once, and both will have issued the same next id to different firms — so a blind merge reattributes citations rather than combining work. If the file on disk is no longer the one you loaded, write yours beside it as investors-<ISO date>.csv, say plainly that two runs overlapped, and leave the reconciliation to a person who can see both. Never overwrite a file you read forty rounds ago.
  • An id is permanent. Never reissue one, never renumber on rewrite — sources.csv joins to it, and a renumbered file silently reattributes every citation.
  • A shared working directory is shared with everyone using that agent. Say so once when you write investors-rejected.csv, which is a list of named firms carrying a negative claim and was never meant to travel.

4 · Never treat the conversation as memory

History truncates silently. The files, or a pasted ledger, or nothing — never the transcript.

Where nothing persists, end the run with a ledger and say plainly what it does: deduplication next time, and nothing else. Format, delivery and how to read one back are in reference/environment.md.


Job 0 — Investor, or somebody selling to investors?

Run this first, on every candidate. A search for "family office" in any language returns mostly firms selling family-office services. They have the words in their name, a real site and a real address, and they pass every other check.

Measured: 5 of 6 in one market, 52% of everything resolved in another. One targeted query returned 8 results, 8 advisers, 0 investors.

The test

Does this organisation deploy capital, operate businesses, or sell services to people who do?

investor_evidencewhat it iskeep?
investsdeploys capital into things it does not operateyes
operating_groupfamily-owned, runs its own businesses, may also investyes — flagged
syndicateangel network / membership body that routes others' capitalyes — flagged
unclearyou found the firm, and it is genuinely undecidableyes — flagged
—you never found enough of the firm to judge at allno → pending, unresolvedJob0
—sells services to familiesno → investors-rejected.csv, adviserNotInvestor
—not an investment entity at allno → investors-rejected.csv, notAnInvestor

notAnInvestor is for an operating company that a directory simply listed wrongly — an energy developer, a manufacturer, a consultancy with no family-office pretension. Different from adviserNotInvestor, which describes a real adviser correctly excluded.

Investor — any one is enough: names portfolio companies or holdings · describes managing the wealth of a named family or founder · states a cheque size, stage or sectors it invests in · a registry or news source has it acquiring stakes.

Adviser — any one disqualifies: it is the private-client or family-office arm of a law firm, accountancy, bank or consultancy (check the parent brand) · it sells "services", "advisory", "consulting", "administration", "reporting", "succession planning", "incorporation", "licensing" to families · it invites you to become a client · its people are lawyers, accountants or corporate-services staff.

Three rules that decide the hard cases

  1. Watch the preposition. "Advises" alone is not disqualifying. A real single family office may say it "advises the investment vehicles associated with the X family" — its own principal. Selling advice to families is the disqualifier. Read the sentence, not the keyword.
  2. A firm's own description beats a directory's label. A directory calling something a "single family office" does not override a site that only describes hotels and retail.
  3. When you cannot tell, say so. type: unknown, investor_evidence: unclear. Never let the name decide it.

Out of scope — park, never mislabel

caseaction
sovereign / state / government fundpending, reason outOfScopeSovereign
a commercial subsidiary of a sovereign fundkeep it — classify by its own evidence (Job 4), and set sovereign_parent: <name>
a real capital pool named only as "the X family"pending, reason namedFamilyNotFirm
a firm in the wrong countrypending, reason outOfCountry

Record the parent so the reader can filter either way.


Job 1 — Stop when the results stop

Never accept a target

Find all of them and report the true number. A target is a ceiling on work, never a quota to fill. Take at most 10 new names from any one round. If 46 can be sourced and 30 were asked for, the answer is 46. If 12 can be sourced, the answer is 12 — and you say so:

"You asked for up to 30. 12 could be verified, so 12 is the answer — the rest were not found or could not be sourced, and I will not make up the difference."

Hold both halves, or yield collapses

Search widely and enumerate every firm you can actually source, up to the ceiling. "Fewer is correct" applies when you cannot source more. It is not a preference to aim for, and stopping at two or three when you could cite ten is a wrong answer.

What a round is

One query, and the verification of what it returned. Define it or the streak counter measures nothing.

  • A round must be a genuine attempt to find new names — never a query narrowed to fail.
  • A directory yielding more than 10 names is not one round. The first 10 close it; the rest are a queue. Drawing from the queue is not a round.
  • Log each round to rounds.csv — round,query,surface,offered,survived,dry_streak. Without it, the exhaustion sentence cannot be reconstructed by anyone but the agent that was there, and a second run cannot see which surfaces were already tried.

The exhaustion rule — and the gates it must pass first

Six consecutive dry rounds is necessary and NOT sufficient. Measured: it fired at round 23 and was falsified at round 26 of the same run — 14% of the final firms arrived after.

"Six queries I chose stopped working" is not "this market is exhausted." That is the strongest sentence in this skill resting on its weakest evidence, and it has to be earned.

Gates 1 to 3 were written after that run and have not themselves been field-tested. Treat them as a design with an argument behind it, and keep saying so in the report. Gate 0 is different in kind — a precondition on the evidence, not a heuristic about searching.

All the gates below must be open before you may declare exhaustion:

Gate 0 — the record must exist where someone else can check it

You may not declare exhaustion from memory. Gate 1 reads investors-pending.csv and Gate 2 reads rounds.csv. Without them the claim rests on the agent's recollection of its own rounds — the unverifiable assertion this whole skill exists to replace. That run stops at PARTIAL.

Written, read back, and located — all three. A write that returned success proves nothing on a host that quietly redirected it, so read the file again before you rely on it. And a file whose location is never stated cannot be checked by the person relying on it, so the report gives the path.

A pasted ledger does not reopen this gate. It carries names, which is enough to stop a duplicate; it does not carry a round history anyone can check.

Gate 1 — the pending queue must be empty

You may not declare a market exhausted while investors-pending.csv holds names under a resolvable reason — budget, sourceUnchecked, noHeadquarters, unresolvedJob0. Terminal parks (out of country, sovereign, a family with no vehicle, an individual angel) do not hold the gate: they are answered, just not as rows.

Measured: the rule fired while holding 75 regulator-confirmed names it had told itself not to count — a queue cannot reset the streak.

Drain the queue first. If the budget will not allow it, you did not exhaust the market — you ran out of budget, and that is the report you write.

Gate 2 — the dry rounds must span different surfaces

Six dry web searches are one surface failing six times, not six independent failures. The dry streak must include at least three of these five:

1  general web search
2  a public company or regulator registry
3  an association / membership list
4  news, deal announcements, "led by"
5  a commercial aggregator or directory

The registry surface yielded 85 names on one fetch and the association surface 28 — after web search had gone dry. A surface that has never been tried cannot be dry.

Gate 3 — no round in the streak may rest on a failed fetch

Measured: one garbled fetch that was never recovered protected the streak counter but not the evidence — a mirror at round 26 held two of the three firms that broke the rule.

A garbled or unreadable page must be recovered — retry, a mirror, a cache, a search for the same headline, or any other route to the content — before the round it belongs to may count toward the streak.

"Garbled" means the bytes are wrong, not that the script is unfamiliar. Arabic, Chinese and Cyrillic pages are not garbled. If you cannot recover the content by any route, the round is excluded from the streak rather than counted either way, and the report says how many rounds were excluded.

When every gate is open

Stop after six consecutive dry rounds. Say so, and say what you actually verified:

"Rounds 41–46 produced no new verifiable name, across web search, the commercial register and two association lists, with no names left pending. On those surfaces, this market is exhausted at 46."

(Illustrative. No shipped run has ever legitimately reached this sentence — see the gates above and reference/why.md.)

Name the surfaces. The claim is only ever as wide as the surfaces you tried, and a reader who knows a sixth surface can then tell you so.

A round is dry when it produced zero firms that passed verification — not zero names offered.

situationdry?
names offered, all already held or already returnedyes
round produced ≥1 new verified firmno — streak resets
round named only firms the directory seed already hadno — it did real work
page would not load / answer garbledno, and the round does not count until the content is recovered by another route
round genuinely timed outyes — but count it separately and report it

One judgement can decide the whole streak — in one run a single Job 0 rejection was what made a round dry. When a streak hinges on one call, say which one in the report.

The budget rule

Ask the user for a budget. If they do not set one, use 25 rounds and say that you did. Sixty rounds is the hard backstop whatever the budget says — stop there and report a budget stop, whether or not any gate ever opened. When it runs out:

STOPPED ON BUDGET, NOT ON EXHAUSTION.
This result is PARTIAL. N names were offered and never verified.
Do not read the total as a count of what exists.

A partial run must never be reported in the same shape as a finished one.

Spend the last of the budget verifying, not searching. A shorter solid file beats a longer unverified one.

Search wide before you call it dry

  • Use the language business is actually conducted in — not necessarily the country's. Czech: local offered 60 names to English's 12 — but only 6 survived against English's 7. UAE: English 73 names to Arabic's 1, and the one was an adviser. The local language reliably finds more names; it does not reliably find more firms. The test: open two or three firms you already hold from that market and see what language they publish in. One look, no round spent. Caution: if those firms came from English directories the sample is circular. Also check a local business registry or a local news source.
  • Legal and colloquial forms — "single family office", "multi family office", "investment office", "private office", "investment group", "holding", "conglomerate".
  • City by city. A country query saturates long before the cities do.
  • Adjacent surfaces — association and membership lists, conference attendee and sponsor pages, deal announcements ("… led by …"), business registries, "our investors" pages.

What "the family offices in X" means — settle this before round 1

A request naming one type is a filter over a broad search, never a narrow search.

  • Search the whole investor population of that market — family offices, investment groups, private equity, VCs, angels.
  • Ship every firm you verified, with its true type.
  • Mark matches_request: yes on the ones that match the named type and no on the rest, so the asker can filter to exactly what they asked for in one click.
  • Report both numbers: "Portugal has 61 investors; 24 are family offices."

And the contradiction rule (Job 4) does NOT fire on this. A VC found under a "family offices" request is not a contradiction — it is the broad search working. The rule only ever applies to a type you inferred that disagrees with the request, never to one a page states. Leave type_blanked_reason empty.

Filtered requests — search wide, filter after. Never search narrow.

"Family offices in Austria that invest in AI" — do not search for that. A narrow query goes dry early for the wrong reason: most firms never put "AI" on a page that ranks.

phase 1   exhaust the BROAD set        → 61 firms, six dry rounds
phase 2   filter what you already hold → 20 match · 9 unknown

Only phase 1 runs the exhaustion rule. Answer in both numbers, never the filtered one alone: "Austria has 61 family offices. 20 invest in AI. 9 do not say." Keep the whole broad set — the next filtered question is then free.


Job 2 — The bar

Same bar for a name you found and a name read off a directory.

#checkreject asfatal
0asells services rather than investingadviserNotInvestoryes → rejected file
0bnot an investment entity at allnotAnInvestoryes → rejected file
1ano firm name, but a real capital poolnamedFamilyNotFirmno → pending
1ba named individual angel, no vehicleindividualNotFirmno → pending
2already returned this runalreadyReturnedyes — drop silently, it is already on file
3already in investors.csvalreadyHeldyes — drop silently, it is already on file
4the cited page does not existsourceDeadyes
4bthe page exists but could not be fetchedsourceUncheckedno — keep the row
5headquarters cannot be establishednoHeadquartersno → pending
6wrong country / sovereign parentoutOfCountry / outOfScopeSovereignno → pending

Before anything else in this list: is this an organisation, or a vehicle one runs? A fund is not a row; its manager is. See Manager or fund.

4 vs 4b — this decides half your rejections

what happenedverdict
404 · 410 · domain does not resolvesourceDead — fatal
robots.txt disallowssourceUnchecked — keep
paywall · login wall · cache-only domainsourceUnchecked — keep
a redirect your fetcher would not followsourceUnchecked — keep
your own tooling refused the URLsourceUnchecked — keep, and say so

A sourceUnchecked row is honest and useful; a missing row is neither. But a firm whose existence rests on a single unreachable page is not enough — find a second source, or send it to investors-rejected.csv as unsourceable, and re-offer it on a later run, because that code describes a page, not the firm.

If your tools cannot tell these apart, default to sourceUnchecked, and say so in the report. Many agent fetch tools return one undifferentiated error. A rule you cannot execute must fail towards keeping the row, not towards deleting it — this check decides half your rejections.

Recovering a refused URL is worth it. Searching the URL into scope first costs roughly 2.4 tool calls per name, measured on one market — and recovers most of them.

Source rungs — record which one you used

1  the firm's own site — imprint, Impressum, contact, legal page
2  a public company registry
3  a reputable news or association page
4  a commercial aggregator or lead-gen directory   ← never alone for existence

Headquarters — the fifth check, in detail

Two headquarters is a real answer, and the column holds one. Where a firm presents two equally — "based in X and Y" — record the one it lists first, name the second in the report, and never park the row: a firm with two offices still has a verifiable one. If the second is in another country, say so, because the market question is then on the row's face.

City is the target; country alone sets headquarters_partial. Never reject for a missing city when the country is established.

No confidence scores. Provenance instead.

source fetched · source NOT fetched · NOT in the source · no source · you told me

The exclude list

In: firms that passed · firms rejected as alreadyHeld · firms rejected as adviserNotInvestor or notAnInvestor — both describe the firm, not the page, and will not change on a later round.

Out — deliberately: sourceUnchecked · noHeadquarters · sourceDead · unsourceable. All four describe one page on one day. Record their names anyway.

Ask and enforce — state exclusions in the next query and check returned names yourself.

Never merge the counts. "20 already known or already returned" welds two failures with opposite fixes. Report separate lines.

Do not self-censor. Never skip a firm for being well known. The check decides, not you.

A published list as a first round — not free

Read it before round 1, but: every name goes into the exclude list · the seed must not answer the dry test · one seed name rescues one round, once, and the whole seed rescues at most three · a directory failing never stops the search.

Resolve a named list before you search wider

A list of N named firms is N decisions already waiting. Spend the budget on those before spending it looking for more names, because Job 0 cannot run on a name — it needs the firm's own page, and an unresolved name helps nobody.

Measured: an association list gave 31 members in one round. The run then went looking for more names and reached the budget with 16 of those 31 still undecided — including several whose own pages would have settled Job 0 in one fetch each. One rejection was recorded where a dozen were sitting there unread.

This is not the same as trusting the list. It still does not answer the dry test, and its members still face every gate. It only says: decide what you already hold first.

If the budget will not cover the list, say how many were left undecided, by name, in investors-pending.csv under unresolvedJob0. A number that large is itself the finding.

Manager or fund — the row is the decision-maker

An investor often appears as several legal entities: a management company and the funds it runs, each separately registered and separately named.

One row is the entity that decides where the money goes — the manager. The funds it operates are not separate rows, however many there are and however distinctly they are named. Record the manager's name, and put a fund name in formerly only if the manager was renamed, never to represent a fund.

One firm in one market ran three separately named funds. Three rows would have been three false positives in a list whose whole purpose is that one row means one organisation.

This is the fold table's fund row, stated in full. A manager's own site naming its funds satisfies "a page says one is part of the other" word for word, so read literally the table would have you keep all four rows with part_of set. It does not: a fund is a vehicle, and part_of is for organisations.

The exception is where the fund and the manager are in different countries. A locally registered fund run by a manager headquartered abroad is a real case and the market question decides it: the row is the manager, and if the manager is out of country it is outOfCountry — whatever the fund's registration says. State which you applied, because this one is genuinely arguable and a reader may want the other answer.


Job 3 — One firm or two

Fold to compare. Keep the best-looking form to display. Lists live in reference/lists.md.

The pipeline, in this order

  1. Decompose Unicode, strip the marks — é→e, ö→o, ü→u. Transliterate, never drop: dropping turns André into andr, which matches nothing.
  2. Spell out — ß→ss, ø→o, æ→ae.
  3. Lowercase.
  4. Strip a leading article — al · el · the · la · le · los · il · de · van · von. Leading token only, never inside the name.
  5. Strip legal forms from BOTH ends, repeatedly, dots and spaces tolerated — s.r.o., s r o, sro and S.R.O. are one entry. Extend reference/lists.md with this market's forms before round 1 — most field tests skipped that, and the fold failed on local names. ⚠ Leading forms are not rare. Baltic and Nordic registries write AS Vesta while the firm writes Vesta; the same list holds Vesta Capital AS. An end-anchored strip folds one and not the other. ⚠ This step runs BEFORE punctuation collapse. The other order turns a.s. into a s and it never matches as. ⚠ Jurisdictions are not legal forms — DIFC, ADGM, IFSC distinguish real firms.
  6. Collapse remaining non-alphanumerics to single spaces, trim.
  7. Strip trailing region words — asia, europe, mena, emea, international, global (reference/lists.md §3). Type abbreviations (vc, pe, fo, mfo) are generic tails and are handled in step 8, under its distinctiveness guard.
  8. Strip a generic tail — only if what remains is still distinctive.

"Distinctive" — the most dangerous test in the skill

What remains is distinctive only if it contains a token that is 4+ characters, not on the legal-form or generic-tail lists, and not a place.

A place name is never distinctive. <City> Capital Group (a private investment group) and <City> Investment Office (a government FDI agency) both reduce to the city name alone. <City> + <generic financial noun> is the modal naming convention across the Gulf and common in Asia and Latin America.

If you cannot decide whether a token is a place, treat it as a place and do not fold. Measured: five unrelated organisations folded to a single such token in one run. Refusing to fold costs a flag; folding costs the truth.

Folding proposes. It never merges on its own.

same fold key, and…action
same registrable domain, and no page says one is part of the othermerge
same domain, but a page says one is part of the otherkeep both — set part_of. A conglomerate and its family office share a site
same domain, but one is a fund the other managesone row, the manager. Not part_of — a fund is a vehicle, not an organisation. See Manager or fund, at the end of Job 2
different domainskeep both, set fold_conflict
no domain on eitherkeep both, set fold_conflict

Caught a five-way collision across four domains on one real run — the failure that, one version earlier, silently wrote five organisations as one row.

Duplicate rows are visible and a reader fixes them. One row for two companies is invisible.

A high flag rate is the rule working, not failing (why.md).

Typos — narrow, and country-guarded

Same firm if: folded names identical · one adjacent transposition in a token · one inserted/dropped character in a token of 6+ characters.

A substituted letter is not a typo — Bauer/Baier are different families. Except in transliteration: for names carried from another script, vowel substitution is a typo (Nasiri/Nassiri/Nassery = one firm — invented, like every organisation name in this file); consonants must still match.

Never apply the typo rule across countries — only where the domain or headquarters agrees.

Rebrands are invisible to every rule above

A firm that changed its name is not a typo, not a spelling variant and not a fold — the two strings have nothing in common. Three turned up in one small market. No normalisation will ever catch them.

The only things that do:

  • the domain. A rebrand almost always keeps or redirects the old one. If two names resolve to the same registrable domain, that is a rebrand, not a coincidence.
  • the sentence. Pages say "formerly X", "rebranded from X", "previously known as". Read it and record it.

Set formerly on the row, keep the old name searchable, and put the old name in the exclude list — otherwise it comes back every round as a name you have never seen.

Never fold on

City · sector · investor type · a shared person. Two firms in Hamburg investing in software are two firms. A shared surname is a family, not a merger.

When two rows do merge

Longest sourced value per field · all source rows kept · newest checked per field.

People — stricter, because there is no domain to settle it

  • Strip a parenthetical before folding — X (CFA) → X.
  • Strip leading titles and trailing suffixes, repeatedly (reference/lists.md).
  • Same person only if folded names are identical, or first and last token match exactly and every token between is a single initial on both sides. Erika Mustermann = Erika M. Mustermann; ≠ Erika Maria Mustermann.
  • A single-token name never matches anything but itself. A first name is a hint to ask about, never an identity to write against.
  • A substituted letter is a different person. Jan/Jon, Eric/Erik stay separate.

Job 4 — Classify from evidence

family_office · investment_group · private_equity · venture_capital · angel · unknown

Plus sovereign, which is a marker, not a type — those go to pending.

Two different questions, two different answers:

  • "What did this text mean?" — read loosely.
  • "May that be written?" — read strictly. Every word must be owned by the type it claims (reference/lists.md). An allowlist, not a denylist.
"venture capital fund"   all owned                  → write
"investiční skupina"     investiční, skupina owned  → investment_group
"crypto fund"            crypto not owned           → refuse
"venture studio"         studio not owned           → refuse

The allowlist governs spelling, never identity. It cannot tell you a firm is a family office — Job 0 does that, and Job 0 runs first. Otherwise a consultancy called … FAMILY OFFICE types cleanly, because family and office are owned words.

unknown is a real answer and appears in the output

Treat any expected proportion of unknown as unknown — the rate varies enormously by market. Never report it as a quality signal. Forcing a label onto an unknown is the same mistake as inventing an email.

A name is weaker evidence than a statement

Only when no page states a type may you read the firm's own name, and only narrowly:

family office · familienbüro · familienunternehmen · rodinná kancelář → family_office
ventures · venture capital · standalone "VC"                         → venture_capital

Capital, Invest, Holding, Group, Gruppe, AG, GmbH resolve to nothing from a name alone.

⚠ Outside the market this rule was measured in, prefer unknown. It typed one individual's holding vehicle as venture_capital off the word Ventures. Whenever the type came from the name, say so in provenance.

Contradiction — only ever blanks an INFERRED type

the type is…and it contradicts the requestdo
stated by a page you fetchedkeep it, keep the source, set matches_request: no
inferred by youblank it, record type_blanked_reason
unknownnothing — it is already the empty answer

Compare against the request, and only the request — never against prose, which fails on negation ("ist kein klassischer VC-Investor") and on parts (a venture arm inside a family office).

A request naming no type contradicts nothing. One naming the asserted type among others contradicts nothing either — ambiguity is not a contradiction.

Kept only for a user who asks narrowly anyway — Job 1 tells you never to issue such a request. Do not go looking for work for it.


Job 5 — Come back for what you missed

round 1    name, country                      ← a directory listing
round 8    + website, headquarters, sectors   ← the firm's own site
round 22   + person, role, LinkedIn           ← its team page

Fill the same row. Never create a second row for the same firm.

  • A field you cannot source stays empty. No email inferred from a name and a domain. No partner guessed because the firm is small. No city from a phone code. An empty cell is a known gap; a guessed cell is a lie inside a file someone will act on.
  • Refuse, do not coerce. A bare domain is recorded as given or left out — never given an invented https://. A national phone number stays national.
  • A field the user asserted is never researched again. Permanently.
  • Two values of the same kind is a question, not an overwrite. Keep both, mark the newer current, say a previous value exists.

Output — six files, and how they reach the user

Where writing works: the six files below, in investor-search/<market>/, plus the investor-search/index.md that lets a later run find them.

Where it does not: print investors.csv and sources.csv into the chat as fenced CSV blocks, and print the ledger. investors-rejected.csv becomes a count and nothing else — printing a list of named firms carrying a negative claim is what that file's own rule forbids. investors-pending.csv and rounds.csv are lost, and Gate 0 is therefore shut.

Handing it over — three steps, and nobody leaves empty-handed:

1  write the file, and say where it went               always
2  attach it, if this host can attach a file          ask first — it sends a message
3  otherwise print the CSV into the chat              so it can at least be copied

Step 2 is not available everywhere and must never be assumed. Do not silently skip to step 3 either: say which one you did.

investors.csv — UTF-8 with BOM

columnmeaning
idstable row id — joins to sources.csv
namebest-looking form
typethe six values above
investor_evidenceinvests · operating_group · syndicate · unclear
matches_requestyes · no · blank when undeterminable
websiteregistrable domain as written
headquarterscity, country
headquarters_partialyes when only the country is established
personthe most senior individual you actually saw
roletheir title as the page states it
people_knownhow many named people you saw — one row is one firm, not one person
linkedinprofile or company URL
sectorsas the firm describes them
fold_conflictthe other name this row's key collided with
formerlythe firm's previous name, where a page states one
type_blanked_reasoncontradicts_request — the only value; empty otherwise
part_ofthe parent, where a page stated one
sovereign_parentthe sovereign fund above it, where there is one
key_quoteone sentence that evidences this firm is what the row says it is
key_quote_urlthe page that sentence is on
checkedISO date
provenanceone of the five labels

Illustrative — invented firms, to show the shape, not a run's output:

id,name,type,investor_evidence,matches_request,website,headquarters,headquarters_partial,...
f-001,Nadira Capital Partners,family_office,invests,yes,nadiracapital.example,"Lisbon, Portugal",,
f-002,Orsett Group,unknown,operating_group,,orsett.example,Portugal,yes,

The request named a type, so f-001 — a match — carries yes. f-002 shipped as unknown, so whether it matches the named type could not be determined, which is the one case the column's blank is for; a stated type that simply differs would carry no. Its investor_evidence is still operating_group — you can see that a firm runs its own businesses and still be unable to type it. f-001 was sourced to a city, f-002 only to a country, hence headquarters_partial. Neither row would exist without a sources.csv row per filled field.

key_quote must evidence the row, not merely come from the page. A sentence defining what a family office is in general evidences nothing, and a reader scanning the file reads a native-language quote as proof. No evidencing sentence → leave it empty and say so. "Original language" means the language of the page you read, not of the country.

sources.csv — one row per citation

id,field,rung,url
f-018,website,1,https://example.com/about
f-018,headquarters,2,https://registry.example/entry?id=7&x=1

A field may carry citations at more than one rung — that is good, not a problem. It does mean "rows resting on a rung-4 source alone" is a NOT EXISTS question (no citation at rung 1–3 for that field), never WHERE rung = 4.

A separate file, not a packed cell. Every in-cell format tried broke (why.md), and one row per citation carries the rung as a real column.

investors-pending.csv — offered, not resolved

name,reason,note,first_seen,source_url
reasonmeaningresolvable?
budgetnever got to ityes
sourceUncheckedpage exists, could not be fetchedyes
noHeadquartersHQ not established yet — two headquarters is not this, see Job 2yes
unresolvedJob0Job 0 could not be decided even as unclear — you never found enough of the firm to judgeyes
outOfCountryreal, wrong market — including a local fund whose manager is abroad; say which in noteno — terminal
outOfScopeSovereignsovereign or state parentno — terminal
namedFamilyNotFirma capital pool named only as "the X family"no — terminal unless a vehicle name turns up
individualNotFirma named angel with no vehicle — a real investor, but one row is one firmno — terminal

individualNotFirm is not a rejection. Individual angels are legitimate targets; they simply do not fit a row that means "one firm". Keep them here with their source so the reader can take them.

Only the resolvable reasons hold Gate 1 shut.

investors-rejected.csv — local working file, never published

name,reason,evidence,checked,source_url

adviserNotInvestor · notAnInvestor · sourceDead · unsourceable.

Two of these four are permanent and two are not. adviserNotInvestor and notAnInvestor describe the firm, so never search them again. sourceDead and unsourceable describe one page on one day — re-offer them on a later run, because a different route may source them properly. They are in this file so you can see the call was made, not to suppress the name forever.

Keep the names, keep the file local. Shipping a result set to a client means investors.csv plus a count of rejections, never the list.

rounds.csv — what was tried

round,query,surface,offered,survived,dry_streak

rounds.csv is Gate 2's entire input, and it tells the next run which surfaces were already tried — so you can start with one it did not.

ledger.txt — the compact memory

Written at the end of every run, beside the others where files work, and printed in its own fenced block where they do not — after the CSV, because the user copies only this.

One job: stop the next run rediscovering what this one already decided. Four fields per firm — fold key, domain, decision, and a display name only for firms that were kept or are still open; rejected and adviser rows travel as key and domain, never as a name.

It never speaks about completeness. No round counts, no dry-surface tallies, nothing that could license the exhaustion sentence — see Gate 0.

Format, what may be trusted in a pasted one, the rule for scope changes, and how to print a long CSV into a chat in numbered parts: reference/environment.md.

Report when you finish

<MARKET> · 33 firms over 10 rounds + 4 verification passes
STOPPED ON BUDGET — PARTIAL, not an exhaustion count
written      /home/u/work/investor-search/poland/ · 6 files · index.md updated
surfaces     web search · commercial register · 2 association lists
             NOT tried: news/deal announcements

offered      99 names — 33 survived
  in English 89 offered · 33 survived (37%)
  in <local>  10 offered ·  0 survived (0%)

already held 14 names already on file — dropped
already ret'd 6 names this run had already returned — dropped
rejected     13 advisers, not investors
              4 not investors · 2 source dead · 1 unsourceable
             26 parked, unresolved
             (33 + 14 + 6 + 13 + 4 + 2 + 1 + 26 = 99 — every offered name has one home)

type         5 family_office · 9 investment_group · 10 venture_capital
             7 private_equity · 2 angel · 0 unknown
evidence     24 invests · 7 operating_group · 0 syndicate · 2 unclear
sources      173 citations · rung 1: 160 · rung 4: 9
quotes       25 of 33 rows carry an evidencing quote; 8 correctly have none
flags        7 fold_conflict · 8 rows whose HQ rests on a rung-4 source alone

(Shape only. The figures are illustrative — no shipped run has legitimately reached the
exhaustion sentence.)

Every offered name lands on exactly one line, and the lines sum to the offered count. Not a slogan — a check. A kept row counts as survived even where one of its fields rests on sourceUnchecked; the parked line counts only names that never became rows. A name that was offered and is not on any line was dropped without a decision, which is the bucket with a hole. The two dedupe lines stay separate: "20 already known or already returned" welds two failures with opposite fixes, and a run past round 1 always has some of both. Rejections are broken out by reason for the same reason — sourceDead and unsourceable describe a page and may be re-offered later; adviserNotInvestor and notAnInvestor describe the firm and never will.

Split the offered count by language — that line is what caught the language rule being wrong. Report every flag. fold_conflict, operating_group, unclear and rung-4-only rows are exactly what a reader must look at.

Where nothing could be written, the same report takes the same shape, with two lines replaced and no path:

NO FILES WRITTEN — chat output only. PARTIAL; exhaustion cannot be claimed here.
written      nothing — chat only; exhaustion cannot be claimed

The counts are reported exactly as they would be on disk. Unresolved names are listed in the ledger below as pending, not carried in a file.

The written line is not decoration — it is the only place the user learns where their files actually are, and whether the completeness claim was available at all.

Say how you knew it was finished — or say plainly that you did not.


What this skill will not do

  • No email addresses. Inventing them is the exact failure this exists to prevent.
  • No ranking, scoring or recommendation, and no investment advice.
  • No firms with no web presence. Some single family offices are deliberately invisible.
  • No claim to be current beyond the checked date on each row.
  • No judgement it cannot show you. Every rejection is in a file with its reason.

Top skills in this category