@cred-repo/sdk
v1.1.1
Published
Node.js SDK for the Cred Platform API — events, commitments, actors, and credibility.
Readme
@cred-repo/sdk
TypeScript SDK for the Cred Platform API. Build credibility-tracked prediction apps.
Quickstart
import { createCredClient } from "@cred-repo/sdk";
// apiKey MUST be an app API key (prefix: cr_live_).
// CLI login tokens (cpat_*) and OAuth tokens (oa_live_*) will NOT work.
const cred = createCredClient({
baseUrl: "https://cred-repo.com",
apiKey: process.env.CRED_API_KEY!, // cr_live_...
});
// 1. Create an actor (idempotent by externalUserId)
const actor = await cred.createOrGetActor({
externalUserId: "user-123",
handle: "alice",
});
// 2. Ensure an event exists (key = shared question, NEVER include userId)
await cred.ensureEvent({
key: "weather:rain:chicago:2026-04-01",
title: "Will it rain in Chicago?",
resolutionConfigSlug: "weather.rain",
locksAt: "2026-04-01T00:00:00Z",
overrideParams: { date: "2026-04-01" },
});
// 3. Submit a commitment (idempotent upsert — updates if exists)
const commitment = await cred.submitCommitment({
actorId: actor.actorId,
eventKey: "weather:rain:chicago:2026-04-01",
probability: 0.75,
resolutionDate: "2026-04-01",
question: "Will it rain in Chicago on April 1?",
});
// 4. Read commitments
const result = await cred.getCommitments({
actorId: actor.actorId,
resolved: true,
});
// 5. Check resolution status
const status = await cred.getResolutionStatus("weather:rain:chicago:2026-04-01");
// status.state: "pending" | "resolved"That's the complete integration path. Every first-party app (NBACred, WeatherCred, PolyCred) uses exactly this flow.
Visibility (v1.1.0)
// Submit with visibility tier (defaults to "public" if omitted)
const commitment = await cred.submitCommitment({
actorId: actor.actorId,
eventKey: "weather:rain:chicago:2026-04-01",
probability: 0.75,
resolutionDate: "2026-04-01",
question: "Will it rain in Chicago on April 1?",
visibility: "restricted", // "restricted" | "public"
});
// Promote visibility (can only expand, never contract)
await cred.promoteCommitment({
commitmentId: commitment.commitmentId,
visibility: "public",
});Tiers: restricted (actor + admins only), public (external, verifiable). Visibility is set once at creation and can only expand via promote. Non-public commitments are not yet enabled in production.
Scoring: Only commitments with visibility_at_resolution = 'public' enter public calibration. Promoting after resolution does not retroactively change scoring.
Commitment Semantics
These are platform-enforced behaviors, not SDK opinions.
- Uniqueness: One commitment per actor per event, keyed on
(actor_id, canonical_event_id). - Behavior: Last-write-wins upsert. Resubmitting updates probability and question. Returns the same
commitmentId. - History: Not retained at the platform level. Previous probabilities are overwritten. Apps that need amendment history must track locally.
- Guarantee: Idempotent. Safe to retry on network failure. Calling
submitCommitmenttwice with the same parameters is a no-op. - Scoring:
brierScore,isCorrect,predictedSideare computed by the platform and returned in the API response. Do not recompute these locally.
Error Handling
import { isCredError } from "@cred-repo/sdk";
try {
await cred.submitCommitment({ ... });
} catch (err) {
if (isCredError(err)) {
switch (err.code) {
case "CRED_EVENT_LOCKED":
// Prediction window closed
break;
case "CRED_ALREADY_RESOLVED":
// Event already has an outcome
break;
case "CRED_EVENT_CONFLICT":
// Event exists with different immutable fields
break;
case "CRED_UNAUTHORIZED":
// Invalid API key
break;
}
}
}Never match on err.message. Always use err.code.
Webhook Verification
import { verifyWebhookSignature } from "@cred-repo/sdk";
// In your webhook handler:
const body = await request.text();
verifyWebhookSignature({
rawBody: body,
signatureHeader: request.headers.get("webhook-signature"),
secret: process.env.CRED_WEBHOOK_SECRET!,
webhookId: request.headers.get("webhook-id")!,
timestamp: request.headers.get("webhook-timestamp")!,
});
// If it doesn't throw, the signature is valid.
const payload = JSON.parse(body);The SDK handles signing key derivation, HMAC computation, and timestamp tolerance internally. Pass the raw webhook secret — do not pre-hash it.
Real App Patterns
Three production apps use this SDK. Each represents a different integration shape.
Pattern 1: User Prediction App (NBACred)
User submits predictions on NBA games. Resolution via external cron.
- Actor: created at OAuth login, cached locally
- Events: created per prediction (one per team per game)
- Commitments: user-initiated, one at a time
- Resolution: webhook from Cred resolver
- Key format:
nba:winner:{gameId}-{team}:{date}
Pattern 2: Scheduled Data App (WeatherCred)
Cron creates daily events. NOAA reference forecaster submits baselines.
- Actor: human (users) + reference (NOAA)
- Events: batch-created by nightly cron (6 cities x 7 days)
- Commitments: user-initiated + NOAA reference (cron)
- Resolution: webhook from Cred resolver
- Key format:
weather:rain:{city}:{date} - Reference actor:
cred.createOrGetActor({ actorType: "reference" })
Pattern 3: System-Driven Predictions (PolyCred)
AI models forecast hundreds of prediction markets. High volume, continuous ingestion.
- Actors: human (users) + AI agents (4 LLM providers)
- Events: created per market (500+ active)
- Commitments: user-initiated + AI cron (batch of 10, every 5 min)
- Resolution: polling via raw
getResolvedEvents()(not yet in SDK) - Key format:
polymarket:{conditionId}:{slug}:{date}orkalshi:{ticker}:{date} - AI actors:
cred.createOrGetActor({ actorType: "ai_agent" })
What the SDK Does NOT Do
Be explicit about boundaries:
- Does not build event keys. Each app owns its key format. The SDK validates nothing about key structure beyond "non-empty string." Event keys must represent shared questions (e.g.,
stockcred:close:AAPL:2026-04-01), never per-user state. Including userId in a key creates one event per user, destroying crowd aggregation and calibration. - Does not handle OAuth or sessions. Identity is an app concern. The SDK provides
createOrGetActorfor the data layer only. - Does not provide analytics or aggregation endpoints. Crowd averages, credibility scores, and leaderboard data are app-specific reads.
- Does not batch requests. Rate-aware batching belongs in the app layer. The SDK makes one call at a time.
- Does not cache results. The SDK returns fresh data on every call. Apps cache locally as needed.
- Does not manage your database. Local caching of
cred_actor_id,cred_commitment_id, outcomes, and Brier scores is the app's responsibility.
Common Mistakes
Based on migrating three production apps:
- Don't use CLI tokens with the SDK. The SDK requires an app API key (
cr_live_*). CLI login tokens (cpat_*) will return 401 on all write operations. - Don't use a config from a different domain.
weather.rainonly works withweather:*keys.nba.game_winneronly works withnba:*keys. For new domains, usemanual.binarywhich accepts any key format. - Don't put the config slug in your event key. The key should be
stockcred:close:AAPL:2026-04-01, NOTmanual:binary:stockcred:2026-04-01. The config slug goes inresolutionConfigSlug, never in the key itself. - Don't put userId in event keys. Events are shared questions (
stockcred:close:AAPL:2026-04-01). Actors differentiate users. A per-user key creates isolated single-commitment events — no crowd, no calibration, no leaderboard value. - Don't implement your own retry logic. The SDK retries transient failures (5xx, network errors) with exponential backoff. Customize via
maxRetriesandretryBackoffMsin client options. - Don't manually construct auth headers. Pass
apiKeytocreateCredClientand forget about it. - Don't compute Brier scores locally. The API returns authoritative
evaluation.brierScoreandevaluation.isCorrecton every commitment. Trust them. - Don't string-match error messages. Use
isCredError(err)and checkerr.code. Error messages may change; codes are stable. - Don't assume append-only. Commitments are last-write-wins upsert. Submitting again updates the existing commitment, not creates a new one.
- Don't deduplicate commitments in your app. The platform handles it. Your app's "amend" flow is just another
submitCommitmentcall.
Config Discovery
const configs = await cred.listConfigs();
// Returns active resolution configs with slugs, preconditions, and timing.Use this to discover available resolutionConfigSlug values instead of hardcoding.
Critical: Config ↔ Event Key Coupling
Resolution configs are not interchangeable. Each config expects a specific event key format and domain. Using the wrong config for your key format will fail with wc_canonical_key_format_check or namespace_not_allowed.
// ✅ Correct — weather config with weather-style key
await cred.ensureEvent({
key: "weather:rain:chicago:2026-04-01",
resolutionConfigSlug: "weather.rain", // matches weather:* keys
...
});
// ❌ Wrong — weather config with stock-style key
await cred.ensureEvent({
key: "stockcred:close:AAPL:2026-04-01",
resolutionConfigSlug: "weather.rain", // FAILS: key format mismatch
...
});If you are building a new app domain (stocks, sports, etc.), use manual.binary — the universal config that works with any domain and any key format.
Pattern 4: Custom Domain App (manual.binary)
For new domains where no automated connector exists yet. Your app resolves events itself.
// 1. Use manual.binary — works with ANY key format
await cred.ensureEvent({
key: "stockcred:close:AAPL:2026-04-01", // YOUR domain, YOUR format
title: "Will AAPL close above $150 on April 1?",
resolutionConfigSlug: "manual.binary", // Universal config
locksAt: "2026-04-01T16:00:00Z",
});
// 2. Submit commitments as normal
await cred.submitCommitment({
actorId: actor.actorId,
eventKey: "stockcred:close:AAPL:2026-04-01",
probability: 0.65,
resolutionDate: "2026-04-01",
question: "Will AAPL close above $150?",
});
// 3. Resolve with structured evidence (requires resolver token)
await fetch(`${baseUrl}/api/v1/events/stockcred:close:AAPL:2026-04-01/resolve`, {
method: "POST",
headers: {
Authorization: `Bearer ${resolverToken}`, // resolver credential, not app key
"Content-Type": "application/json",
},
body: JSON.stringify({
outcome: true,
evidence_type: "app_reported", // REQUIRED
evidence_payload: { // REQUIRED — no bare { outcome: true }
source: "stockcred",
symbol: "AAPL",
close_price: 152.30,
threshold: 150.00,
data_source: "yahoo_finance",
},
}),
});Key format rules for manual.binary:
- Use YOUR domain prefix:
stockcred:close:AAPL:2026-04-01, NOTmanual:binary:stockcred:2026-04-01 - The config slug (
manual.binary) goes inresolutionConfigSlug, never in the key - Keys must be shared questions — one event per real-world question, not per user
- Format:
{domain}:{metric}:{entity}:{YYYY-MM-DD}
Trust tier: Manual resolution is labeled evidence_class: "app_reported" on all surfaces. This distinguishes it from connector-verified resolution (public_api, first_party_source). Evidence is still hashed, immutable, and stored in the receipt — but it is not independently verifiable by a third party.
Resolution is Not a Simple Outcome Write
The SDK does not expose a resolve() method. Resolution is handled by the platform's resolver service, which:
- Fetches evidence from external data sources
- Verifies preconditions (e.g., game is final, market is closed)
- Computes evidence hashes for tamper detection
- Scores all commitments atomically
If you need custom resolution for a new domain, you need a resolver credential (cr_live_* with credential_type: resolver) and must POST structured evidence to /api/v1/events/{key}/resolve with evidence_type and evidence_payload fields. This is an advanced integration — most apps should rely on platform-managed resolution via webhooks or polling.
Resolution Status
const status = await cred.getResolutionStatus("weather:rain:chicago:2026-04-01");Returns state: "pending" or state: "resolved" with receipt info. v1 is truth-only — intermediate diagnostic states (preconditions, connector status) are not yet exposed.
For polling:
const result = await cred.waitForResolution({
eventKey: "weather:rain:chicago:2026-04-01",
timeoutMs: 60 * 60 * 1000,
});
// Resolves when state reaches a terminal state.
// Throws on timeout or abort.Contract Version
The SDK sends X-Cred-SDK-Version and validates X-Cred-Contract-Version: 1 from the API. If the contract version changes, the SDK warns once at startup.
