Read-only TypeScript client for the Signet API. Signet is a verifiable developer career record built on Stellar/Soroban: developers bind a deployment wallet to a handle on-chain, an indexer collects everything that wallet has deployed and invoked, and the result is served as a public profile. This SDK fetches those profiles over plain HTTP GET so an integrator can render someone's contract history without running a Signet deployment, speaking tRPC, or adding a client library.
pnpm add @signet/sdk # npm install @signet/sdk / yarn add @signet/sdkNot on npm yet.
packages/sdkis still"private": trueinside this monorepo and is consumed as"@signet/sdk": "workspace:*". Publishing is tracked in #60; until it lands, use the workspace dependency or a git dependency on this repo.
The SDK talks to a Signet deployment. There is no hosted public deployment yet, so
baseUrl is a required option — point it at a local server:
git clone https://github.com/blockchain-maxis/signet && cd signet
pnpm install
pnpm --filter @signet/web dev # http://localhost:3000, no database requiredThen, in another terminal (quickstart.ts):
import { SignetClient } from '@signet/sdk';
const signet = new SignetClient({ baseUrl: 'http://localhost:3000' });
const handles = await signet.listHandles();
console.log(handles); // [ 'aquawolf', 'sorobuilder', 'stellardev' ]
const profile = await signet.getProfile('aquawolf');
if (!profile) throw new Error('handle not found');
console.log(profile.profile.name); // 'Aqua Wolf'
console.log(profile.profile.wallet); // 'GASAAEJC…' — the bound Stellar account
console.log(profile.stats); // { invocations, uniqueFunctions, reputation }node --experimental-strip-types quickstart.tsThe demo handles above are served from static testnet fixtures with synthetic data.
A deployment with DATABASE_URL set and the indexer running serves real indexed activity
through the same procedures and the same response shapes.
Everything below is exported from the package root.
| Option | Type | Default | Notes |
|---|---|---|---|
baseUrl |
string |
(required) | Origin of a Signet deployment, e.g. http://localhost:3000. There is no hosted public deployment yet, so this has no default. A trailing slash is stripped, so https://x.dev/ and https://x.dev behave identically. The SDK appends /api/trpc/… itself — don't include a path. |
fetch |
typeof fetch |
globalThis.fetch |
Override for tests, proxies, or runtimes without a global fetch. |
Throws Error('[signet] no fetch implementation available; pass options.fetch') from
the constructor when no fetch was passed and the runtime has no globalThis.fetch
(Node 17 and older). This is the only error the SDK throws on its own.
const signet = new SignetClient({
baseUrl: 'https://signet.example.com',
fetch: myInstrumentedFetch,
});| Parameters | handle: Handle (string) — must match /^[a-z0-9_-]{1,32}$/; the server lowercases before validating. |
| Returns | Promise<ProfileResponse | null> |
| Requests | GET {baseUrl}/api/trpc/profile.byHandle?input={"handle":"…"} |
Resolves to null — never throws — when the handle is unknown, when the handle fails
server-side validation, when the response status is not 2xx (including a rate-limit
rejection), and when the tRPC envelope carries no result.data. If you need to tell
"not found" apart from "the deployment is down", check the deployment's /api/health
endpoint separately.
const res = await signet.getProfile('aquawolf');
// {
// handle: 'aquawolf',
// profile: { handle, name, bio, wallet, joined },
// stats: { invocations, uniqueFunctions, reputation },
// }The server also returns a raw
operationsarray alongside these fields. It is deliberately not part ofProfileResponse— its shape is a Horizon payload that is not yet a stable public contract, so don't rely on it through the SDK's types.
| Parameters | none |
| Returns | Promise<Handle[]> — [] on any failure, never null, never throws. |
| Requests | GET {baseUrl}/api/trpc/profile.list |
Lists every handle the deployment can serve: the curated static manifest, or the on-chain-bound handles once a database is configured.
| Parameters | handle: Handle |
| Returns | Promise<RegistryEntry | null> — null if the handle isn't bound. |
| Requests | GET {baseUrl}/api/trpc/registry.resolve?input={"handle":"…"} |
Resolves a handle to its on-chain-bound wallet address via the Identity Registry.
| Parameters | wallet: string — a Stellar G… account address. |
| Returns | Promise<RegistryEntry | null> — null if the wallet has no bound handle. |
| Requests | GET {baseUrl}/api/trpc/registry.lookup?input={"wallet":"…"} |
Reverse lookup of resolveHandle: given a wallet, find the handle bound to it.
| Parameters | none |
| Returns | Promise<RegistryCount> — { count: 0 } on any failure, never throws. |
| Requests | GET {baseUrl}/api/trpc/registry.count |
Total number of handle ↔ wallet bindings currently on the Identity Registry.
Re-exported from @signet/types, so integrators depend on @signet/sdk alone. This is
a deliberately curated subset — only the types that appear in the signatures above — not
everything @signet/types exports; see src/types.ts for why.
type Handle = string;
/** A Stellar account or contract address (G… / C…). */
type StellarAddress = string;
interface SignetProfile {
handle: Handle;
name: string;
bio: string;
wallet: StellarAddress; // the on-chain-bound Stellar account
joined: string;
}
interface ProfileStats {
invocations: number;
uniqueFunctions: number;
reputation: number; // 0–100 heuristic score derived from observed activity
}
interface ProfileResponse {
handle: Handle;
profile: SignetProfile;
stats: ProfileStats;
}
/** A single handle ↔ wallet binding from the on-chain registry. */
interface RegistryEntry {
handle: Handle;
wallet: StellarAddress;
}
interface RegistryCount {
count: number;
}
interface SignetClientOptions {
baseUrl?: string;
fetch?: typeof fetch;
}The client is intentionally total: every method resolves to a value (null / [])
rather than rejecting, so a dead deployment degrades a page instead of crashing it.
Network-level failures from the underlying fetch (DNS, TLS, aborted request) still
reject — wrap calls in try/catch if you need to handle those.
Deployments rate-limit each caller IP to 60 requests per minute per procedure
(fixed window, in-memory per instance). Over the limit the API responds with an error
status, which the SDK surfaces as null / [] — indistinguishable from "not found", so
cache results client-side rather than fetching per render.
- Node.js 22+ — the package is ESM-only (
"type": "module") and relies on the globalfetch. On older Node, passoptions.fetch(e.g.undici). It currently ships TypeScript sources (main/types→src/index.ts), so a Node consumer needs a TypeScript-aware loader such asnode --experimental-strip-types,tsx, or a bundler. - Browsers — works in any browser with
fetch. Requests are same-origin-agnostic plainGETs, so a cross-originbaseUrlneeds CORS enabled on that deployment. - No runtime dependencies. The only dependency,
@signet/types, is types-only and erases at build time.
options.fetch is the injection point — no network needed:
const client = new SignetClient({
baseUrl: 'http://localhost:3000',
fetch: async () => ({ ok: true, json: async () => ({ result: { data: fixture } }) }) as Response,
});See src/client.test.ts for the full pattern. Run the suite with
pnpm --filter @signet/sdk test.
- Repository: https://github.com/blockchain-maxis/signet
- Architecture and data flows:
ARCHITECTURE.md - API surface (tRPC router):
apps/web/lib/server/trpc.ts