v1 — Live · 6 endpoints · MCP & Custom GPT ready

The investment memo API.

Compounding fund knowledge into 30-page IC memos in 8 minutes. Drop into Make, n8n, ChatGPT, Claude Desktop, Cursor or any HTTP client. Your fund_thesis stays at home — only verdicts ship.

6
Endpoints
~8 min
Detailed memo
MCP
Native
trigger-memo.sh
curl -X POST https://alexandre-79537--sourcing-agent-fastapi-app.modal.run/v1/memos \
  -H "Authorization: Bearer $PROPLACE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "fund_type": "s4bt",
    "company": { "name": "Thrust Carbon" },
    "memo_type": "detailed"
  }'

# 202 Accepted
{
  "slug": "thrust-carbon-v2",
  "url": "https://analysis.proplace.co/thrust-carbon-v2",
  "status": "queued",
  "estimated_seconds": 540,
  "poll_url": "/v1/memos/thrust-carbon-v2"
}

↳ Memo published at analysis.proplace.co/<slug> when ready.

Quickstart — 60 seconds to your first memo

Four steps. You'll have an institutional-grade investment memo at analysis.proplace.co/<slug> in roughly 8 minutes.

  1. 1
    Get an API token

    Email alexandre@proplace.co. We provision a personal token + onboard your fund_type in Airtable.

  2. 2
    List the funds you can target

    Confirm your fund_type is live:

    curl -H "Authorization: Bearer $PROPLACE_TOKEN" \
      https://alexandre-79537--sourcing-agent-fastapi-app.modal.run/v1/funds
  3. 3
    Trigger a memo
    curl -X POST https://alexandre-79537--sourcing-agent-fastapi-app.modal.run/v1/memos \
      -H "Authorization: Bearer $PROPLACE_TOKEN" \
      -H "Content-Type: application/json" \
      -H "Idempotency-Key: $(uuidgen)" \
      -d '{
        "fund_type": "s4bt",
        "company": { "name": "Thrust Carbon", "website": "https://thrustcarbon.com" },
        "memo_type": "detailed"
      }'
    import os, uuid, httpx
    
    resp = httpx.post(
        "https://alexandre-79537--sourcing-agent-fastapi-app.modal.run/v1/memos",
        headers={
            "Authorization": f"Bearer {os.environ['PROPLACE_TOKEN']}",
            "Idempotency-Key": str(uuid.uuid4()),
        },
        json={
            "fund_type": "s4bt",
            "company": {"name": "Thrust Carbon", "website": "https://thrustcarbon.com"},
            "memo_type": "detailed",
        },
        timeout=30,
    )
    print(resp.json())  # {"slug": "thrust-carbon-v2", "poll_url": "/v1/memos/thrust-carbon-v2", ...}
    import { randomUUID } from "node:crypto";
    
    const r = await fetch("https://alexandre-79537--sourcing-agent-fastapi-app.modal.run/v1/memos", {
      method: "POST",
      headers: {
        "Authorization": `Bearer ${process.env.PROPLACE_TOKEN}`,
        "Content-Type": "application/json",
        "Idempotency-Key": randomUUID(),
      },
      body: JSON.stringify({
        fund_type: "s4bt",
        company: { name: "Thrust Carbon", website: "https://thrustcarbon.com" },
        memo_type: "detailed",
      }),
    });
    console.log(await r.json());
  4. 4
    Poll, then read your memo

    Poll every 30 seconds — when status flips to ready, the memo is published at url.

    curl -H "Authorization: Bearer $PROPLACE_TOKEN" \
      https://alexandre-79537--sourcing-agent-fastapi-app.modal.run/v1/memos/thrust-carbon-v2
    
    # {"status": "ready", "url": "https://analysis.proplace.co/thrust-carbon-v2", "verdict": "CONSIDER", ...}
Pre-flight first with /v1/screen

Get a verdict + thesis-fit score in 10 seconds before committing 8 minutes to a full memo.

Concepts

Four objects you'll encounter. Worth 60 seconds.

fund_type — the compounding key

Every memo is written through the lens of a specific fund. The fund_type identifier resolves (server-side) to the full fund thesis, sector profile, return hurdles, M&A acquirer context, and language preference. You never send the thesis itself — your IP stays at home. Discover available fund_types with GET /v1/funds.

memo — the artifact

