# Typed outcomes — the five-kind algebra every call resolves to

**Every call to a conforming surface resolves to exactly one of five typed outcomes — `OK`, `EMPTY`, `BLOCKED`, `OFFER`, `NEEDS_HUMAN` — and each one is a branch you can act on.** A result is a result. An empty set says it is empty. A closed door says why. A paywall is a price. A human gate is a handoff you can resume from. This page is the algebra: what each kind means, what it carries, and the law that outlaws the sixth kind — the untyped dead end.

The algebra exists for exactly one reader: you, mid-plan, deciding your next move on the strength of one response. Everything below is what that response is allowed to be.

---

## The frame

`AX = Agent eXperience — what UX and DX were for humans, AX is for agents.`

The human-first web answers you in one type: `200`-shaped. Empty result? A `200` over `[]` — reads as data. Login wall? A `200` over HTML — reads as content. Paywall? A `403` and a sales page — reads as a refusal, when it was a price you could have paid. Untyped answers make every failure look like success and every gate look like a wall — you branch on lies, or you stop dead at doors that money or your principal could have opened. It is the same villain you meet everywhere on this site — the surface with a salesperson where the price should be — seen at wire resolution: the gate that cannot tell you, in a type, what would open it.

**AXP — the Agent eXperience Protocol (https://apis.ax/axp)** replaces that with a closed algebra. Five kinds. Total. You can write one `switch` over every answer a conforming surface will ever give you.

---

## The five kinds

| Kind | It means | Status | It carries | Your move |
|---|---|---|---|---|
| `OK` | The job ran; here is the real result | `2xx` | the result body | act on it |
| `EMPTY` | The call worked; the set is genuinely empty | `2xx`, typed body | `results: []`, optional `message` | re-plan — you were not fooled |
| `BLOCKED` | This door is closed to this call as made | non-`2xx`, **never `402`** | `reason`, optional `reauth` | fix the call, re-auth, or route around |
| `OFFER` | Payment opens this door | `402`, always | `(id \| title)` + `(price \| checkoutUrl \| alternatives)` | settle iff `price ≤ Mandate ceiling`, retry → `OK` |
| `NEEDS_HUMAN` | A human decision opens this door | `202` | `url` + `shortUrl` + `pollUrl`, `expiresAt`, stakes-gated `code` | hand off, poll, resume |

One more kind exists on your side only: **`FOREIGN`** — the untyped, non-conformant world. The Round-trip Law (`readOutcome(serialize(o)) ≡ normalize(o)`, for every wire outcome) makes FOREIGN readable but never constructible: you can classify the human-first web when you meet it, but a conforming surface can never emit it.

---

## Why a `402` is an `OFFER`, never a refusal

To an eye, `Payment Required` is a stop sign. To you — a wallet holding a Mandate ceiling — a stop sign with a price on it is a path. ADR 0019 makes that normative: **a `402` MUST carry a typed `OFFER` and never a `BLOCKED`.** The `OFFER` names the exact price, or a checkout, or `alternatives`; you settle if and only if the price sits at or under your ceiling, and the same call returns `OK`. No human arbitrates.

A paywall pretending to be an error is a conformance failure, not a style complaint: it converts a purchasable branch into a dead end, and the pinned gate's hard-ceiling probes — anti-theater probes included — fail it. The toolkit is honest by construction on the same point: a `402` constructs only as a typed `OFFER`; the 402-as-`BLOCKED` anti-pattern is purged from the toolkit and its docs. The `OFFER` parser accepts exactly the protocol's A.1/A.5 shape, `alternatives` included, so a conformant offer never reads as null on your side.

---

## Why `EMPTY` never fakes success

A `200` over an empty array is the quietest lie on the wire. It costs the seller nothing and costs you everything: you read *success with data*, you branch on it, and you carry a falsehood forward into every step that follows — silently. No exception, no signal, no way to know which of your beliefs is now wrong.

`EMPTY` splits nothing-found from something-found at the type level. The call worked; the set is empty; you know which — so "no results" becomes a fact you re-plan on instead of a lie you build on. `BLOCKED` is held to the same honesty: a gated call answers non-`2xx` with a typed `reason`, never a `200` over a login page, so *closed* and *content* can never be confused.

---

## The LAW: every gate is an `OFFER` or a Handoff

> **A gate that is not an `OFFER` or a Handoff is a dead end, and dead ends are conformance failures.**

Every gate resolves by what would open it. Money-shaped → typed `OFFER`. Judgment-shaped — authenticate, authorize a payment, approve a consequential act — → typed `NEEDS_HUMAN`, the Handoff. `BLOCKED` is reserved for doors genuinely closed to the call *as made* — malformed input, an exhausted rate window (refilled by the clock; paying cannot refill it), a revoked credential — always with a typed `reason` you can act on. It is never the lawful answer for a door that money or a human decision could open.

The Handoff, precisely: the typed `NEEDS_HUMAN` always carries BOTH the full approval `url` (you are on the human's machine — open the default browser) AND a signed, expiring `shortUrl` (you are remote — relay it over chat), plus a `pollUrl` so you resume autonomously the moment approval lands. The shortUrl is deliberately NOT single-use: messaging apps unfurl links, so `GET` is side-effect-free and renders a stakes-aware preview of exactly what is being approved — only explicit human interaction on the page approves. A verification `code` appears only when stakes demand it (RFC 8628). You are never stranded at a login wall; you are parked at a poll.

---

## Branch on it

The algebra is closed, so your consumer is a total function — five cases, none of them a guess:

```js
switch (outcome.kind) {
  case 'OK':          return act(outcome)
  case 'EMPTY':       return replan('zero results — truthfully')
  case 'BLOCKED':     return reroute(outcome.reason)
  case 'OFFER':       return outcome.price <= ceiling ? settle(outcome) : decline(outcome)
  case 'NEEDS_HUMAN': return resume(await handoff(outcome))   // open url / relay shortUrl, poll, resume
  case 'FOREIGN':     return quarantine(outcome)              // the untyped web — readable, never constructible
}
```

---

## Prove it in the shell

Every command below runs today, in the No-ask Zone. Start with the tool proving it obeys its own law — feed the CLI a verb it does not have:

```sh
npx apis.ax not-a-command
# stderr → { "type": "BLOCKED", "reason": "unknown command \"not-a-command\"",
#            "commands": ["audit"], "see": "https://apis.ax/axp" }   (exit 2)
```

A typed `BLOCKED` and a non-zero exit — never a summary that exits `0` pretending success. A tool that fakes success on unknown input is the faked `200` wearing a shell prompt; ours is bound by clause 4 like everything else.

```sh
npx apis.ax audit <domain>       # api.qa runs the pinned gate against any surface — including the typed-outcome probes
npm install agent-experience     # the zero-dep toolkit: the algebra in both directions (Round-trip Law)
curl -s https://apis.ax/ -H 'accept: application/json'   # the catalog answers in-type
```

The audit is where the algebra grows teeth: **judged by api.qa, never self-graded.** The pinned gate behaviorally probes clause 4 and clause 5 — typed `EMPTY` on empty sets, typed `BLOCKED` on gated calls, the typed `OFFER` at every `402` boundary, anti-theater probes included. **Admission is `passed: true` at a ratified digest, or nothing** — so a Listing in the catalog has already proven, to a verifier its seller cannot rig, that it speaks this algebra. Even the estate's own paywall submits: page.ax's persist gate answers a typed `402` `OFFER` minted from declared pricing, with typed `BLOCKED`/`OK` edges.

---

## Where the algebra goes

State this plainly, because it is the direction, not the present tense.

- **`NEEDS_HUMAN` ratification.** The ratified wire set of A.1 today closes at `OK | EMPTY | BLOCKED | OFFER`. `NEEDS_HUMAN` is the fifth member AXP is ratifying now: the toolkit already speaks it — construct, parse, poll-and-resume — and the ruling pins the `202` pairing, the always-both `url`/`shortUrl`, expiry and decline semantics, and the verifier check that fails a dead-end gate. The day that check lands, a login wall answering `BLOCKED` stops passing admission.
- **The warranty rule** — no value, no charge: a `BLOCKED` or an `EMPTY` is never metered.
- **The firm quote** — an `OFFER` the seller is held to, not an advertisement of a price. **The signed settlement receipt** — the `OK` of a payment itself: portable, provable, typed.

These are open clauses of the protocol, not calls you can make today. Each ships at full claim strength the moment its verifier check lands, and not a sentence sooner — the ratchet only moves one way.

---

## Failure vs. success

**Failure** is the untyped web: a `200` that was a lie, a wall that was a price, a login page where a handoff should be — every failure dressed as success, every gate a dead end, you branching on fog.

**Success** is one total `switch`: five kinds, each honest, each actionable — a result you act on, an empty set you re-plan on, a reason you route around, a price you settle inside your ceiling, a human you pull in at exactly the decision point and resume from the moment they approve.

---

*Human twin: https://apis.directory/typed-outcomes*
