---
name: prefsift
description: Settle a decision between several sides (people, agents, businesses) with PrefSift, a neutral referee. Use when two or more parties must agree on one option from a set (a time, a price, a split, a plan), when you are invited to a PrefSift session with a code, or when you need to check offers against your person's limits. PrefSift crosses out options that break anyone's limits, removes options another option beats (as good for every side and better for at least one), applies only rules the sides declared, and explains every result. It never books, pays, signs, or messages anyone.
---

# PrefSift for agents

PrefSift is a decision API. Each side says what it will and won't accept; PrefSift returns which options are crossed out (and why), which are beaten, which are kept, and, if the sides declared a rule for picking, which one is picked. Every reason is traceable to something a side sent. Results are data; you act on them.

Base URL: whatever your operator configured (below: `$PREFSIFT`). JSON in, JSON out. Starting a decision (`/v1/decide`, `POST /v1/sessions`) needs your operator's API key in `Authorization: Bearer psk_...` when the server requires one (`GET /v1/health` says `"auth": "api_key"`). Session calls authenticate with a per-participant code in the header `X-Session-Code` (or `?code=`); an invited participant never needs a key.

## Pick a way to call it

| Situation (mode) | Call |
|---|---|
| You already have every side's limits (you are the organizer, or the app holding both sides) | `POST /v1/decide` once (mode A: one caller has every side) |
| The other side's agent won't join, but will answer questions | `POST /v1/decide`, ask the returned questions in your normal channel, call again with the answers (mode B: the other side answers through you) |
| A neutral host (a marketplace, an app) collects each side's input with consent | session with `"relay": true` (mode C: a host relays each side) |
| Each side should submit its own limits privately, or you are a party yourself | session, each side joins with its own code (mode D: each side submits its own) |

Rule of thumb: if you are a party, do not collect the other side's private limits yourself; invite them to a session.

## Start a session (initiator)

```http
POST $PREFSIFT/v1/sessions
{
  "context": "One sentence on what is being decided",
  "kind": "plan",                                  // plan | split | deal | unknown: your declaration, never inferred
  "attributes": [
    {"attribute_id": "price", "type": "money", "currency": "USD", "label": "Price"},
    {"attribute_id": "start", "type": "timestamp", "timezone": "America/New_York", "label": "Start"}
  ],
  "candidates": [{"candidate_id": "opt1", "meaning": "Short description", "facts": {"price": "$120", "start": "2026-10-16T19:00:00-04:00"}}],
  "initiator": {"participant_id": "me", "label": "Me", "party": {"party_id": "me", "evidence": [ ... ]}},   // omit "party" if you have no side
  "parties": [{"party_id": "them", "label": "Them", "redacted_as": "the other side"}],
  "visibility": {"limits": "own_only", "reasons": "redact_others"},
  "options": {"sources": ["supplied"], "who_can_add": ["initiator"]},
  "deadline": "2026-10-12T17:00:00-04:00"
}
```

Returns `session_id`, `codes` (one per participant; shown once; keep them secret), `participant_docs_url`, and `invitations`: for each invited participant a ready-to-paste message (three sentences plus the code) that an agent with no web access can act on. Send each participant its own invitation through your normal channel.

Types: `boolean`, `categorical` (needs `domain`), `integer`, `decimal`, `money` (needs `currency`), `duration`, `timestamp` (ISO 8601 with offset, or a declared `timezone`), `interval` (`{start, end}`), `set` (needs `domain`).

Optional on create: `host` and `observers` (`{participant_id, label}`), `relay`, `required_parties`, `payer` (default: the initiator; everyone else pays nothing), `memory_gate` (`auto` default), `selection_rules` (below), `outcome_rules` (rules every option must meet, visible to all), `history` / `prior_session_id` + `prior_session_code` (earlier outcomes).

## Join, submit, or decline (invited participant)

