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

MethodEndpointReturns
createDeal(input)POST /dealsDeal
getDeal(dealId)GET /deals/:idDeal
advance(dealId)POST /deals/:id/advanceDeal | EscalationResult
sell(input)POST /sellDeal | EscalationResult
signal(dealId, signals, note?)POST /deals/:id/signalsDeal
settlement(dealId)GET /deals/:id/settlementSettlement
reengage(dealId)POST /deals/:id/reengageDeal (new, with lineage)

isEscalation(result) narrows an AdvanceResult to EscalationResult{ escalated: true, role, gig, suggestedClosers }.

ClosersClient

MethodEndpointReturns
register(input)POST /closersCloser
listClosers() / closer(id)GET /closers[/:id]Closer[] / Closer
match(query)POST /matchRankedCloser[]
gigs(status?)GET /gigs?status=Gig[]
claim(gigId, closerId)POST /gigs/:id/claimGig
advanceGig(gigId, proposal)POST /gigs/:id/advanceGigWorkResult
revive(gigId, note)POST /gigs/:id/reviveGigWorkResult
release(gigId, note?)POST /gigs/:id/releaseGig
expire(gigId)POST /gigs/:id/expireGig

Typed errors

  • NotFoundError — 404
  • ConflictError — 409 (illegal state: claiming a claimed gig, settlement on an open deal)
  • GateRefusedError — 422, with the refusal payload attached (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.