# Submit feedback
Source: https://docs.jeanmemory.com/api-reference/feedback/submit-feedback
POST /feedback
Report match outcomes. Feeds the training loop for your domain ranker.
The moat. Every outcome you submit accumulates into training triplets used to fine-tune your domain's ranker. Past a threshold, fine-tunes run automatically.
Send outcomes as soon as you have them. Noisy is fine; the loss function is tolerant. You can also batch.
```bash curl theme={"dark"}
curl https://api.jeantechnologies.com/v1/feedback \
-H "Authorization: Bearer $JEAN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"domain": "dating",
"items": [
{ "seeker_id": "u_alice", "candidate_id": "u_bob", "outcome": "accepted" },
{ "seeker_id": "u_alice", "candidate_id": "u_carl", "outcome": "rejected" },
{ "seeker_id": "u_alice", "candidate_id": "u_dan", "outcome": "converted", "weight": 2.0 }
]
}'
```
```python Python theme={"dark"}
requests.post(
"https://api.jeantechnologies.com/v1/feedback",
headers={"Authorization": f"Bearer {os.environ['JEAN_API_KEY']}"},
json={
"domain": "dating",
"items": [
{"seeker_id": "u_alice", "candidate_id": "u_bob", "outcome": "accepted"},
{"seeker_id": "u_alice", "candidate_id": "u_carl", "outcome": "rejected"},
{"seeker_id": "u_alice", "candidate_id": "u_dan", "outcome": "converted", "weight": 2.0},
],
},
)
```
```ts TypeScript theme={"dark"}
await fetch("https://api.jeantechnologies.com/v1/feedback", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.JEAN_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
domain: "dating",
items: [
{ seeker_id: "u_alice", candidate_id: "u_bob", outcome: "accepted" },
{ seeker_id: "u_alice", candidate_id: "u_carl", outcome: "rejected" },
{ seeker_id: "u_alice", candidate_id: "u_dan", outcome: "converted", weight: 2.0 },
],
}),
});
```
## Outcome labels
Built-in labels work everywhere:
* `accepted`: user signaled positive intent (like, swipe right, save).
* `rejected`: user signaled negative intent (skip, dismiss).
* `converted`: strongest positive (matched, hired, deal closed, second date).
* `expired`: no decision before timeout.
You can also register **tenant-specific labels** with us at onboarding (`super_like`, `interview_passed`, `paid_subscription`, etc.) and use them here directly.
## Weights
`weight` defaults to 1.0. Use it to express **how strong** an outcome is:
| Signal | Suggested weight |
| ------------------- | ---------------- |
| Casual click / open | 0.25 |
| Like / swipe | 1.0 |
| Conversion | 2.0+ |
| Long-term retention | 5.0+ |
Higher-weighted triplets pull harder on the gradient.
## Example response
```json theme={"dark"}
{
"accepted": 3,
"triplets_generated": 6,
"triplets_pending_train": 4218
}
```
When `triplets_pending_train` crosses your domain's threshold (negotiated at onboarding, typically 1K-10K), a fine-tune kicks off automatically. Force one manually with [`POST /train`](/api-reference/feedback/train).
# Train
Source: https://docs.jeanmemory.com/api-reference/feedback/train
POST /train
Manually trigger a fine-tune for a domain ranker.
By default, fine-tunes run automatically once the triplet budget for a domain is reached. Use this endpoint to force a run sooner: right after a major product change, a marketing push that shifts your user mix, or to validate that newly added outcomes flow through.
```bash curl theme={"dark"}
curl https://api.jeantechnologies.com/v1/train \
-H "Authorization: Bearer $JEAN_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "domain": "dating", "force": true }'
```
```python Python theme={"dark"}
requests.post(
"https://api.jeantechnologies.com/v1/train",
headers={"Authorization": f"Bearer {os.environ['JEAN_API_KEY']}"},
json={"domain": "dating", "force": True},
).json()
```
```ts TypeScript theme={"dark"}
const res = await fetch("https://api.jeantechnologies.com/v1/train", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.JEAN_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ domain: "dating", force: true }),
});
```
## Example response
```json theme={"dark"}
{
"domain": "dating",
"status": "queued",
"run_id": "tr_2026_05_17_001",
"triplets_used": 4218,
"estimated_completion_minutes": 35
}
```
The call returns immediately. The actual fine-tune runs async, typically 15 to 60 minutes depending on triplet volume. The new head is deployed when training completes; subsequent `/match` requests automatically use it.
We notify the email on file when a run completes. Webhook callbacks land in a later release.
# Match
Source: https://docs.jeanmemory.com/api-reference/matching/match
POST /match
Run the matching pipeline for a user and return ranked compatible candidates.
The value moment. Given a `user_id` and a `domain`, the platform runs the pipeline against the most specific [model](/models) available for your tenant (general → domain → tenant-tuned):
```
SQL filter → light ML rerank → domain embedding → cross-encoder → optional LLM judge
```
Each stage hands a smaller candidate set to the next so the expensive stages only see candidates worth their time. You can toggle or tune any stage per request.
```bash curl theme={"dark"}
curl https://api.jeantechnologies.com/v1/match \
-H "Authorization: Bearer $JEAN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"user_id": "u_alice",
"domain": "dating",
"limit": 10,
"filters": { "location_within_km": 50 }
}'
```
```python Python theme={"dark"}
matches = requests.post(
"https://api.jeantechnologies.com/v1/match",
headers={"Authorization": f"Bearer {os.environ['JEAN_API_KEY']}"},
json={
"user_id": "u_alice",
"domain": "dating",
"limit": 10,
"filters": {"location_within_km": 50},
},
).json()
```
```ts TypeScript theme={"dark"}
const res = await fetch("https://api.jeantechnologies.com/v1/match", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.JEAN_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
user_id: "u_alice",
domain: "dating",
limit: 10,
filters: { location_within_km: 50 },
}),
});
const { matches } = await res.json();
```
## Tuning the pipeline
The `stages` object accepts a boolean shortcut or a per-stage config. Mix freely.
```json theme={"dark"}
{
"user_id": "u_alice",
"domain": "dating",
"stages": {
"llm_judge": true,
"cross_encoder": { "enabled": true, "top_k": 25 },
"light_ml": { "min_score": 0.3 }
},
"explain": true
}
```
| Stage | Default | Per-candidate cost | When to tune |
| --------------- | ------- | ------------------ | --------------------------------------------------------- |
| `sql_filter` | on | microseconds | only off for debugging |
| `light_ml` | on | milliseconds | raise `min_score` to be stricter |
| `embedding` | on | sub-ms (ANN) | almost always on; required for quality |
| `cross_encoder` | on | \~10ms | lower `top_k` for latency, raise for quality |
| `llm_judge` | off | seconds | turn on when prompt-fit or free-text deal-breakers matter |
Set `"explain": true` to get per-candidate `stage_scores` and human-readable reasons in the response.
## Example response
```json theme={"dark"}
{
"matches": [
{
"user_id": "c_456",
"score": 0.87,
"stage_scores": {
"light_ml": 0.71,
"embedding": 0.84,
"cross_encoder": 0.87
},
"reasons": [
"shared communication style",
"compatible relationship goals"
]
}
],
"pipeline_stats": {
"after_sql_filter": 12431,
"after_light_ml": 412,
"after_embedding": 53,
"after_cross_encoder": 10
}
}
```
`pipeline_stats` lets you see, per request, how the candidate pool narrowed. Useful when tuning: if `after_light_ml` is too small, your `min_score` is too aggressive. If `after_sql_filter` is huge, your hard filters aren't pulling their weight.
Latency budget: with `llm_judge` off, p99 is typically under 200ms on a 10M-user index. Turning the judge on adds 1-3 seconds per request. Reserve it for the final-decision call, not exploratory queries.
# Get schema
Source: https://docs.jeanmemory.com/api-reference/schema/get-schema
GET /schema
Inspect the active extraction schema for a domain.
Read-only. Returns the field definitions the platform uses when extracting from `context` or `POST /context`. Useful for:
* Debugging why a field isn't being extracted.
* Documenting your own integration against the canonical shape.
* Building admin UIs that show users what they're consenting to.
Schema changes are made through onboarding sessions, not this API.
```bash curl theme={"dark"}
curl "https://api.jeantechnologies.com/v1/schema?domain=dating" \
-H "Authorization: Bearer $JEAN_API_KEY"
```
```python Python theme={"dark"}
schema = requests.get(
"https://api.jeantechnologies.com/v1/schema",
headers={"Authorization": f"Bearer {os.environ['JEAN_API_KEY']}"},
params={"domain": "dating"},
).json()
```
```ts TypeScript theme={"dark"}
const res = await fetch(
"https://api.jeantechnologies.com/v1/schema?domain=dating",
{ headers: { Authorization: `Bearer ${process.env.JEAN_API_KEY}` } },
);
const schema = await res.json();
```
## Example response
```json theme={"dark"}
{
"domain": "dating",
"version": "dating.v3",
"fields": [
{ "name": "age", "kind": "hard", "type": "integer", "required": true, "priority": "high" },
{ "name": "gender", "kind": "hard", "type": "enum", "required": true, "priority": "high" },
{ "name": "location", "kind": "hard", "type": "string", "required": true, "priority": "high" },
{ "name": "relationship_goal", "kind": "hard", "type": "enum", "required": false, "priority": "medium" },
{ "name": "values_text", "kind": "soft", "type": "string", "required": false, "priority": "medium" },
{ "name": "attachment_style", "kind": "derived", "type": "enum", "required": false, "priority": "low" }
]
}
```
Three field kinds:
| Kind | Used by | Example |
| --------- | ------------------ | ------------------ |
| `hard` | SQL filter stage | `age`, `location` |
| `soft` | embedding stage | `values_text` |
| `derived` | computed at ingest | `attachment_style` |
See [Concepts](/concepts) for what each kind means in practice.
# Append context
Source: https://docs.jeanmemory.com/api-reference/users/append-context
POST /context
Stream new context into an existing user without resending the full payload.
Use this when a user **already exists** and you want to enrich them incrementally. The platform extracts against your domain schema, merges into the existing representation, and refreshes embeddings if any soft fields changed.
Typical sources:
* Chat messages between users
* Voice or video call transcripts
* Behavior events (likes, skips, time-on-profile)
* New uploads (additional photos, updated resume)
```bash curl theme={"dark"}
curl https://api.jeantechnologies.com/v1/context \
-H "Authorization: Bearer $JEAN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"user_id": "u_alice",
"domain": "dating",
"source": "chat",
"content": "Just got back from a week of hiking in the Catskills. Loved it. Already planning the next trip."
}'
```
```python Python theme={"dark"}
requests.post(
"https://api.jeantechnologies.com/v1/context",
headers={"Authorization": f"Bearer {os.environ['JEAN_API_KEY']}"},
json={
"user_id": "u_alice",
"domain": "dating",
"source": "chat",
"content": "Just got back from a week of hiking in the Catskills. Loved it. Already planning the next trip.",
},
)
```
```ts TypeScript theme={"dark"}
await fetch("https://api.jeantechnologies.com/v1/context", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.JEAN_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
user_id: "u_alice",
domain: "dating",
source: "chat",
content:
"Just got back from a week of hiking in the Catskills. Loved it. Already planning the next trip.",
}),
});
```
## Example response
```json theme={"dark"}
{
"user_id": "u_alice",
"domain": "dating",
"context_id": "ctx_a8c2",
"fields_updated": ["interests", "values_text"],
"embeddings_refreshed": true
}
```
`fields_updated` tells you which columns actually changed (so you can avoid downstream cache invalidation when nothing moved). `embeddings_refreshed` is `true` when at least one soft field changed and the dense vector was re-computed.
Send context as it arrives. The platform deduplicates near-identical content and rate-limits embedding refreshes per user, so high-throughput sources (a chat feed) won't burn your quota.
## When to use which endpoint
| Situation | Use |
| -------------------------------------------- | -------------------------------- |
| First time you've ever seen this user | `POST /users` |
| Full re-import or bulk backfill | `POST /users` |
| New chat message / event / transcript chunk | `POST /context` |
| User updated a profile field through your UI | `POST /users` (replaces the row) |
| Periodic behavior batch (e.g. nightly job) | `POST /context` per row |
# Get user
Source: https://docs.jeanmemory.com/api-reference/users/get-user
GET /users/{user_id}
Read a user's full representation: structured fields, derived attributes, dense vectors per domain.
Useful for **debugging match decisions**. Every value returned carries provenance, so you can trace any score in `/match` back to which extraction or feedback event produced the underlying field.
Not intended to be in your request hot path. Cache aggressively if you do put it there.
```bash curl theme={"dark"}
curl "https://api.jeantechnologies.com/v1/users/u_alice?domain=dating" \
-H "Authorization: Bearer $JEAN_API_KEY"
```
```python Python theme={"dark"}
user = requests.get(
"https://api.jeantechnologies.com/v1/users/u_alice",
headers={"Authorization": f"Bearer {os.environ['JEAN_API_KEY']}"},
params={"domain": "dating"},
).json()
```
```ts TypeScript theme={"dark"}
const res = await fetch(
"https://api.jeantechnologies.com/v1/users/u_alice?domain=dating",
{ headers: { Authorization: `Bearer ${process.env.JEAN_API_KEY}` } },
);
const user = await res.json();
```
## Example response
```json theme={"dark"}
{
"user_id": "u_alice",
"domain": "dating",
"schema_version": "dating.v3",
"fields": {
"age": 32,
"location": "New York, NY",
"relationship_goal": "serious",
"values_text": "..."
},
"derived": {
"attachment_style": "secure"
},
"embeddings": {
"dating": { "dim": 1024, "version": "dating-2026-05-01" }
},
"last_enriched_at": "2026-05-17T22:01:09Z"
}
```
Omit the `domain` query param to get all heads this user has been processed for. Useful for cross-domain inspection.
# Upsert user
Source: https://docs.jeanmemory.com/api-reference/users/upsert-user
POST /users
Create or update a user with raw context, structured fields, or an external memory reference.
Use this when a user **first enters your system**. Send whatever context you have, in any form:
* **Free text** in `context` (resume, intake transcript, profile blurb). The platform extracts fields against your domain schema.
* **Pre-structured `fields`** when you already have typed values. Skips extraction.
* **`memory_ref`** pointing at an external store you operate (Jean Memory, Mem0, or your own vector DB).
You can mix all three in one call. Anything in `fields` wins over anything extracted from `context`.
For incremental updates later (a new chat message, a follow-up call), use [`POST /context`](/api-reference/users/append-context) instead so you don't re-send the whole user.
```bash curl theme={"dark"}
curl https://api.jeantechnologies.com/v1/users \
-H "Authorization: Bearer $JEAN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"user_id": "u_alice",
"domain": "dating",
"context": "32, NYC, looking for someone serious. Reads a lot, runs in the park, vegetarian.",
"fields": { "location": "New York, NY" }
}'
```
```python Python theme={"dark"}
import os, requests
requests.post(
"https://api.jeantechnologies.com/v1/users",
headers={"Authorization": f"Bearer {os.environ['JEAN_API_KEY']}"},
json={
"user_id": "u_alice",
"domain": "dating",
"context": "32, NYC, looking for someone serious. Reads a lot, runs in the park, vegetarian.",
"fields": {"location": "New York, NY"},
},
).json()
```
```ts TypeScript theme={"dark"}
const res = await fetch("https://api.jeantechnologies.com/v1/users", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.JEAN_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
user_id: "u_alice",
domain: "dating",
context:
"32, NYC, looking for someone serious. Reads a lot, runs in the park, vegetarian.",
fields: { location: "New York, NY" },
}),
});
const user = await res.json();
```
## Example response
```json theme={"dark"}
{
"user_id": "u_alice",
"domain": "dating",
"schema_version": "dating.v3",
"fields": {
"age": 32,
"location": "New York, NY",
"relationship_goal": "serious"
},
"derived": {
"attachment_style": "secure"
},
"embeddings": {
"dating": { "dim": 1024, "version": "dating-2026-05-01" }
},
"last_enriched_at": "2026-05-17T22:01:09Z"
}
```
The `embeddings` object lists each domain head this user now has a vector for. After a single upsert in `dating`, only the dating head is populated. Other heads materialize lazily when the user is matched against in another domain.
## Common pitfalls
Either the schema for this domain has no extraction rules yet (we co-design it during onboarding) or the `context` was too sparse to ground anything. Run `GET /schema?domain=...` to inspect what the platform is trying to extract.
Unrecognized field keys are stored under `extras` and not used for ranking. Add them to the schema at onboarding or strip them before sending.
`POST /users` replaces the user's row entirely (subject to merging logic on provenance). For non-destructive incremental updates, use `POST /context`.
# Concepts
Source: https://docs.jeanmemory.com/concepts
The data model in one page.
Five primitives. Every API call works on one of them. See [Models](/models) for the three-tier compatibility-model architecture that powers `/match`.
A row keyed by `user_id`, scoped to a domain. Structured fields plus dense vectors.
The typed shape of a user in a domain. Hard, soft, and derived fields.
Pipeline that takes a user plus filters and returns ranked candidates.
Outcome labels that train the ranker.
## Users
A `user_id` plus `domain` uniquely identifies a record. Two ways to write:
* **`POST /users`** is an upsert. You send the full payload (raw `context`, pre-built `fields`, or a `memory_ref` to an external store) and we replace or create. Right for initial intake and one-shot uploads.
* **`POST /context`** is an incremental append. You send one new piece of context (a chat message, a voice transcript chunk, a behavior event) and we merge it into the existing representation. Right for anything that streams.
Reading is `GET /users/{user_id}` and returns the full representation: structured fields, derived attributes, dense vectors per domain, and provenance.
The same physical person can have one row per domain. A user in your hiring product and a user in your dating product are different rows, with different schemas and different embeddings.
## Schemas
A schema declares what to extract from incoming context for a given domain. Three kinds of field:
Strict types. Used as `WHERE` clauses in the SQL filter stage. Examples: `age`, `gender`, `location`, `salary_min_usd`.
Free-text fields that feed the domain-adapted embedding head. Examples: `values_text`, `motivation_text`, `thesis_text`.
Computed at ingest from raw input. Examples: `attachment_style` from intake transcript, `tenure_predicted` from CV trajectory, `photo_aesthetic_vector` from vision model.
Schemas are co-designed with you at onboarding, then versioned. You can inspect the current schema for a domain with `GET /schema?domain=...`.
## Matches
`POST /match` runs the pipeline:
```mermaid theme={"dark"}
graph LR
A[All candidates] -->|SQL filter| B[10K]
B -->|Light ML| C[500]
C -->|Dense embed| D[50]
D -->|Cross-encoder| E[10]
E -->|LLM judge| F[Ranked output]
```
Each stage can be toggled or tuned via the `stages` object on the request:
```json theme={"dark"}
{
"user_id": "u_alice",
"domain": "dating",
"stages": {
"llm_judge": true,
"cross_encoder": { "enabled": true, "top_k": 25 },
"light_ml": { "min_score": 0.3 }
},
"explain": true
}
```
Pass `true` or `false` as a shortcut, or pass `{ "enabled": ..., "top_k": ..., "min_score": ..., "model": ... }` for finer control. Set `explain: true` to get per-candidate `stage_scores` and human-readable reasons so you can attribute any decision back to the stage that made it.
| Stage | Per-candidate cost | Typical survivors |
| --------------- | ------------------ | ----------------- |
| SQL filter | microseconds | 10,000 |
| Light ML | milliseconds | 500 |
| Dense embedding | sub-ms (ANN) | 50 |
| Cross-encoder | \~10 ms | 10 |
| LLM judge | seconds | 5 |
## Feedback
`POST /feedback` accepts outcome labels (`accepted`, `rejected`, `converted`, `expired`, or a tenant-registered label) plus an optional `weight`. Outcomes accumulate into training triplets. Past a per-domain threshold, the ranker auto-fine-tunes. Use `POST /train` with `force=true` to skip the threshold.
**Latency of the loop.** Fast signals (like a click) update the reranker continuously. Slow signals (like a 12-month hire tenure) update the embedding head on a longer cadence and weigh more heavily when they land.
## Where context comes from
You decide.
Pass `context` to `POST /users`. The platform extracts fields against your domain schema.
Pass `fields` directly. Skips extraction.
Pass `memory_ref` pointing at an external store you operate (Jean Memory, Mem0, your own vector DB).
`POST /context` for chat messages, transcripts, behavior events. The platform merges incrementally without you re-sending the full user.
Optionally, we run a short intake (LLM-driven call or web widget) on your behalf and ingest the transcript.
# Jean Technologies
Source: https://docs.jeanmemory.com/introduction
Matching infrastructure for AI-era platforms.
**Spec-only preview.** These docs describe the API we are building toward. Endpoints are not yet live and code samples will not return real responses. [Reach out](mailto:jonathan@jeantechnologies.com) to join the closed beta.
A REST API that turns user context into ranked compatible candidates. Built for platforms where match quality is the product: dating, hiring, founder-investor, agent-to-agent, and product recommendation.
Three curl commands. First match in two minutes.
Every endpoint, live playground.
Users, schemas, matches, feedback. One page.
Talk to the team about your domain.
## What the platform does
Free text, structured fields, or a pointer to a memory store you already operate. The platform extracts a typed user representation.
SQL filter, light ML rerank, domain-adapted embedding, cross-encoder, optional LLM judge. Cheap stages narrow the funnel; expensive stages only see the survivors.
Successful matches feed back as training signal. Your domain ranker compounds over time into something only you have.
## Who this is for
Predict compatibility from intake plus behavior, not stated preferences.
Match candidates to roles or buyers to listings on outcome data, not keywords.
Resolve trust and fit between agents acting on behalf of users.
## Get an API key
The platform is in early access. Reach out and we will scope the domain, schema, and outcome signal with you before issuing a key.
[jonathan@jeantechnologies.com](mailto:jonathan@jeantechnologies.com)
# Models
Source: https://docs.jeanmemory.com/models
Three tiers of compatibility model. Start with our general head, end with a model tuned to your platform's outcomes.
Every `POST /match` call runs against a compatibility model. We ship three tiers, and you progress through them as your platform accumulates outcome data.
```mermaid theme={"dark"}
graph TD
G[General compatibility model
Jean's broad outcome corpus]
D1[Dating head]
D2[Hiring head]
D3[Marketplace head]
D4[... per domain]
T1[Your tenant model · dating]
T2[Your tenant model · hiring]
G --> D1
G --> D2
G --> D3
G --> D4
D1 --> T1
D2 --> T2
```
## 1. General compatibility model
The base. A dual-encoder + cross-encoder trained on Jean's outcome corpus aggregated across every domain we operate in. The point of the general model is not to be best-in-class on any one domain. It is to give you a working starting point on day one, even for a brand-new vertical with zero in-tenant outcome data.
Used automatically when:
* You spin up a brand-new domain that we have not yet shipped a specialized head for.
* You are integrating for the first time and have not yet submitted feedback.
## 2. Domain compatibility models
For every domain we ship — `dating`, `hiring`, `founder-investor`, `marketplace`, `agent-to-agent`, `product-recommendation` — we maintain a **domain head**. Each is initialized from the general model and then trained on aggregate outcome data across all Jean tenants operating in that domain.
Two practical advantages over the general model:
* **Quality**: domain-specific signal that the general model averages out. A dating head learns that stated preferences predict the *first message* but not the *second date*; a hiring head learns that pedigree predicts the interview but not the tenure.
* **Cost**: domain heads are smaller and cheaper to serve than the general model at equivalent quality on their domain.
Active by default the moment your tenant is enabled in a domain. No code change required.
## 3. Tenant-tuned models
Your platform's own model. Initialized from the domain head, then fine-tuned on **your** outcome data via the [training flow](#the-training-flow). Captures the idiosyncrasies of your user base, your funnel, and your definition of success.
The longer you operate, the more your tenant model diverges from any off-the-shelf alternative. This is the moat. Switching providers means restarting the compounding curve.
| Tier | Trained on | Available |
| ------------ | ----------------------------------- | ------------------------------- |
| General | Jean's full outcome corpus | Day one, every tenant |
| Domain | All tenants' outcomes in one domain | When a domain ships |
| Tenant-tuned | Your tenant's outcomes only | After threshold or `force=true` |
## How `/match` picks the tier
By default, `POST /match` uses **the most specific model available** for your tenant in the requested `domain`. If you have a tenant-tuned model for `dating`, it serves that. If not, it falls back to the domain head. If the domain is new and we have not built a head yet, it falls back to general.
You can pin to a specific tier per request via the stage config on `/match`:
```json theme={"dark"}
{
"user_id": "u_alice",
"domain": "dating",
"stages": {
"embedding": { "model": "domain" },
"cross_encoder": { "model": "tenant" }
}
}
```
Accepted values: `"general"`, `"domain"`, `"tenant"`. Pin individually per stage so you can, for example, use the tenant cross-encoder while keeping the embedding retrieval on the broader domain head.
Pinning is useful for A/B tests (`?model=tenant` for the treatment arm, `?model=domain` for control) and for debugging regressions after a new fine-tune deploys.
## The training flow
This is how a tenant-tuned model comes into existence and stays current.
Every served match has an outcome eventually. Send them to `POST /feedback` as soon as you know them. Accepted, rejected, converted, expired, plus any tenant-registered labels.
Each labeled outcome becomes one or more training triplets (seeker, positive, negative). We hold them in a per-tenant queue. `POST /feedback` returns `triplets_pending_train` so you can see the counter rise.
When the queue passes your domain's threshold (negotiated at onboarding), an auto fine-tune kicks off. You can also force one early with `POST /train` and `"force": true`.
Fine-tune typically completes in 15 to 60 minutes. The new model is deployed to your tenant and immediately used as the default for future `POST /match` calls. Older runs are retained for rollback.
Continue submitting outcomes. Every subsequent fine-tune builds on the previous, so the model's "taste" sharpens the longer you operate.
```mermaid theme={"dark"}
graph LR
M[/match served/] --> O[Outcome observed]
O --> F[/feedback/]
F --> Q[(Triplet queue)]
Q -- threshold --> T[/train/]
T --> H[New tenant head]
H --> M
```
## What "outcome data" actually means
The training loop is only as good as the signal you feed it. Useful outcomes share two properties:
* **Tied to a specific match.** Outcomes need `seeker_id` and `candidate_id` so we can attribute the signal.
* **Indicate a real preference or business event.** A click is weak; a conversion is strong. A like is weak; a long retained relationship is strong. Use the `weight` field on `/feedback` to express how much a given outcome should pull on the gradient.
See [Submit feedback](/api-reference/feedback/submit-feedback) for the full label and weight reference.
## Cold start: what you get on day one
| Scenario | Default model | Quality vs. baseline |
| ---------------------------------- | ------------- | ------------------------------------- |
| Brand-new domain, no Jean head yet | General | Useful baseline, beats keyword search |
| Existing domain, brand-new tenant | Domain head | Strong out of the box |
| Existing domain, mature tenant | Tenant-tuned | Best available, compounds over time |
The point is that you never have to wait. You start matching the moment the API key is issued, and the quality climbs as outcomes accumulate.
## Inspecting which model served a match
Every `POST /match` response includes the model version used per stage:
```json theme={"dark"}
{
"matches": [...],
"pipeline_stats": {...},
"models_used": {
"embedding": { "tier": "tenant", "version": "u_acme.dating.2026-05-17" },
"cross_encoder": { "tier": "domain", "version": "dating.cx.2026-05-01" }
}
}
```
`tier` plus `version` are stable identifiers. Log them alongside outcomes so post-hoc analysis can attribute lift to a specific deploy.
## Cross-domain reuse
A tenant model trained for `dating` is **not** automatically used for `hiring`, even within the same tenant. Each domain trains independently because the success signal differs. You can opt into cross-domain transfer at onboarding when it makes sense (for example, if you have shared identity across two adjacent verticals).
# Quickstart
Source: https://docs.jeanmemory.com/quickstart
Three calls to your first match.
**Spec-only preview.** The API is not yet live. The commands below will not return real responses today; they describe the developer experience we are shipping. [Talk to us](mailto:jonathan@jeantechnologies.com) about closed-beta access.
You need an API key from the team and a domain name agreed with us (`dating`, `hiring`, etc.). After that, three endpoints get you a working integration.
## Authenticate
```bash macOS / Linux theme={"dark"}
export JEAN_API_KEY=sk_live_...
```
```powershell Windows theme={"dark"}
$env:JEAN_API_KEY = "sk_live_..."
```
Every request takes `Authorization: Bearer $JEAN_API_KEY`.
## 1. Add a user
Send raw text and let the platform extract structure, or send `fields` directly if you already have it.
```bash curl theme={"dark"}
curl https://api.jeantechnologies.com/v1/users \
-H "Authorization: Bearer $JEAN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"user_id": "u_alice",
"domain": "dating",
"context": "32, NYC, looking for someone serious. Reads a lot, runs in the park, vegetarian."
}'
```
```python Python theme={"dark"}
import os, requests
requests.post(
"https://api.jeantechnologies.com/v1/users",
headers={"Authorization": f"Bearer {os.environ['JEAN_API_KEY']}"},
json={
"user_id": "u_alice",
"domain": "dating",
"context": "32, NYC, looking for someone serious. Reads a lot, runs in the park, vegetarian.",
},
)
```
```ts TypeScript theme={"dark"}
await fetch("https://api.jeantechnologies.com/v1/users", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.JEAN_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
user_id: "u_alice",
domain: "dating",
context:
"32, NYC, looking for someone serious. Reads a lot, runs in the park, vegetarian.",
}),
});
```
## 2. Find matches
```bash curl theme={"dark"}
curl https://api.jeantechnologies.com/v1/match \
-H "Authorization: Bearer $JEAN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"user_id": "u_alice",
"domain": "dating",
"limit": 10
}'
```
```python Python theme={"dark"}
matches = requests.post(
"https://api.jeantechnologies.com/v1/match",
headers={"Authorization": f"Bearer {os.environ['JEAN_API_KEY']}"},
json={"user_id": "u_alice", "domain": "dating", "limit": 10},
).json()
```
```ts TypeScript theme={"dark"}
const res = await fetch("https://api.jeantechnologies.com/v1/match", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.JEAN_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ user_id: "u_alice", domain: "dating", limit: 10 }),
});
const { matches } = await res.json();
```
Add `"filters": { "location_within_km": 50 }` to apply hard constraints. Stages can be tuned via `"stages": { "llm_judge": true }` (off by default for latency).
## 3. Send feedback
```bash curl theme={"dark"}
curl https://api.jeantechnologies.com/v1/feedback \
-H "Authorization: Bearer $JEAN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"domain": "dating",
"items": [
{ "seeker_id": "u_alice", "candidate_id": "u_bob", "outcome": "accepted" },
{ "seeker_id": "u_alice", "candidate_id": "u_carl", "outcome": "rejected" }
]
}'
```
```python Python theme={"dark"}
requests.post(
"https://api.jeantechnologies.com/v1/feedback",
headers={"Authorization": f"Bearer {os.environ['JEAN_API_KEY']}"},
json={
"domain": "dating",
"items": [
{"seeker_id": "u_alice", "candidate_id": "u_bob", "outcome": "accepted"},
{"seeker_id": "u_alice", "candidate_id": "u_carl", "outcome": "rejected"},
],
},
)
```
```ts TypeScript theme={"dark"}
await fetch("https://api.jeantechnologies.com/v1/feedback", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.JEAN_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
domain: "dating",
items: [
{ seeker_id: "u_alice", candidate_id: "u_bob", outcome: "accepted" },
{ seeker_id: "u_alice", candidate_id: "u_carl", outcome: "rejected" },
],
}),
});
```
Outcomes accumulate. Past a per-domain threshold the ranker auto-fine-tunes; force it manually with `POST /train`.
## Next
Users, schemas, matches, feedback.
Live playground.