# haggle

**Negotiation for buyer and seller agents, inside limits their people set.**

Shopping agents are starting to make offers, and store agents are starting to answer them. `haggle` is the part in
the middle: two agents trade offers on price *and* everything else that matters (delivery, warranty, extras), each
inside a private limit, each with a strategy, and every message lands in a hash-chained transcript that either side
can verify later.

One file, zero dependencies, Node 18+ and the browser. MIT licence.

```
npm test                          # 13 tests
node bin/haggle.js demo           # two agents, one laptop
node bin/haggle.js play           # you sell, the agent buys
node bin/haggle.js bench          # every strategy against every strategy
```

## What it does

- **Multi-issue offers.** An offer is a bundle: `{ price, delivery, warranty, extras }`, or any issues you define.
  Each side scores bundles with its own weights, so the agents can trade: the buyer gives on delivery speed it barely
  cares about, the seller gives on a warranty that costs it little, and both end up better off than splitting the price.
- **Private limits that hold.** The buyer's highest price and the seller's lowest price, plus hard requirements such
  as "never without a 6-month warranty". Nothing outside them is ever offered or accepted.
- **An approval line.** `askAbove` (buyer) or `askBelow` (seller): the agent negotiates as if the line were its limit,
  and only when it has no room left and the other side's offer is still inside the real limit, it stops and asks its
  person. Yes closes the deal; no turns the line into the limit.
- **Strategies.** How fast each side gives ground: `boulware` (holds out), `linear` (steady), `conceder` (eager) and
  `mirror` (gives what it got, never slower than holding out).
- **An opponent model.** Issues the other side keeps moving on matter less to them; the agent uses that to pick, among
  offers it likes equally, the one the other side probably likes most.
- **Honest messages.** Agents exchange offers and a fixed set of notes: `opening`, `counter`, `final`, `accept`,
  `walk`, `ask-human`. `final` is only sent when it is true (last round or no room left). There is no free text, so
  there is nothing to bluff with.
- **A transcript you can check.** Every message carries the SHA-256 of the one before; `verify()` finds any edit.
- **Analysis.** `zopa()` (where deals are possible), `frontier()` (the efficient frontier), `concession()` curves,
  and Pareto-optimality of every deal.
- **A benchmark.** `bench()` runs every strategy against every strategy on seeded scenarios; same seed, same numbers.

## Use

```js
const H = require("haggle");            // or <script src="haggle.js"> → window.Haggle
const space = H.SPACES.laptop;          // price $300–600, delivery 1–10 days, warranty 0–24 months, extras

const result = H.negotiate(space,
  { limit: 480, require: { warranty: { min: 6 } }, strategy: "linear", askAbove: 450 },
  { limit: 410, strategy: "boulware" },
  { approve: (offer, side) => askMyPerson(offer) }      // optional; without it the run stops at "needs-approval"
);

result.outcome      // "deal" | "no-deal" | "needs-approval"
result.deal         // { price: 410, delivery: 10, warranty: 6, extras: 0 }
result.surplus      // { buyer: 70, seller: 0 }  room each side kept inside its limit
result.transcript   // every message, hash-chained
H.verify(result.transcript).ok
```

Your own deal space:

```js
const space = { issues: {
  price:    { min: 900, max: 1500, step: 10, unit: "$" },
  delivery: { min: 1, max: 14, step: 1, unit: "days" },
  install:  { options: ["none", "basic", "full"] }
} };
const buyer = { limit: 1300, prefs: { price: { weight: 0.6 }, install: { weight: 0.3, prefer: "high" }, delivery: { weight: 0.1, prefer: "low" } } };
```

An agent against a person (or any other program):

```js
const s = H.session(space, { role: "buyer", limit: 480, strategy: "linear" });
s.start();                                   // the agent's opening offer
s.send({ price: 560, delivery: 3, warranty: 12, extras: 1 });   // → { type: "offer" | "accept" | "walk" | "ask-human", … }
s.decide(true);                              // after "ask-human": the person's answer
s.state();                                   // { done, transcript, head }
```

## API

| | |
|---|---|
| `negotiate(space, buyer, seller, opts)` | runs a negotiation; `opts.approve(offer, side)` answers approval requests |
| `session(space, agentCfg)` | one agent against a person: `start()`, `send(offer)`, `accept()`, `walk()`, `decide(yes)`, `state()` |
| `createAgent(cfg, space)` | a single agent: `open()`, `respond(offer)`, `utility(offer)` |
| `zopa(buyerLimit, sellerLimit)` | the price range where a deal is possible, or `null` |
| `frontier(space, buyer, seller)` | the efficient frontier: `[{ buyer, seller, offer }]` |
| `concession(strategy, floor, steps)` | a strategy's target curve |
| `bench({ scenarios, seed, strategies })` / `createBench()` | the tournament, all at once or step by step |
| `verify(transcript)` | `{ ok, at, head }` |
| `describe(space, offer)`, `utility(space, role, prefs, offer)`, `sha256(text)`, `rng(seed)` | helpers |

Agent config: `limit` (required), `require`, `prefs` (`{ issue: { weight, prefer: "low" | "high" } }`),
`strategy`, `deadline` (rounds, default 10), `askAbove` / `askBelow`.

## Benchmark (60 scenarios, seed 7)

| buyer vs seller | deal rate | buyer's share of the overlap | rounds | deals on the frontier |
|---|---|---|---|---|
| holds out vs holds out | 56% | 58% | 10.1 | 88% |
| steady vs steady | 93% | 44% | 8.5 | 64% |
| eager vs eager | 98% | 49% | 5.3 | 43% |
| mirror vs mirror | 82% | 28% | 7.8 | 81% |

Deal rate counts scenarios where the limits overlap. The buyer's share is about price only; the rest of the value
moves through delivery, warranty and extras. Run `node bin/haggle.js bench` for the full 4×4 table.

## Honest limits

- The agents are only as private as your code: `haggle` never sends a limit anywhere, but it runs where you run it.
- The opponent model is simple (frequency of change). A determined opponent can read your concessions too.
- `mirror` can be exploited: an opponent that gives away issues it doesn't care about gets real concessions back.
- Utilities are linear per issue. Real preferences have thresholds; model them with `require` or finer issues.
- This is a negotiation engine, not a payment system. Closing a deal does not move money.

## Licence

MIT
