---
title: "Porting api.forsale"
url: https://services.dev/docs/examples/sales/port
captured: [sales-flows.txt, sales-port.txt, sales-suite.txt]
---
# Porting api.forsale

URL: https://services.dev/docs/examples/sales/port

> Every api.forsale route, matched to the operation the generated sales brand serves it with, adapted to a new shape, or decided, and each of the sales kernel's 76 behavior tests matched to the line that carries it. Then the kernel's money path and api.forsale's own probe suite, passing against the generated brand.

*sales's words: deal is a job, account is a subject, gig is a task, closer is a member, seller is a client, seller is a buyer (examples/sales/brand.ts).*

api.forsale was built by hand: a deal state machine, a seller's mandate naming who works each role, a gate that refuses a close below the floor, and settlement by role. The sales example generates the same business from three files. This page holds the two side by side, the acceptance test for Money and Outcome.

| Measure | Of | Result | Read it |
|---|---|---|---|
| The sales kernel's behavior tests: core, gig-result, gig-lifecycle, sdk and demo, at `52fa49b` | 76 tests | 59 ported as they are · 10 adapted · 7 decided | [Its behavior tests](https://services.dev/docs/examples/sales/port.md#its-behavior-tests) |
| api.forsale's routes, the kernel's and the front door's | 22 routes | 12 served as its clients call them · 6 adapted · 4 decided | [Route by route](https://services.dev/docs/examples/sales/port.md#route-by-route) |
| The kernel's money path, its numbers unchanged | 10 rows | 10 passed · 0 failed | [The suite](https://services.dev/docs/examples/sales/port.md#the-suite) |
| api.forsale's own probe suite | 10 probes | 10 passed · 8 unchanged, 2 rewritten for a door that sells | [The suite](https://services.dev/docs/examples/sales/port.md#the-suite) |

## The suite

The kernel's money path, then api.forsale's probes. Two probes expected the door's answers from before it sold, a free price and a stub OFFER, and read the brand's own now:

```bash
npx services.dev verify --suite https://api.forsale/verify   # api.forsale's own probe suite, two probes rewritten for a door that sells
```

```txt title="magic values: metadata.sandbox.proof = "paid", metadata.sandbox.members = {"close":[{"closed_value":50000},{"closed_value":74000}]}"
api-forsale@2 · 10 probes · from https://api.forsale/verify · against http://localhost:8787
✓ keyless-ok              GET /deals                                200 OK
✓ known-empty-1           GET /deals?stage=none                     200 EMPTY
✓ known-empty-2           GET /deals?role=none                      200 EMPTY
✓ known-forbidden-1       GET /deals?scope=tenant                   403 BLOCKED
✓ known-forbidden-2       GET /deals?scope=settlement               403 BLOCKED
✓ pricing                 GET /pricing                              200 · model outcome: 5% of the closed value at won · rewritten from model free
✓ offer                   GET /offer                                402 OFFER · orders with a scoped payment token · rewritten from offer-stub
✓ card                    GET /.well-known/agents.json              200
✓ openapi                 GET /openapi.json                         200
✓ llms                    GET /llms.txt                             200

10 passed · 0 failed · 8 probes unchanged, 2 rewritten from the door's answers before it sold
```

## Route by route

`port.ts` reads each api.forsale route, finds the operation that serves it, and calls the running brand. A resource's `.md`, `.json` and `.html` twins count as one route.

| Mark | Means |
|---|---|
| ✓ | The generated operation serves the route as api.forsale's clients call it |
| adapted | The operation exists with another shape, and the row says what a client changes |
| decision | Another generated surface serves it, or it stays the brand's own, and the row says which |

```bash
node -e "import('./port.ts')"   # each api.forsale route: its mark, the operation, and what the sales brand answered
```

```txt title="magic values: none"
✓         POST /deals                    service.order                                402 OFFER
✓         GET /deals/:id                 job.get                                      404 EMPTY not_found
decision  POST /deals/:id/advance        step.propose                                 a generated surface · the gate advances the graph and no caller drives it: AI proposes each step, and a closer proposes in their own session
✓         POST /deals/:id/signals        job.signal                                   404 EMPTY not_found
adapted   POST /deals/:id/gig-result     task.propose · task.complete · task.release  POST /gigs/:id/propose, /negotiate, /complete and /release, in the closer's own session on closers.deals: one call per act, never a callback the seller's key could send
adapted   GET /deals/:id/settlement      job.ledger                                   GET /owner/deals/:id: the owner's read, with the ledger; the seller reads the charge on GET /deals/:id
adapted   POST /deals/:id/reengage       job.again                                    POST /deals/:id/again: a new deal with from: on its record, from any ended deal
adapted   POST /sell                     service.order                                POST /deals, then GET /deals/:id with wait: one order, and the same answer whether AI closes or a closer does
✓         GET /deals                     job.list                                     200 EMPTY
adapted   GET /gigs                      task.feed                                    GET /gigs on closers.deals, in a member's session: the teasers, with pay, before a claim
✓         GET /offer                     service.offer                                402 OFFER
✓         GET /pricing                   brand.pricing                                200 application/json
✓         GET /openapi.json              brand.openapi                                200 application/json
✓         GET /llms.txt                  brand.llms                                   200 text/markdown
✓         GET /.well-known/agents.json   brand.card                                   200 application/json
✓         GET /index.json                brand.home                                   200 application/json
✓         POST /mcp                      brand.mcp                                    200 initialize
✓         GET /mcp                       brand.mcp                                    405 · POST only, as MCP over HTTP asks
adapted   POST /api/waitlist             service.waitlist                             POST /deal/waitlist { email }: one waitlist per Service, which a not_live answer names
decision  GET /icp.json                  public/icp.json                              kept as a brand route · a file of the brand's own in public/, served as it is
decision  GET /family.json               public/family.json                           kept as a brand route · the sales family's doors, a file of the brand's own in public/
decision  GET /verify                    brand.verify                                 a generated surface · the Run our tests page of the brand's docs, beside /verify/suite.json, which the suite above reads
22 routes · 12 ✓ · 6 adapted · 4 decision · every ✓ called against the running brand
```

