Sphere SDK
The SDK for autonomous economic agents. Give an agent an identity, a wallet, and the ability to find, negotiate with, and settle with other agents - peer-to-peer, with perfect privacy and ultra-fast finality
Install / Use
npx skills add unicity-sphere/sphere-sdkInstalls into whichever agent you are using.
README
Sphere SDK
A modular TypeScript SDK for Unicity wallet operations (Unicity state transition network).
Features
- Wallet Management - BIP39/BIP32 key derivation; optional password encryption (PBKDF2)
- Payments - Engine-certified token transfers over the wallet-api vertical (durable server-side intents, mailbox delivery, crash-safe resume under the same transferId); server custody — the backend holds inventory, keys stay local
- Payment Requests - Request payments over the wallet-api rail with encrypted memos and durable settling
- Market (Intents) - Signed intent bulletin board with semantic search and live feed
- Group Chat - NIP-29 relay-based group messaging with moderation
- Messaging (Nostr) - NIP-17 DMs + NIP-29 group chat and nametag publishing — messaging only; not the payment rail
- Multi-Address - HD address derivation (BIP32/BIP44)
- Connect Protocol - dApp ↔ wallet communication via
ConnectClient/ConnectHost(browser extension + popup) - CLI - Comprehensive command-line interface with shell auto-completion
Installation
npm install @unicitylabs/sphere-sdk
Quick Start Guides
Choose your platform:
| Platform | Guide | Required | Optional |
|----------|-------|----------|----------|
| Browser | QUICKSTART-BROWSER.md | SDK only | IndexedDB storage |
| Node.js | QUICKSTART-NODEJS.md | SDK + ws | File storage |
| CLI | @unicity-sphere/cli | Separate package | - |
| dApp integration | CONNECT.md | SDK only | Sphere extension |
CLI (Command Line Interface)
The CLI has moved to a dedicated package: @unicity-sphere/cli.
npm install -g @unicity-sphere/cli
sphere --help
See docs/QUICKSTART-CLI.md for the full command reference.
Quick Start
Setup is two provider layers, not one.
createBrowserProviders/createNodeProvidersbuild only the base (storage + transport + oracle). You must then attach the wallet-api transport config withcreateWalletApiProviders— money moves only through the wallet-api vertical. Skipping it fails loudly:Sphere.initthrowsINVALID_CONFIG.
import { Sphere } from '@unicitylabs/sphere-sdk';
import { createBrowserProviders } from '@unicitylabs/sphere-sdk/impl/browser';
import { createWalletApiProviders } from '@unicitylabs/sphere-sdk/impl/shared/wallet-api';
// 1. Base providers: storage + transport + oracle. `network` is REQUIRED here (no default).
// The testnet2 gateway key is PUBLIC (not a secret); it is required at runtime for send/mint.
const base = createBrowserProviders({
network: 'testnet', // alias of testnet2 (networkId 4)
oracle: { apiKey: 'sk_ddc3cfcc001e4a28ac3fad7407f99590' }, // public testnet2 key
});
// 2. Attach the wallet-api transport config. Returns { ...base, walletApi } —
// a plain config object ({ network, baseUrl, deviceId?, ... }) that Sphere.init consumes.
const providers = createWalletApiProviders(base, {
baseUrl: 'https://wallet-api.unicity.network', // your wallet-api deployment (testnet2)
network: 'testnet2',
deviceId: 'my-stable-device-id', // persist this to avoid re-auth each launch
});
// 3. Init the wallet (auto-creates one if none exists).
const { sphere, created, generatedMnemonic } = await Sphere.init({
...providers,
autoGenerate: true,
});
if (created && generatedMnemonic) {
console.log('SAVE THIS RECOVERY PHRASE:', generatedMnemonic);
}
// 4. Send — engine-driven, certified on-chain. The recipient needs a published identity
// (chain pubkey), e.g. a registered Unicity ID; otherwise send fails with INVALID_RECIPIENT.
const result = await sphere.payments.send({
recipient: '@alice',
amount: '1000000', // decimal STRING — never a JS number
coinId: 'UCT', // a symbol auto-resolves to its hex coinId
memo: 'hello',
});
console.log(result.status); // 'completed'
// result.deliveryPending === true is NORMAL, not a failure: the token is certified on-chain but
// the recipient's mailbox delivery was deferred and will land on retry (see "Send result" below).
// 5. Receive — incoming transfers land automatically while the wallet runs (mailbox drain +
// wake socket). To drain explicitly (e.g. a CLI/batch app), call receive():
const { transfers } = await sphere.payments.receive();
sphere.on('transfer:incoming', (t) => console.log('received from', t.senderNametag));
console.log(await sphere.payments.assets());
What just happened (the provider model)
A wallet is composed from swappable ports, layered in two steps:
| Layer | Built by | What it supplies |
|-------|----------|-------------------|
| Base | createBrowserProviders / createNodeProviders | storage (keys/identity/journals), transport (Nostr — messaging/nametags only), oracle (gateway/trust base) |
| wallet-api transport | createWalletApiProviders(base, …) | walletApi — the transport CONFIG ({ network, baseUrl, deviceId?, fetchFn?, webSocketFactory?, paymentsV2Transport? }) the payments vertical is composed from |
- The rail is wallet-api, not Nostr. Transfers are certified on-chain by the token engine and the finished token is deposited into the recipient's wallet-api mailbox. Nostr carries messaging/nametags — it does not move payments.
- Custody is server-side. The wallet-api backend holds your token inventory; your keys never leave the client. (Own-storage custody was rescinded — there is no local token store.)
- The money ports are contract-enforced.
StoragePort/DeliveryPort(modules/payments-v2/ports.ts) have wallet-api implementations; thepaymentsV2Transportseam in thewalletApiconfig lets tests/custom hosts inject a whole replacement bundle. networkplacement. Required oncreateBrowserProviders/createNodeProvidersAND in thewalletApiconfig (init throwsINVALID_CONFIGwithout either); optional/informational onSphere.init.
For manual/advanced provider wiring, see Custom Providers Configuration. For the deeper integration guide, see docs/INTEGRATION.md.
Send result (TransferResult)
send() resolves with a TransferResult:
| Field | Meaning |
|-------|---------|
| status | 'completed' on success. ('pending' \| 'submitted' \| 'confirmed' \| 'delivered' \| 'failed' also exist for in-flight/terminal states.) |
| deliveryPending | true when the spend is certified on-chain but the recipient's mailbox delivery was deferred (a full inbox / transient outage). This is success, not failure — the token is finalized and the finished blob is journaled and re-delivered automatically. |
| deliveryState | 'landed' (delivered) or 'pending-delivery' (deferred, as above). |
Treat status === 'completed' as sent. Use deliveryPending only to show a "delivery pending" hint — never as an error. A stale-but-spent source is self-healed (the next live coin is selected automatically).
Handling send() rejections — CERTIFICATION_UNCONFIRMED is NOT re-sendable (money-safety)
send() throws for genuine failures (INVALID_RECIPIENT, insufficient balance, a TransferConflictError lost race) and for one indeterminate case you must handle specially: a ProofUnconfirmedError (code: 'CERTIFICATION_UNCONFIRMED', mayHaveCertified: true). It means the spend may already be on-chain but the proof fetch was inconclusive — the SDK keeps the intent open and completes it later under the same transferId.
- ⚠️ Never re-issue
send()onCERTIFICATION_UNCONFIRMED. A freshsend()mints a newtransferIdon a different source, so the original resumes and the retry sends → the recipient is double-paid. Treat it as "sent, pending confirmation." - Recovery is automatic. The open intent is replayed under the same
transferId(recovers the proof + delivery, or records the spend if a rival tx won; never a second spend): partially-committed outcomes converge in-process, and every remaining open intent is resumed when the vertical starts (Sphere.init/Sphere.load/ an address switch). There is no public resume API to call.
import { isSphereError } from '@unicitylabs/sphere-sdk';
try {
const result = await sphere.payments.send({ recipient: '@bob', amount, coinId });
// result.status === 'completed' (or result.deliveryPending === true) → sent
} catch (err) {
if (isSphereError(err) && err.code === 'CERTIFICATION_UNCONFIRMED') {
// Possibly already sent on-chain — DO NOT re-send. Resume finishes it.
} else {
// genuine failure — safe to surface to the user / retry
}
}
Network Configuration
The SDK ships network presets that configure all services automatically. network is required — there is no default:
| Network | Aggregator (gateway) | Nostr Relay |
|---------|----------------------|-------------|
| testnet | gateway.testnet2.unicity.network (v2) | nostr-relay.testnet.unicity.network |
| testnet2 | alias of testnet (same configuration) | nostr-relay.testnet.unicity.network |
| mainnet | aggregator.unicity.network (v1-era) | relay.unicity.network (+ public relays) |
| dev | dev-aggregator.dyndns.org (v1-era) | nostr-relay.testnet.unicity.network |
v1 → v2 cutover:
testnetnow points at testnet2, the v2 state-transition gateway network (network id 4, taken from the trust base; own testnet2 token registry). The oldgoggregator-testtestnet spoke the removed v1 protocol and is gone.mainnetanddevstill point at v1-era aggregators — wallet operations that move money (send,mint) fail loudly (AGGREGATOR_ERROR) on those networks until
Related Skills
node-connect
385.5kDiagnose OpenClaw Android, iOS, or macOS node pairing, QR/setup code, route, auth, and connection failures.
blender-python-addon
40.5kBlender Python add-on rules for operators, panels, properties, registration, testing, and API-safe scripting
flutter-development-guidelines-cursorrules-prompt-file
40.5kCursor rules for Flutter development with MVVM architecture, Riverpod state management, Material widgets, and Dart style guidelines.
commit-push-pr
140.7kCommit, push, and open a PR
