perplexed

Exa retrieves, Perplexity writes — and NATS stops being an air traffic authority

A retrieval stage runs before the model does, handing it verified company facts instead of hoping a web search lands on the right company.

Why Care?

Perplexity writes a genuinely good company profile. What it is bad at is finding the right company.

Ask it about NATS — the messaging system — and it will happily cite the UK's National Air Traffic Services. Ask it about Kestra and you get ten citations to Kestra Medical Technologies and Kestra Financial, a medical device maker and a financial advisory firm. The prose is excellent. The company is wrong.

So we stopped asking one model to do both jobs. Exa retrieves. Perplexity writes. Exa runs first, finds the actual entity, pulls structured facts about it, and hands them to Perplexity as named sources. Perplexity keeps doing the thing it is already good at, on top of inputs it can trust.

The difference on a real note: where the old MindStudio profile said "No reliable, source-backed public information was found" three separate times — about founding date, founders, and funding — the new pipeline returns founding year, headcount, funding totals with round-level detail, and named customers.

What's New?

  • include-sources: — a new key in the cft fence that declares a retrieval stage. Exa is the first provider; the resolver is shaped so vault globs and Chroma can land in the same slot later.

  • pin: entity / pin: none — the precision knob. entity restricts the search to the target's own domain, for profiling one company. none lets similarity search run wide, which returns competitors — exactly what a market map wants.

  • A separate [^E1] citation namespace and a ## Sources (retrieved) footer, so retrieved sources never collide with the model's own web-search numbering.

  • Master switch plus a per-run toggle. Settings → Exa retrieval turns the whole stage off; the run dialog offers "Retrieve sources first (Exa)" when a template declares the key.

  • Degrade, never block. No key, rate limited, Exa down — the run proceeds Perplexity-only with a notice. A forty-minute deep-research run is never aborted because a retrieval provider had a bad minute.

How it Works

Retrieval happens before the first write to the target file. That ordering is deliberate: everything that can fail should fail while the note is still intact.

YAML
include-sources:
  - provider: exa
    query: "{{basename}} {{url}} overview product funding customers"
    category: company
    num-results: 6
    pin: entity
    contents:
      summary:
        query: "What does this do, founded when, funding raised, customers?"
        schema:
          type: object
          properties:
            founded_year: { type: string }
            funding: { type: string }
            notable_customers: { type: array, items: { type: string } }

What lands in the prompt:

MARKDOWN
## Retrieved sources

### [^E1] Synadia — https://nats.io/

```yaml
founded_year: 2017
funding: USD 41,200,000 total funding across multiple rounds (Seed, Venture, Series B)
notable_customers: [NVIDIA, Mastercard, Rivian, Replit]
workforce: { total: 44 }

That funding line is the one we were least sure Exa could deliver. It can.

## Under the Hood

Four things turned out to be load-bearing, and every one of them was found by
running the thing rather than reviewing it.

### Two providers in one prompt breaks the citation contract

Perplexity numbers its citations `[1]`, `[2]` against **its own** search-results
array. Sources spliced in from Exa are not in that array. A model citing a
retrieved source as `[3]` therefore footnotes the claim to whatever Perplexity's
third search result happened to be — and the note passes every check we run. It
has inline markers, it has a populated footer, every marker resolves. It is
simply attributing facts to the wrong documents.

Hence the `[^E1]` namespace, a dedicated preamble that enforces it, and two
footer sections. This is a correctness boundary wearing a formatting costume.

### `includeDomains` is a substring match, not a host match

The one that nearly shipped silently. Pinning a Cresta profile to `cresta.com`
returned six companies:

| Host | Actually is |
|---|---|
| `cresta.com` | the target |
| `alcresta.com` | Alcresta Therapeutics, a pharma company |
| `codecresta.com` | a dev shop |
| `ridgecresta.com` | a training company |
| `grupocresta.com.gt` | a Guatemalan group |
| `cresta.com.au` | an unrelated Australian firm |

Every one of them *ends with* the pinned string. Exa filters by substring, so a
short domain pins nothing — it widens.

Our first spike used NATS, which has no such namesakes, so it passed and
reported pinning as solved. **A pin that works on one entity proves nothing
about the mechanism.** Pinning is now enforced on the results with an explicit
host check: the hostname must equal the pin or be a true subdomain. And because
the model cites the impostors, the failure made notes look *better* sourced
while being wrong about the company.

### A `type: string` schema field returns the word "null"

A schema field declared `type: string` cannot hold JSON null, so when Exa has no
value the model writes the word instead — `"funding": "null"`. Non-deterministic
on the same query, too: one call returned a real JSON null, the next the string.
Rendered unstripped, `funding: null` reads to the writing model as an asserted
fact about the company. Now filtered through a sentinel set, with `Undisclosed`
and `Bootstrapped` deliberately excluded because those are real answers.

### Markers must be numbered after truncation, not during

The context budget fills greedily, so it keeps packing smaller sources in after
a large one has been skipped. Numbering assigned during retrieval came out as
`E1, E3, E4` with a hole where `E2` was dropped. The prompt then instructs the
model to cite `[^E2]`, nothing matches, and a model that helpfully renumbers
`E3` down to `E2` repoints every citation. Contiguity is part of the citation
contract, not tidiness.

### On cost, since everyone asks

$7 per 1,000 searches, $1 per 1,000 pages per content type. A typical profile
run is about **$0.023**. The free tier is $20 of credits plus $10 a month. We
deliberately built no cost-guarding machinery — the real ceiling is the
*context* budget, because past roughly 12,000 characters of pasted source text
the model's attention drifts off the section outline and onto the sources. That
failure looks exactly like a healthy run that quietly ignored its instructions.

## What's Next

- Feed the entity pin into Perplexity's own `search_domain_filter`, so the
  wrong-Kestra pollution dies at the source rather than being outvoted.
- An aggregator denylist for `pin: none`, which currently returns bare
  `github.com` and `wikipedia.org` homepages alongside the real competitors.
- Vault-glob and Chroma providers in the same `include-sources:` slot.

Full findings, including the ones we decided against, live in
[[2026-09-10_Exa-Retrieval-Stage-and-Include-Sources]].