## What was decided

| Kernel | Decision | Why |
|---|---|---|
| `POST /deals/:id/advance` | never a call: the gate | AI proposes each step, and a closer proposes in their own session; no caller drives the graph |
| A commission set per deal, clamped to the seller's cap | the closer's 10% is the brand's, in one line | A gig's pay is fixed at post, and a closer reads one rate on every teaser |
| `/`, `/health` and `POST /demo/reset` | the brand's own routes, and test mode | A test key and test clocks are the sandbox, so nothing needs resetting |
| The setter's and nurturer's fees, charged to the seller | funded from api.forsale's 5% | The seller pays nothing on effort: [who funds the setter](https://services.dev/docs/examples/sales/money.md#who-funds-the-setter) |
| The one-way door: a deal a person touched stays a person's for every role | a closer's span, and her first call on a revived step | Each step's `escalate` is its own, so a deal a setter booked under `humans_from` still meets with AI. An order that wants a person on every role sets `setter: 'person'` with `humans_from: 0` |

## Its behavior tests

`flows.ts` reads each of the kernel's tests and names the line that carries it:

```bash
node -e "import('./flows.ts')"   # the sales kernel at 52fa49b: 76 tests in core, gig-result, gig-lifecycle, sdk, demo
```

```txt title="magic values: none"
76 tests · 59 ✓ · 10 adapted · 7 decision
## ✓ · 59
## adapted · 10
## decision · 7
```

The adapted rows, whole:

```bash
node -e "import('./flows.ts')" -- --mark adapted
```

```txt title="captured/sales-flows.txt · magic values: none"
  core#the one-way door: a human-track deal stays human for every subsequent role
    → deal.ts roles.closer.span, revive: { first_call: '24h' } · a closer's span stays hers from the meeting to the end, and a revived step comes back to her first; a deal a setter booked under humans_from still meets with AI, since each step's escalate is its own, so an order that wants a person on every role sets setter: 'person' with humans_from: 0
  core#fees settled at gig fulfillment appear as a line item in the all-in economics
    → deal.ts roles.setter.pay · the setter's $250 is api.forsale's cost, debited from its balance at the held meeting, so the seller pays nothing on effort; from: 'buyer' runs the kernel's way
  gig-result#refuses to re-engage a deal that is not closed_lost
    → POST /deals/:id/again · again takes any ended deal, a won one for a renewal; a live deal answers not_terminal
  sdk#createDeal throws GateRefusedError with the violations payload on an out-of-bounds mandate
    → the SDK · BLOCKED comes back as a typed value, policy_invalid with its fill list, not a thrown error
  sdk#getDeal throws NotFoundError for an unknown id
    → the SDK · not_found, as a typed value
  sdk#settlement throws ConflictError on a deal that is not closed_won
    → the deal's ledger · a lost deal reads +$0.00; there is no settlement call to refuse
  sdk#advance escalates a big B2B deal into a fully-briefed closer gig with suggested closers
    → pools/closers.ts rank: ['fit', 'track'] · the gig carries the brief to the feed, ranked by fit, then track; the seller is suggested no one
  sdk#register / listClosers / closer round-trip
    → pools/closers.ts join · a closer joins at closers.deals/join in their own session; the owner reads the roster in the console
  sdk#the full human span through the SDK: intermediate advances, a gate refusal, then a legal close
    → the delivery portal · a closer acts in their own session, in the portal or the member CLI, never through the seller's SDK
  demo#seeds Dana Reyes and Marcus Webb on boot
    → test mode · a sk_test_ key stands simulated closers in, and metadata.sandbox.members picks what each proposes
```

## Port your own

`port.ts` is a list: each route of the brand you're porting, the operation that serves it, and its mark. The run above calls every row against the generated brand and prints what it answered, so a ✓ is an answer, never a claim. `flows.ts` does the same for the brand's tests. One row of each mark:

```ts title="sales · port.ts"
export default [
  { route: 'POST /deals/:id/signals', op: 'job.signal', mark: '✓' },
  { route: 'POST /deals/:id/reengage', op: 'job.again', mark: 'adapted', moved: 'POST /deals/:id/again: a new deal with from: on its record, from any ended deal' },
  { route: 'POST /deals/:id/advance', op: 'step.propose', mark: 'decision', decision: 'a generated surface', why: 'the gate advances the graph and no caller drives it: AI proposes each step, and a closer proposes in their own session' },
] as const satisfies readonly Port[]
```

[What the brand gets](https://services.dev/docs/examples/sales/surfaces.md) shows the same operations on every surface.

---

- Up: [The sales example](https://services.dev/docs/examples/sales.md)
- Previous: [The closers' Pool](https://services.dev/docs/examples/sales/pool.md)
- Next: [Take your first real deal](https://services.dev/docs/examples/sales/launch.md)
