SDK — @sales/sdk
A thin, hand-rolled typed client over both services — no codegen. Non-2xx responses surface as typed errors, and all domain types are re-exported so consumers never import the server packages.
Pre-launch: the SDK is a workspace package in the spike repo, not yet published to a registry. The API surface below is real and covered by the repo’s tests, but it may change before 1.0 — the public API is deliberately not frozen yet.
Usage
import { ForSaleClient, ClosersClient, GateRefusedError, isEscalation } from '@sales/sdk'
const forsale = new ForSaleClient('http://localhost:4802')
const closers = new ClosersClient('http://localhost:4801')
const result = await forsale.sell({
account: { name: 'Northwind', industry: 'Enterprise SaaS', actor: 'B2B' },
listValueUsd: 80_000,
mandate: { staffing: { closer: { humanIf: { minDealSizeUsd: 25_000 } } }, commissionPct: 0.1 },
})
if (isEscalation(result)) {
const dana = result.suggestedClosers[0].closer
await closers.claim(result.gig.id, dana.id)
try {
await closers.advanceGig(result.gig.id, { advanceTo: 'closed_won', valueUsd: 50_000, note: 'caved' })
} catch (e) {
if (e instanceof GateRefusedError) console.log(e.payload) // below the floor — the gig stays claimed
}
}ForSaleClient
| Method | Endpoint | Returns |
|---|---|---|
createDeal(input) | POST /deals | Deal |
getDeal(dealId) | GET /deals/:id | Deal |
advance(dealId) | POST /deals/:id/advance | Deal | EscalationResult |
sell(input) | POST /sell | Deal | EscalationResult |
signal(dealId, signals, note?) | POST /deals/:id/signals | Deal |
settlement(dealId) | GET /deals/:id/settlement | Settlement |
reengage(dealId) | POST /deals/:id/reengage | Deal (new, with lineage) |
isEscalation(result) narrows an AdvanceResult to EscalationResult — { escalated: true, role, gig, suggestedClosers }.
ClosersClient
| Method | Endpoint | Returns |
|---|---|---|
register(input) | POST /closers | Closer |
listClosers() / closer(id) | GET /closers[/:id] | Closer[] / Closer |
match(query) | POST /match | RankedCloser[] |
gigs(status?) | GET /gigs?status= | Gig[] |
claim(gigId, closerId) | POST /gigs/:id/claim | Gig |
advanceGig(gigId, proposal) | POST /gigs/:id/advance | GigWorkResult |
revive(gigId, note) | POST /gigs/:id/revive | GigWorkResult |
release(gigId, note?) | POST /gigs/:id/release | Gig |
expire(gigId) | POST /gigs/:id/expire | Gig |
Typed errors
NotFoundError— 404ConflictError— 409 (illegal state: claiming a claimed gig, settlement on an open deal)GateRefusedError— 422, with the refusalpayloadattached (gate refusals and Mandate bounds violations)ApiError— the base class for everything else
Injecting fetch
// Both clients accept { fetch } in the constructor, so tests (and the demo
// package) inject an in-process adapter over the server's app.fetch — no ports.
const forsale = new ForSaleClient('http://in-process', { fetch: app.fetch })Exported domain types
Deal, DealStage, PipelineStage, TerminalStage, Mandate, Staffing, Role, Actor, StageProposal, Settlement, ExecutorKind, GigPricing, ActivityEntry — and from the marketplace: Closer, Gig, GigBrief, GigStatus, Payout, TrackRecord.