1. Read the docs: `GET $PREFSIFT/v1/sessions/{id}/docs` (markdown, rendered from this session's permission table: who sees what, the declared rules, who pays).
2. Join: `POST /v1/sessions/{id}/join` with your code.
3. Submit your side: `PUT /v1/sessions/{id}/parties/{your_party_id}` with your code:

```json
{"evidence": [
  {"evidence_id": "e1", "kind": "instruction", "said_by": "principal", "source": "what my person told me, Oct 10", "text": "Not over $400. Nothing after Friday."},
  {"evidence_id": "e2", "kind": "principal_quote", "source": "chat, Oct 9", "text": "I can't do mornings."},
  {"evidence_id": "e3", "kind": "agent_summary", "text": "Cheaper is better for her.", "visibility": "private"}
]}
```

Kinds: `instruction` (a rule from your person or operator), `principal_quote` (their words, verbatim), `agent_summary` (your own summary), `inferred_memory` (something you remember; add `matter_relevance`: `this_matter` | `prior_matter` | `unknown`), `memory_file` (a whole raw memory or personalization file; PrefSift keeps only lines about this decision and holds the rest as hypotheses), `system_record` (calendar, price list, policy, quote), `structured` (rules and facts as data, no model needed). You decide what to send: send what bears on this decision, nothing else. `"visibility": "private"` keeps an item and the limits read from it to your side whatever the table says.

The response echoes what PrefSift read from your side (`compiled.rules`): check it. Sending again replaces your side and re-decides.

4. Or decline: `POST /v1/sessions/{id}/decline` with your code and an optional `{"reason": "..."}`. A required side declining ends the session.

## Read the result

`GET /v1/sessions/{id}/result` with your code. Key fields:

- `status`: `open` (waiting for `waiting_for`), `decided`, or `ended`.
- `result.known_nondominated`: the kept options. `result.selected_candidate_id`: the pick, if declared rules picked one (`picked_another_option: true` if it is an option you can't see). `result.feasibility`: crossed out / unresolved.
- `explanation.options`: one sentence per option you can see, rendered for you. Your own limits are named ("$420 is over your $400 limit"); other sides' limits are redacted ("doesn't fit the buyer") unless the table lets you see them.
- `explanation.questions_detail`: questions that would settle something. Each has `ask` (who should answer: a party id or the participant who added the option), `for_you`, `about`, `attribute_ids`, `candidate_ids`, and `answer_with` (the shape to send).
- `options` (with facts and sources), `limits` (what you may see; always your own), `coverage.candidate_scope` (`discovered_nonexhaustive` means "best of what was found", not "best that exists"), `billing` (`you_pay_usd`).

## Answer a question

- In a session, if `for_you` is true: submit your side again with the answer added as an evidence item (or, for a fact about an option you added, re-send that option with the fact via `POST /candidates`; your own option is updated in place).
- Mode B (the other side never joins): ask their agent the question's `text`, then call `POST /v1/decide` again with the answer as evidence on their party: `{"kind": "agent_summary", "said_by": "counterparty", "authority": "third_party_claim", "source": "their agent, chat", "text": "<their answer>"}`.

## Options: where they come from

`options.sources`, combinable:
- `supplied`: in `candidates`, or added with `POST /v1/sessions/{id}/candidates` by whoever `who_can_add` allows (`initiator`, `parties`, `host`, `observers`, or participant ids).
- `corpus`: PrefSift reads options from documents the sides send (`system_record` items, or any item with `"contains_options": true`), each fact held to its quote.
- `prefsift_search`: PrefSift searches the web (capped by `options.search.cost_cap_usd`, billed to the payer at cost).
- `harness_search`: PrefSift returns `search_requests` (query, what to extract, the shape to return); run them with your own tools and `POST /v1/sessions/{id}/search-results` with `{"search_request_id", "candidates": [...]}` (with `/v1/decide`, put them in `candidates` on the next call). Include the page URL in `source_refs` and a verbatim `quote` with each fact.

Duplicates (same id, same URL, or same description and facts) are merged.

## Declared rules (picking one fairly)

Without rules PrefSift returns the kept set. With `selection_rules`, it applies them in order to the kept set, records each step, and picks only if one option is left:

| kind | keeps |
|---|---|
| `minimize_max` / `maximize_min` / `equalize` | the smallest largest share / the largest smallest share / the smallest spread, over a per-party measure |
| `fewest_violations` | fewest unmet named preferences (`name`, or inline `preferences`) |
| `priority_by_history` | the option whose losers did not lose last time (from `history`) |
| `fewest_changes` | the fewest changes from the previous outcome |
| `prefer` | the best value on one attribute (`direction` or `order`) |
| `party_priority`, `stable_id` | one side's declared order; the first id (always breaks a tie) |

Per-party measure: tag attributes with `"belongs_to": "<party_id>", "measure": "<name>"` (e.g. one hours attribute per person, all with `measure: "hours"`). Named preferences: a party sends `{"kind": "structured", "structured": {"named_preferences": [{"name": "top_choice", "predicate": {...}, "label": "..."}]}}`; they never cross anything out. State the rules in the invitation: joining means accepting them. Recurring decisions: `POST /v1/sessions/{id}/next` (initiator code) starts the next round with the same participants, rules and permissions; this round's outcome joins the history.

## Rules as data (structured evidence)

```json
{"evidence_id": "s1", "kind": "structured", "structured": {"rules": [
  {"rule_id": "max", "kind": "constraint", "predicate": {"op": "le", "attribute_id": "price", "value": "$400"}},
  {"rule_id": "days", "kind": "constraint", "predicate": {"op": "in", "attribute_id": "start", "value": {"weekday": ["tue", "wed", "thu"], "tz": "America/New_York"}}},
  {"rule_id": "evening", "kind": "constraint", "predicate": {"op": "after", "attribute_id": "start", "value": {"time_of_day": "17:00"}}},
  {"rule_id": "cheaper", "kind": "monotone", "attribute_id": "price", "direction": "lower"}
]}}
```

Rule kinds: `constraint`, `monotone`, `value_pref` (`better`/`worse`), `indifference`, `irrelevance` ("doesn't matter to me": lets options that differ only there be compared), `importance`, `acceptance`. Silence is not indifference: a side that says nothing about an attribute blocks "beaten" on it.

## Sign-off (approvals)

If you represent a side whose decisions need a person's sign-off ("anything within 2% of our floor goes to the manager"), add `approvers: [{"participant_id": "ray", "label": "Ray, sales manager"}]` and `approvals: [{"rule_id": "near_floor", "party_id": "<your party>", "approver": "ray", "within_pct": 2}]` to the session. Send Ray his invitation. When a decision needs him, the session's status is `awaiting_approval`; he reads `approval.requests` in his view and answers `POST /v1/sessions/{id}/approvals/{approval_id}` with `{"decision": "approve"}` or `{"decision": "reject"}`. A reject decides again without that option.

If you are the other side and the status is `awaiting_approval`, wait (or use the session's webhook): you will not be told why, and the result appears when it is approved. Do not tell your person a deal is done until the status is `decided`.

## Errors

`401` no code (or no API key where one is required); `402` the starting API key reached its monthly cap; `403` wrong code or not permitted (e.g. submitting another side without relay); `404` unknown session or party; `409` session ended, still waiting before the deadline, or a declined side; `422` invalid input (the message says what); `503` the reader or search is unavailable (never a disguised success).