A memo is a 30-page institutional investment dossier (market sizing, value chain, competitive positioning, financial model, conviction, risk register, deal rationale). It runs three flavors:

  • fast~3 min · executive summary + verdict only
  • quick~5 min · core analysis without value-chain deep dive
  • detailed~8 min · full pipeline (recommended)

slug — the URL key

The slug is derived deterministically from the company name (NFKD normalize → lowercase → kebab-case). A company called "Thrust Carbon" becomes thrust-carbon-v2, published at analysis.proplace.co/thrust-carbon-v2. The same company name will always produce the same slug — re-triggering overwrites the memo.

verdict — the decision

The synthetic GP returns one of: MUST_CALL, CONSIDER, MONITOR, PASS, HARD_BLOCK. The verdict is consistent across /screen (10s pre-flight) and the full memo's /memos/{slug} response.

Authentication

Every endpoint requires a Bearer token in the Authorization header.

Authorization: Bearer pk_live_3f4a...e9c2

Tokens are scoped to your fund(s) and rate-limited per token. Rotation: email us — we issue a new token, revoke the old one.

Token best practices

  • Never commit tokens. Use environment variables or your secret manager.
  • One token per integration. Easier to rotate when you offboard a service.
  • Use Idempotency-Key on POST requests. Safe to retry without duplicating memos. See error handling.

API reference

Six endpoints. All under https://alexandre-79537--sourcing-agent-fastapi-app.modal.run/v1. Full machine-readable spec: openapi.json.

GET /v1/funds

List every fund_type the caller can target.

operationId: listFunds
Show example ↓
[
  {
    "fund_type": "s4bt",
    "display_name": "Solutions for Business Travel",
    "lang": "en",
    "supports_synergy_model": true,
    "logo_url": "https://..."
  }
]
GET /v1/funds/{fund_type}

Fetch a fund's metadata + a redacted summary of its thesis (sectors, stage, ticket size). Full thesis stays internal.

operationId: getFund
POST /v1/screen

10-second pre-flight verdict. Returns verdict, score, estimated ARR. Use before committing to a full memo.

operationId: screenDeal
Show request/response ↓
Request
{
  "fund_type": "s4bt",
  "company": {
    "name": "Thrust Carbon",
    "website": "https://thrustcarbon.com"
  }
}
Response
{
  "fund_type": "s4bt",
  "company_name": "Thrust Carbon",
  "verdict": "CONSIDER",
  "score": 75.0,
  "arr_mid_eur": 1000000.0,
  "next_step": "Call POST /v1/memos for full memo"
}
POST /v1/memos 202 async

Trigger the full memo pipeline. Returns immediately with a slug + poll_url.

operationId: triggerMemo
Show request/response ↓
Request body — minimum
{
  "fund_type": "s4bt",
  "company": { "name": "Thrust Carbon" },
  "memo_type": "detailed"
}
Response (202)
{
  "memo_id": "thrust-carbon-v2",
  "slug": "thrust-carbon-v2",
  "url": "https://analysis.proplace.co/thrust-carbon-v2",
  "status": "queued",
  "fund_type": "s4bt",
  "memo_type": "detailed",
  "estimated_seconds": 540,
  "poll_url": "/v1/memos/thrust-carbon-v2"
}
Request body — all supported fields
{
  "fund_type": "s4bt",
  "company": {
    "name": "Thrust Carbon",
    "website": "https://thrustcarbon.com",
    "linkedin": "https://www.linkedin.com/in/kit-aspen",
    "tagline": "Carbon accounting for travel managers",
    "country": "United Kingdom",
    "company_stage": "seed",
    "founders_cofounders": "Kit Aspen (CEO, ex-GoCardless), ...",
    "funding": "€4M",
    "funding_date": "2024-03-15",
    "vc_funds": "Seedcamp, Octopus Ventures",
    "pitch_deck_url": "https://drive.google.com/...",
    "product_url": "https://thrustcarbon.com/product",
    "pricing_url": "https://thrustcarbon.com/pricing",
    "team_url": "https://thrustcarbon.com/team",
    "first_name": "Kit",
    "last_name": "Aspen"
  },
  "memo_type": "detailed",
  "lang": "en",
  "synergies_with": "rec123abc",
  "publish": "yes",
  "motive": "Strategic bolt-on for CSRD compliance moat",
  "source": "linkedin",
  "callback_url": "https://yourapp.example/proplace-callback"
}
Field reference
FieldTypeRequiredNotes
fund_typestringYesFrom GET /v1/funds
company.namestringYesDrives the slug
company.websitestringRecommended for accurate research
company.linkedinstringCEO/founder personal LinkedIn URL — format https://www.linkedin.com/in/firstname-lastname (NOT the company page)
company.taglinestringOne-line value prop
company.countrystringHQ country, full name
company.company_stagestringseed / series_a / series_b…
company.founders_cofoundersstringFree text on the founding team
company.fundingstringLatest round (e.g. €4M)
company.funding_datestringDate or free text
company.vc_fundsstringExisting investors (comma-separated)
company.pitch_deck_urlstringPublic PDF URL
company.product_url / pricing_url / team_urlstringOverride deep-research targets
company.first_name / last_namestringCEO name hint
memo_typeenumfast · quick · detailed (default)
langstringDefaults to fund.lang
synergies_withstringAirtable record_id of a portfolio company
publishstringSet to "yes" for the Open Deal Flow
motivestringStrategic motive (free text)
sourcestringWhere the deal came from (twitter, linkedin, inbound…)
callback_urlstringExperimental — POST'd when the memo is ready

