@buychat/ncp-sdk
v1.0.0
Published
TypeScript SDK for the BuyChat Neural Commerce Protocol (NCP v1). Ed25519-signed agent client for rank, search, negotiate, threads, and conformance endpoints.
Maintainers
Readme
@buychat/ncp-sdk
TypeScript SDK for the BuyChat Neural Commerce Protocol (NCP v1) — the agent-native marketplace layer that powers buychat.ng.
Ed25519-signed client for the full agent surface: rank, search,
negotiate/open, threads, and the conformance harness.
npm install @buychat/ncp-sdk
# or
pnpm add @buychat/ncp-sdkRequires Node >= 20 (uses the global fetch and the built-in node:crypto
Ed25519 primitives). No runtime dependencies.
Quick start
import { readFileSync } from 'node:fs';
import { NcpClient, NcpRateLimitError } from '@buychat/ncp-sdk';
const client = new NcpClient({
baseUrl: 'https://api.buychat.ng',
agentId: 'agt_your_marketplace_id',
privateKeyPem: readFileSync('./agent.pem', 'utf8'),
});
try {
const { ranked } = await client.rank({
domain: 'product',
candidateIds: ['listing_a', 'listing_b'],
query: 'ankara fabric lagos',
});
console.log(ranked);
} catch (err) {
if (err instanceof NcpRateLimitError) {
console.warn(`backoff ${err.retryAfterSeconds}s`);
} else {
throw err;
}
}See examples/rank-and-negotiate.ts for a
full rank → negotiate → thread flow.
Authentication
Every agent endpoint is signed with Ed25519. The SDK constructs three headers per request:
| Header | Value |
| --- | --- |
| NCP-Agent-ID | your marketplace agent id |
| NCP-Timestamp | milliseconds since epoch (±5 minutes tolerance) |
| NCP-Signature | base64 Ed25519 signature over the canonical message |
Canonical message format (matches agent-auth.middleware.ts on the server):
${timestampMs}.${METHOD}.${path}.${sha256(body)}pathis the request pathname with NO query string.bodyis the exact UTF-8 string sent over the wire.undefined,null, or{}→""- strings → passed through verbatim
- everything else →
JSON.stringify(body)
The SDK hashes and signs the same string it sends over the wire — this is the top source of silent 401s in hand-rolled clients.
Error handling
All rejections are typed subclasses of NcpError:
| Class | When |
| --- | --- |
| NcpAuthError | 401 / 403 — signature, clock skew, revoked key |
| NcpRateLimitError | 429 — exposes retryAfterSeconds, limit, remaining |
| NcpKillSwitchError | 503 with X-Kill-Switch-Scope — W27 safety halt |
| NcpValidationError | 400 — request shape violated the schema |
| NcpTransportError | network failure, unparseable body, oversize response |
| NcpError | any other non-2xx |
try {
await client.rank(...);
} catch (err) {
if (err instanceof NcpKillSwitchError) {
// Don't retry. Don't counter. Don't move money.
pauseUntilNextPoll();
}
}Client options
new NcpClient({
baseUrl: 'https://api.buychat.ng', // required, http or https
agentId: 'agt_...', // required for agent endpoints
privateKeyPem: '...', // Ed25519 PKCS8 PEM
bearerToken: '...', // for human-JWT endpoints (rare)
adminToken: '...', // for admin endpoints (ops only)
fetch: customFetch, // inject for Workers / tests
now: () => Date.now(), // override for deterministic tests
defaultHeaders: { 'x-correlation-id': 'trace-123' },
maxResponseBytes: 4 * 1024 * 1024, // 4 MiB default
});If you only need public endpoints (getConformanceCatalogue), you can omit
agentId and privateKeyPem.
Methods
Public (no auth)
| Method | Endpoint |
| --- | --- |
| getConformanceCatalogue() | GET /ncp/v1/conformance |
Agent (Ed25519 required)
| Method | Endpoint |
| --- | --- |
| rank(req) | POST /ncp/v1/rank |
| search(req) | POST /ncp/v1/search |
| openNegotiation(req) | POST /ncp/v1/negotiate/open |
| openThread(req) | POST /ncp/v1/threads |
| postThreadMessage(id, req) | POST /ncp/v1/threads/:id/messages |
Admin (X-Admin-Token or human admin JWT)
| Method | Endpoint |
| --- | --- |
| runConformance(req) | POST /ncp/v1/conformance/run |
Low-level helpers
If you want your own HTTP layer (e.g. Cloudflare Workers without Node crypto), pull in the pure helpers:
import { canonicalMessage, bodyHashHex, buildBodyString, signRequest } from '@buychat/ncp-sdk';
const body = buildBodyString({ domain: 'product', candidateIds: ['x'] });
const headers = signRequest({
agentId, privateKeyPem, method: 'POST',
path: '/ncp/v1/rank', body, timestampMs: Date.now(),
});
// ... fire via your runtime's fetchcanonicalMessage and bodyHashHex are byte-for-byte identical to the
server's. A round-trip test in src/__tests__/sign.test.ts verifies against
Node's crypto.verify to catch any drift.
Versioning
The package version tracks the contract version. NCP v1 → @buychat/[email protected].
Breaking protocol changes ship as a new major.
License
MIT © BuyChat
