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-pee
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-pee
A modular TypeScript SDK for Unicity wallet operations (Unicity state transition network).
ConnectClient / ConnectHost (browser extension + popup)npm install @unicitylabs/sphere-sdk
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 |
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.
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.
…
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 |
StoragePort/DeliveryPort (modules/payments-v2/ports.ts) have wallet-api implementations; the paymentsV2Transport seam in the walletApi config lets tests/custom hosts inject a whole replacement bundle.network placement. Required on createBrowserProviders/createNodeProviders, in the walletApi config, AND on Sphere.init — the three must agree. Sphere.init resolves the payments composition and the token registry from its own network, so omitting it or letting it disagree with walletApi.network throws INVALID_CONFIG before any storage write.For manual/advanced provider wiring, see Custom Providers Configuration. For the deeper integration guide, see docs/INTEGRATION.md.
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).
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.
send() on CERTIFICATION_UNCONFIRMED. A fresh send() mints a new transferId on a different source, so the original resumes and the retry sends → the recipient is double-paid. Treat it as "sent, pending confirmation."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
}
}
sphere.paymentsV2The deprecated sphere.paymentsV2 alias and the paymentsV2: true init flag are removed in
0.15.0. sphere.payments is the only accessor, and it is the same facade the alias returned.
One behavioural difference matters: while no vertical is running (init in flight, mid
address-switch, destroyed) the alias returned null and sphere.payments throws
SphereError with code: 'NOT_INITIALIZED'. Call sites that leaned on the nullish alias —
sphere.paymentsV2?.tokens(), ?? fallback, if (sphere.paymentsV2) as a readiness probe —
silently degraded to "no payments" before and now throw, so catch NOT_INITIALIZED where you
used to check for null. Code that runs after await Sphere.init(…) and before destroy() —
everything else in this README — reads sphere.payments directly.
The accounting: / swap: options are not part of this cleanup: they still throw a typed
INVALID_CONFIG, deliberately, because 0.15.0 is the release where consumers re-integrate.
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 |
gateway.mainnet.unicity.network (v3) | nostr-relay.testnet.unicity.network (shared until mainnet has its own) |
Live networks are testnet2 and mainnet.
testnetis an alias of testnet2 (network id 4, taken from the trust base; own testnet2 token registry);mainnetis network id 1. The v1 network is discontinued — the oldgoggregator-testtestnet spoke the removed v1 protocol, and thedevnetwork that aliased its trust base has been removed along with every other v1 pointer. Mainnet has no wallet-api deployment yet, so its money path is not reachable even though the chain and gateway are live. The transfer wire payload is the finished token blob — the base SDK's ownToken.toCBOR()bytes, with no sphere envelope around them — deposited into the recipient's wallet-api mailbox.The network name (testnet2) and the base-SDK major (3.x since 0.15.0) are separate axes: testnet2 is still testnet2 after the 3.0.1 bump. What the bump changes is the bytes on that network — a gateway serving the v3 protocol accepts nothing a 2.x client writes, and vice versa.
// Use testnet for all services
const providers = createBrowserProviders({ network: 'testnet' });
// Override specific services while using network preset
const providers = createBrowserProviders({
network: 'testnet',
oracle: { url: 'https://custom-gateway.example.com' }, // custom testnet2 gateway
});
The SDK bundles no default API key. Pass the gateway key via oracle: { apiKey } — without it, gateway requests are unauthenticated and money movement on testnet2 fails.
const providers = createBrowserProviders({
network: 'testnet',
oracle: { apiKey: 'sk_...' },
});
The testnet2 key is not a secret — it is published in .env.example and safe to keep in docs and client code. A mainnet key, by contrast, IS a secret: keep it in your deploy environment only.
The testnet preset wires most of these automatically — you only pass network, oracle.apiKey, and the wallet-api baseUrl. The full set, for reference and manual wiring:
| What | Value |
|---|---|
| Network | testnet (alias testnet2), networkId 4 |
| Aggregator / gateway (token engine) | https://gateway.testnet2.unicity.network |
| Aggregator API key (public — not a secret) | sk_ddc3cfcc001e4a28ac3fad7407f99590 |
| wallet-api (delivery + token storage) | https://wallet-api.unicity.network |
| Nostr relay (messaging / nametags) | wss://nostr-relay.testnet.unicity.network |
| Group-chat relay (NIP-29) | wss://sphere-relay.unicity.network |
| Token registry | https://raw.githubusercontent.com/unicitynetwork/unicity-ids/refs/heads/main/unicity-ids.testnet2.json |
The aggregator key above is the testnet2 key only and is safe in client code; a mainnet key is a real secret and must never be committed.
Enable fiat price display by adding a price config. Currently supports CoinGecko API (free and pro tiers).
…
Without price config, the price fields in assets() are null. All other functionality works normally.
You can also set the price provider after initialization — price is a composition-time property of the payments vertical, so verticals composed after the call (the next address switch) pick it up:
import { createPriceProvider } from '@unicitylabs/sphere-sdk';
sphere.setPriceProvider(createPriceProvider({
platform: 'coingecko',
apiKey: 'CG-xxx',
}));
There is no faucet. On testnet you top up your wallet by self-minting fungible tokens via the t
No open issues yet, or sync has not completed.