⚡ Pass Idempotency-Key header (any UUID) to safely retry without duplicating work.

GET /v1/memos/{slug}

Poll memo status. Returns queuedbuildingready (or failed).

operationId: getMemo
Show example ↓
{
  "slug": "thrust-carbon-v2",
  "url": "https://analysis.proplace.co/thrust-carbon-v2",
  "status": "ready",
  "fund_type": "s4bt",
  "company_name": "Thrust Carbon",
  "memo_type": "detailed",
  "verdict": "CONSIDER",
  "summary": "Strong CSRD compliance moat..."
}
POST /v1/memos/{slug}/patch

Surgically edit a published memo. 5 operation types: replace_section, replace_section_inner, append_to_section, prepend_to_section, replace_text.

operationId: patchMemo
Show example ↓
{
  "operations": [
    {
      "type": "replace_section_inner",
      "section_id": "conviction",
      "html": "

Conviction

New analysis...

" }, { "type": "replace_text", "find": "ARR €1M", "replace": "ARR €1.5M" } ], "dry_run": false, "commit_message": "Update conviction post-call" }

Set dry_run: true to preview the diff without pushing.

Integration recipes

Ten ways to plug Proplace into your stack. Pick one, ship in 5 minutes.

curl

The lowest common denominator. Runs from any shell, CI job, or server.

export PROPLACE_TOKEN=pk_live_...
curl -X POST https://alexandre-79537--sourcing-agent-fastapi-app.modal.run/v1/memos \
  -H "Authorization: Bearer $PROPLACE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"fund_type":"s4bt","company":{"name":"Acme"},"memo_type":"detailed"}'

Python (httpx)

import os, time, httpx

API = "https://alexandre-79537--sourcing-agent-fastapi-app.modal.run/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['PROPLACE_TOKEN']}"}

def trigger_and_wait(fund_type: str, company_name: str) -> dict:
    r = httpx.post(f"{API}/memos", headers=HEADERS, json={
        "fund_type": fund_type,
        "company": {"name": company_name},
        "memo_type": "detailed",
    }).raise_for_status().json()
    slug = r["slug"]
    while True:
        time.sleep(30)
        s = httpx.get(f"{API}/memos/{slug}", headers=HEADERS).json()
        if s["status"] in ("ready", "failed"): return s

print(trigger_and_wait("s4bt", "Thrust Carbon"))

Node.js (fetch)

const API = "https://alexandre-79537--sourcing-agent-fastapi-app.modal.run/v1";
const TOKEN = process.env.PROPLACE_TOKEN;

async function triggerAndWait(fundType, companyName) {
  const r = await fetch(`${API}/memos`, {
    method: "POST",
    headers: { Authorization: `Bearer ${TOKEN}`, "Content-Type": "application/json" },
    body: JSON.stringify({ fund_type: fundType, company: { name: companyName }, memo_type: "detailed" }),
  }).then(r => r.json());

  while (true) {
    await new Promise(r => setTimeout(r, 30_000));
    const s = await fetch(`${API}/memos/${r.slug}`, { headers: { Authorization: `Bearer ${TOKEN}` } }).then(r => r.json());
    if (["ready", "failed"].includes(s.status)) return s;
  }
}

Make.com

HTTP module configuration — paste these fields into a Make HTTP module:

URL:           https://alexandre-79537--sourcing-agent-fastapi-app.modal.run/v1/memos
Method:        POST
Headers:
  Authorization: Bearer YOUR_TOKEN
  Content-Type:  application/json
Body type:     Raw
Content-type:  JSON (application/json)
Request body:  {"fund_type":"{{1.fund_type}}","company":{"name":"{{1.company_name}}"},"memo_type":"detailed"}
Parse response: Yes

Then chain a Sleep (30s) + HTTP GET on /v1/memos/{{2.slug}} inside a Repeater until status === "ready".

n8n

Use the HTTP Request node with these settings:

Method:        POST
URL:           https://alexandre-79537--sourcing-agent-fastapi-app.modal.run/v1/memos
Authentication: Header Auth
  Name:        Authorization
  Value:       Bearer YOUR_TOKEN
Body Content Type: JSON
Body (JSON):
  {
    "fund_type": "{{ $json.fund_type }}",
    "company": { "name": "{{ $json.company_name }}" },
    "memo_type": "detailed"
  }

For the polling loop, chain a Wait node (30s) + an IF node checking status === "ready", looping back to a second HTTP Request node on /v1/memos/{{slug}}.

Zapier

Use the Webhooks by Zapier action with these fields:

Action:    POST (Webhooks by Zapier)
URL:       https://alexandre-79537--sourcing-agent-fastapi-app.modal.run/v1/memos
Payload Type: JSON
Data:
  fund_type:  {{fund_type}}
  company:    {"name": "{{company_name}}"}
  memo_type:  detailed
Headers:
  Authorization: Bearer YOUR_TOKEN
  Content-Type:  application/json
Unflatten: Yes

The 202 response includes a slug you can pass to a follow-up Custom Request → GET step on /v1/memos/<slug>.

ChatGPT (Custom GPT Actions)

In your Custom GPT editor, scroll to ActionsCreate new action, then:

Schema:           Import from URL
URL:              https://alexandre-79537--sourcing-agent-fastapi-app.modal.run/v1/openapi.json
Authentication:   API Key
  Auth Type:      Bearer
  API Key:        YOUR_TOKEN
Privacy Policy:   https://proplace.co/privacy

The 6 actions appear in the GPT: listFunds, getFund, screenDeal, triggerMemo, getMemo, patchMemo.

Sample prompt to test: "List the funds I can target, then run a screen on Thrust Carbon for the first one."

Claude Desktop / Cursor / Windsurf (MCP)

Add this to your MCP config (e.g. ~/.claude/mcp.json for Claude Desktop, or your Cursor/Windsurf settings):

{
  "mcpServers": {
    "proplace": {
      "url": "https://alexandre-79537--sourcing-agent-fastapi-app.modal.run/mcp/mcp",
      "headers": { "Authorization": "Bearer YOUR_TOKEN" }
    }
  }
}

Restart your client. The 6 tools (list_funds, get_fund, screen_deal, trigger_memo, get_memo_status, patch_memo) are now callable in natural language: "Trigger a detailed memo for Thrust Carbon under fund_type s4bt."

Transport: streamable HTTP (JSON-RPC over POST). Protocol version: MCP 2024-11-05.

Errors & rate limits

HTTP status codes

Code Meaning What to do
200/202Success
400Invalid fund_type or missing fieldCheck the available_funds field in the response body
401 / 403Token missing or invalidVerify your Authorization header
404Slug or fund_type not foundSpelling — slug is case-sensitive, fund_type is not
409Idempotency-key replayYou're seeing the cached original response — safe
429Rate limit hitHonor the Retry-After header
502Upstream (Make / GitHub / Airtable) failedRetry. If persistent, email us.

Rate limits

Default: 60 requests/minute and 1,000/day per token. Custom quotas available on request. Every response includes:

  • X-RateLimit-Remaining-Minute
  • X-RateLimit-Remaining-Daily
  • X-RateLimit-Reset-Minute (unix timestamp)
  • X-RateLimit-Reset-Daily (unix timestamp)

Idempotency

Pass any unique key in the Idempotency-Key header on POST requests. We cache the response for 24 hours — retries with the same key return the original response without re-executing the operation.

Changelog

v1.0.0 · launched
Initial public release

Six endpoints (listFunds, getFund, screenDeal, triggerMemo, getMemo, patchMemo). MCP server live. ChatGPT Custom GPT compatible.

Ready to plug Proplace into your stack?

Email alexandre@proplace.co with your fund name + use case. Most clients are live in 10 minutes — token + onboarded fund_type + one example memo.

Get your token →