unicity-sphere/sphere-sdkPublic

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

AI summary: Modular TypeScript SDK for Unicity wallet operations, enabling peer-to-peer agent settlements and state transitions.

Stars
5.4K
+-1 today
Forks
107
Watchers
33
Open issues
121
Open PRs
4
Contributors
~7
Commits
999
Branches
355

TypeScriptMITCreated Jan 27, 2026Last push 1d agoLatest release v0.8.0+-3 stars this week+-11 this month

Quick answers

What is sphere-sdk?
Modular TypeScript SDK for Unicity wallet operations, enabling peer-to-peer agent settlements and state transitions.
What does sphere-sdk do?
The Sphere SDK is the official TypeScript client for interacting securely with the Unicity state transition network. It provides developers with robust tools to manage BIP39/BIP32 wallets, orchestrate encrypted token transfers, and handle self-healing coin selection. The SDK utilizes a sophisticated dual-layer provider model, integrating a base storage and transport layer closely with a durable wallet-api delivery rails system. By wrapping complex, engine-certified token operations into highly familiar TypeScript interfaces, it allows applications to facilitate peer-to-peer economic transactions with absolute privacy and ultra-fast finality.
Who is sphere-sdk for?
TypeScript developers and AI engineers building autonomous economic agents or decentralized applications. It requires a firm understanding of cryptographic wallets and peer-to-peer network concepts.
How do I get started with sphere-sdk?
npm install @unicitylabs/sphere-sdk
How popular is sphere-sdk on GitHub?
unicity-sphere/sphere-sdk has 5,387 stars and 107 forks on GitHub, and gained -3 stars in the last 7 days.
What license does sphere-sdk use?
unicity-sphere/sphere-sdk is released under the MIT license.

Star history

since Jul 29, 2026
02K4KJul 2026Aug 2026Sep 2026Oct 2026
5.4K stars as of Oct 3, 2026. Measured daily since Jul 29, 2026; GitHub no longer exposes earlier star timestamps.

Contribution activity

commits per day, last 52 weeks
OctNovDecJanFebMarAprMayJunJulAugSepMonWedFri2025-10-04: 0 commits2025-10-05: 0 commits2025-10-06: 0 commits2025-10-07: 0 commits2025-10-08: 0 commits2025-10-09: 0 commits2025-10-10: 0 commits2025-10-11: 0 commits2025-10-12: 0 commits2025-10-13: 0 commits2025-10-14: 0 commits2025-10-15: 0 commits2025-10-16: 0 commits2025-10-17: 0 commits2025-10-18: 0 commits2025-10-19: 0 commits2025-10-20: 0 commits2025-10-21: 0 commits2025-10-22: 0 commits2025-10-23: 0 commits2025-10-24: 0 commits2025-10-25: 0 commits2025-10-26: 0 commits2025-10-27: 0 commits2025-10-28: 0 commits2025-10-29: 0 commits2025-10-30: 0 commits2025-10-31: 0 commits2025-11-01: 0 commits2025-11-02: 0 commits2025-11-03: 0 commits2025-11-04: 0 commits2025-11-05: 0 commits2025-11-06: 0 commits2025-11-07: 0 commits2025-11-09: 0 commits2025-11-10: 0 commits2025-11-11: 0 commits2025-11-12: 0 commits2025-11-13: 0 commits2025-11-14: 0 commits2025-11-15: 0 commits2025-11-16: 0 commits2025-11-17: 0 commits2025-11-18: 0 commits2025-11-19: 0 commits2025-11-20: 0 commits2025-11-21: 0 commits2025-11-22: 0 commits2025-11-23: 0 commits2025-11-24: 0 commits2025-11-25: 0 commits2025-11-26: 0 commits2025-11-27: 0 commits2025-11-28: 0 commits2025-11-29: 0 commits2025-11-30: 0 commits2025-12-01: 0 commits2025-12-02: 0 commits2025-12-03: 0 commits2025-12-04: 0 commits2025-12-05: 0 commits2025-12-06: 0 commits2025-12-07: 0 commits2025-12-08: 0 commits2025-12-09: 0 commits2025-12-10: 0 commits2025-12-11: 0 commits2025-12-12: 0 commits2025-12-13: 0 commits2025-12-14: 0 commits2025-12-15: 0 commits2025-12-16: 0 commits2025-12-17: 0 commits2025-12-18: 0 commits2025-12-19: 0 commits2025-12-20: 0 commits2025-12-21: 0 commits2025-12-22: 0 commits2025-12-23: 0 commits2025-12-24: 0 commits2025-12-25: 0 commits2025-12-26: 0 commits2025-12-27: 0 commits2025-12-28: 0 commits2025-12-29: 0 commits2025-12-30: 0 commits2025-12-31: 0 commits2026-01-01: 0 commits2026-01-02: 0 commits2026-01-03: 0 commits2026-01-04: 0 commits2026-01-05: 0 commits2026-01-06: 0 commits2026-01-07: 0 commits2026-01-08: 0 commits2026-01-09: 0 commits2026-01-10: 0 commits2026-01-11: 0 commits2026-01-12: 0 commits2026-01-13: 0 commits2026-01-14: 0 commits2026-01-15: 0 commits2026-01-16: 0 commits2026-01-17: 0 commits2026-01-18: 0 commits2026-01-19: 0 commits2026-01-20: 0 commits2026-01-21: 0 commits2026-01-22: 0 commits2026-01-23: 0 commits2026-01-24: 0 commits2026-01-25: 0 commits2026-01-26: 0 commits2026-01-27: 14 commits2026-01-28: 30 commits2026-01-29: 10 commits2026-01-30: 0 commits2026-01-31: 0 commits2026-02-01: 0 commits2026-02-02: 10 commits2026-02-03: 15 commits2026-02-04: 20 commits2026-02-05: 17 commits2026-02-06: 4 commits2026-02-07: 3 commits2026-02-08: 1 commit2026-02-09: 12 commits2026-02-10: 27 commits2026-02-11: 39 commits2026-02-12: 20 commits2026-02-13: 21 commits2026-02-14: 11 commits2026-02-15: 16 commits2026-02-16: 16 commits2026-02-17: 5 commits2026-02-18: 0 commits2026-02-19: 8 commits2026-02-20: 3 commits2026-02-21: 0 commits2026-02-22: 0 commits2026-02-23: 0 commits2026-02-24: 0 commits2026-02-25: 5 commits2026-02-26: 7 commits2026-02-27: 0 commits2026-02-28: 9 commits2026-03-01: 5 commits2026-03-02: 2 commits2026-03-03: 7 commits2026-03-04: 11 commits2026-03-05: 0 commits2026-03-06: 0 commits2026-03-07: 3 commits2026-03-08: 0 commits2026-03-09: 0 commits2026-03-10: 3 commits2026-03-11: 0 commits2026-03-12: 3 commits2026-03-13: 4 commits2026-03-14: 1 commit2026-03-15: 3 commits2026-03-16: 0 commits2026-03-17: 4 commits2026-03-18: 6 commits2026-03-19: 1 commit2026-03-20: 2 commits2026-03-21: 1 commit2026-03-22: 0 commits2026-03-23: 0 commits2026-03-24: 3 commits2026-03-25: 0 commits2026-03-26: 0 commits2026-03-27: 0 commits2026-03-28: 0 commits2026-03-29: 2 commits2026-03-30: 9 commits2026-03-31: 0 commits2026-04-01: 0 commits2026-04-02: 0 commits2026-04-03: 0 commits2026-04-04: 0 commits2026-04-05: 0 commits2026-04-06: 7 commits2026-04-07: 0 commits2026-04-08: 0 commits2026-04-09: 0 commits2026-04-10: 0 commits2026-04-11: 0 commits2026-04-12: 0 commits2026-04-13: 0 commits2026-04-14: 0 commits2026-04-15: 0 commits2026-04-16: 0 commits2026-04-17: 8 commits2026-04-18: 0 commits2026-04-19: 0 commits2026-04-20: 0 commits2026-04-21: 0 commits2026-04-22: 0 commits2026-04-23: 0 commits2026-04-24: 0 commits2026-04-25: 0 commits2026-04-26: 0 commits2026-04-27: 0 commits2026-04-28: 0 commits2026-04-29: 0 commits2026-04-30: 0 commits2026-05-01: 0 commits2026-05-02: 0 commits2026-05-03: 0 commits2026-05-04: 4 commits2026-05-05: 0 commits2026-05-06: 0 commits2026-05-07: 2 commits2026-05-08: 7 commits2026-05-09: 0 commits2026-05-10: 0 commits2026-05-11: 0 commits2026-05-12: 8 commits2026-05-13: 3 commits2026-05-14: 0 commits2026-05-15: 0 commits2026-05-16: 0 commits2026-05-17: 0 commits2026-05-18: 0 commits2026-05-19: 0 commits2026-05-20: 0 commits2026-05-21: 0 commits2026-05-22: 0 commits2026-05-23: 0 commits2026-05-24: 0 commits2026-05-25: 0 commits2026-05-26: 0 commits2026-05-27: 0 commits2026-05-28: 0 commits2026-05-29: 0 commits2026-05-30: 0 commits2026-05-31: 0 commits2026-06-01: 0 commits2026-06-02: 0 commits2026-06-03: 0 commits2026-06-04: 16 commits2026-06-05: 16 commits2026-06-06: 17 commits2026-06-07: 8 commits2026-06-08: 13 commits2026-06-09: 6 commits2026-06-10: 31 commits2026-06-11: 14 commits2026-06-12: 12 commits2026-06-13: 0 commits2026-06-14: 0 commits2026-06-15: 9 commits2026-06-16: 23 commits2026-06-17: 2 commits2026-06-18: 7 commits2026-06-19: 3 commits2026-06-20: 0 commits2026-06-21: 0 commits2026-06-22: 0 commits2026-06-23: 0 commits2026-06-24: 4 commits2026-06-25: 2 commits2026-06-26: 11 commits2026-06-27: 0 commits2026-06-28: 0 commits2026-06-29: 0 commits2026-06-30: 2 commits2026-07-01: 0 commits2026-07-02: 0 commits2026-07-03: 6 commits2026-07-04: 0 commits2026-07-05: 4 commits2026-07-06: 0 commits2026-07-07: 0 commits2026-07-08: 2 commits2026-07-09: 6 commits2026-07-10: 3 commits2026-07-11: 2 commits2026-07-12: 2 commits2026-07-13: 0 commits2026-07-14: 2 commits2026-07-15: 12 commits2026-07-16: 2 commits2026-07-17: 1 commit2026-07-18: 2 commits2026-07-19: 4 commits2026-07-20: 4 commits2026-07-21: 0 commits2026-07-22: 0 commits2026-07-23: 2 commits2026-07-24: 0 commits2026-07-25: 0 commits2026-07-26: 0 commits2026-07-27: 5 commits2026-07-28: 11 commits2026-07-29: 10 commits2026-07-30: 8 commits2026-07-31: 5 commits2026-08-01: 13 commits2026-08-02: 0 commits2026-08-03: 0 commits2026-08-04: 7 commits2026-08-05: 14 commits2026-08-06: 17 commits2026-08-07: 6 commits2026-08-08: 0 commits2026-08-09: 2 commits2026-08-10: 4 commits2026-08-11: 6 commits2026-08-12: 3 commits2026-08-13: 1 commit2026-08-14: 1 commit2026-08-15: 0 commits2026-08-16: 0 commits2026-08-17: 0 commits2026-08-18: 2 commits2026-08-19: 0 commits2026-08-20: 0 commits2026-08-21: 1 commit2026-08-22: 0 commits2026-08-23: 0 commits2026-08-24: 0 commits2026-08-25: 2 commits2026-08-26: 0 commits2026-08-27: 1 commit2026-08-28: 2 commits2026-08-29: 0 commits2026-08-30: 0 commits2026-08-31: 0 commits2026-09-01: 1 commit2026-09-02: 1 commit2026-09-03: 2 commits2026-09-04: 1 commit2026-09-05: 0 commits2026-09-06: 0 commits2026-09-07: 0 commits2026-09-08: 0 commits2026-09-09: 0 commits2026-09-10: 0 commits2026-09-11: 5 commits2026-09-12: 0 commits2026-09-13: 0 commits2026-09-14: 0 commits2026-09-15: 4 commits2026-09-16: 0 commits2026-09-17: 1 commit2026-09-18: 4 commits2026-09-19: 7 commits2026-09-20: 0 commits2026-09-21: 7 commits2026-09-22: 3 commits2026-09-23: 0 commits2026-09-24: 2 commits2026-09-25: 0 commits2026-09-26: 0 commits2026-09-27: 0 commits2026-09-28: 0 commits2026-09-29: 0 commits2026-09-30: 1 commit2026-10-01: 0 commits2026-10-02: 0 commits2026-10-03: 0 commits
842 commits in the last yearLessMore

Signals and awards

derived from tracked data
  • Very active

    842 commits in 52 weeks

  • Permissive license

    MIT

  • Continuous integration

    Automated checks passing

What sphere-sdk does

The Sphere SDK is the official TypeScript client for interacting securely with the Unicity state transition network. It provides developers with robust tools to manage BIP39/BIP32 wallets, orchestrate encrypted token transfers, and handle self-healing coin selection. The SDK utilizes a sophisticated dual-layer provider model, integrating a base storage and transport layer closely with a durable wallet-api delivery rails system. By wrapping complex, engine-certified token operations into highly familiar TypeScript interfaces, it allows applications to facilitate peer-to-peer economic transactions with absolute privacy and ultra-fast finality.

TypeScript developers and AI engineers building autonomous economic agents or decentralized applications. It requires a firm understanding of cryptographic wallets and peer-to-peer network concepts.

  • Wallet management: Derives keys securely using standard BIP39/BIP32 and supports optional PBKDF2 password encryption.
  • Engine-certified payments: Ensures concurrent-send safety with a dedicated SpendQueue and robust self-healing coin selection.
  • Dual-layer architecture: Decouples the base storage and transport layer effectively from the durable wallet-api mailbox delivery system.
  • Trustless token swaps: Facilitates peer-to-peer atomic swaps utilizing a secure escrow and direct message-based negotiation protocol.
  • Nostr messaging integration: Implements NIP-17 direct messages and NIP-29 group chats securely for autonomous agent communication.

Where teams use it

Autonomous agent economies

Equipping AI agents with secure identities and financial capabilities to negotiate and settle transactions entirely independently.

Decentralized payment integration

Integrating robust, self-healing payment rails directly into modern dApps without ever relying on traditional centralized banking APIs.

Peer-to-peer atomic trading

Enabling trustless atomic token swaps between distributed users utilizing the built-in escrow and encrypted messaging protocols.

Secure communication networks

Leveraging the decentralized Nostr network to provide highly private, encrypted direct messaging and moderated group chats.

Getting started: npm install @unicitylabs/sphere-sdk

README

main branch

Sphere SDK

A modular TypeScript SDK for Unicity wallet operations (Unicity state transition network).

Features

  • Wallet Management - BIP39/BIP32 key derivation; optional password encryption of the stored seed (see Wallet Security & Encryption)
  • 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 (hosted wallet in an iframe, or WebSocket for Node.js dApps)

Installation

npm install @unicitylabs/sphere-sdk        # browser
npm install @unicitylabs/sphere-sdk ws     # Node.js: ws is required, see "Node.js Providers"

Quick Start Guides

Choose your platform:

Platform Guide Required Notes
Browser QUICKSTART-BROWSER.md SDK only Default storage: IndexedDB. TypeScript: ./impl/browser ships no type declarations yet (see the shim)
Node.js QUICKSTART-NODEJS.md SDK + ws, Node.js >= 22 Default storage: a wallet file under ./sphere-data
CLI unicity-sphere/sphere-cli Separate repository Not published to npm yet
dApp integration CONNECT.md SDK only ws (Node.js dApps)

CLI (Command Line Interface)

The Sphere CLI lives in its own repository, unicity-sphere/sphere-cli, and is not published to npm yet: npm install -g @unicity-sphere/cli fails with a 404. Its package.json depends on this SDK through a local path (file:../../sphere-sdk), so it builds only next to a checkout of this repository. See docs/QUICKSTART-CLI.md.

Quick Start

Setup is two provider layers, not one. createBrowserProviders / createNodeProviders build only the base (storage + transport + oracle). You must then attach the wallet-api transport config with createWalletApiProviders — money moves only through the wallet-api vertical. Skipping it fails loudly: Sphere.init throws INVALID_CONFIG.

import { Sphere, TokenRegistry, getCoinIdBySymbol, randomUUID } from '@unicitylabs/sphere-sdk';
import { createBrowserProviders } from '@unicitylabs/sphere-sdk/impl/browser'; // untyped entry: add the declaration shim below
import { createWalletApiProviders } from '@unicitylabs/sphere-sdk/impl/shared/wallet-api';

// One network literal, used in all three places below.
const NETWORK = 'testnet2';

// A per-device id: stable across launches on this device, different on every device.
function deviceId(): string {
  let id = localStorage.getItem('sphere-device-id');
  if (!id) {
    // The SDK's randomUUID(): unlike crypto.randomUUID(), it also works outside a secure context.
    id = randomUUID();
    localStorage.setItem('sphere-device-id', id);
  }
  return id;
}

// 1. Base providers: storage (IndexedDB) + transport (Nostr) + oracle (gateway).
//    `network` is required here: createBrowserProviders throws INVALID_CONFIG without it.
const base = createBrowserProviders({
  network: NETWORK,
  oracle: { apiKey: 'sk_ddc3cfcc001e4a28ac3fad7407f99590' }, // public testnet2 gateway key
});

// 2. The wallet-api transport config that the payments vertical is composed from.
//    Returns { ...base, walletApi }; walletApi is a plain config object.
const providers = createWalletApiProviders(base, {
  baseUrl: 'https://wallet-api.unicity.network', // testnet2 wallet-api
  network: NETWORK,
  deviceId: deviceId(),
});

// 3. Load the wallet in this storage, or create one. `network` is required here too.
const { sphere, created, generatedMnemonic } = await Sphere.init({
  ...providers,
  network: NETWORK,
  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.
//    coinId is the 64-hex coin id; getCoinIdBySymbol() returns it for a symbol.
await TokenRegistry.waitForReady(); // Sphere.init starts the registry load but does not await it
const coinId = getCoinIdBySymbol('UCT'); // string | undefined
if (!coinId) throw new Error('UCT is not in this network\'s token registry');

const result = await sphere.payments.send({
  recipient: '@alice',
  amount: '1000000',     // base units, as a decimal STRING (never a JS number)
  coinId,
  memo: 'hello',
});
console.log(result.status);   // 'delivered', or 'confirmed' with result.deliveryPending === true
// A resolved send() means sent. deliveryPending === true is NORMAL, not a failure: the token is
// certified on-chain and the mailbox delivery is retried automatically (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 ?? t.senderPubkey));

console.log(await sphere.payments.assets());

generatedMnemonic is returned only by the Sphere.init call that created the wallet. The phrase is stored before the rest of the setup runs, so if that call then throws (for example, a requested nametag is already taken), the next Sphere.init loads the stored wallet with created: false. Gate your backup prompt on your own "backup confirmed" flag and read the phrase with sphere.getMnemonic() until the user confirms.

Nametag bindings do not carry a network yet, so the SDK cannot prove that a @nametag or DIRECT:// recipient uses your network. Every such send emits transfer:attention with code: 'recipient:network-unverified' and an empty transferId, and then proceeds on your network. Treat it as information, not an error; on mainnet, make sure the recipient runs mainnet. A bare 66-hex chain pubkey recipient is taken as being on your network.

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; 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; use one literal, 'testnet2', in all three places. Neither provider factory returns a network field, so ...providers cannot supply it. Sphere.init compares its own network with walletApi.network as plain strings and throws INVALID_CONFIG ("walletApi.network "testnet2" does not match the Sphere network ...") when they differ, including when Sphere.init gets no network at all; this happens before any storage write. 'testnet' and 'testnet2' reach the same endpoints but are different strings, so mixing them fails this check. The wallet-api deployment names its network too: the testnet2 deployment signs you in only as 'testnet2', and the SDK refuses a sign-in challenge for any other network. The base-provider literal is not compared by that check, but it scopes the storage keys, while the payments state is keyed by the Sphere.init network, so mixing the two literals splits one wallet's state across two names.
  • Messaging-only wallets say so out loud. A wallet that never touches money — a Nostr DM or group-chat bot — passes walletApi: 'none' instead of a config: no wallet-api session, device registration, mailbox drain, token engine or pv2g2: key. network is still required, because it selects the token registry and the group-chat relays. sphere.payments then throws PAYMENTS_NOT_COMPOSED and sphere.hasPayments is false. Omitting walletApi altogether still throws INVALID_CONFIG — a dropped env var must never read as a deliberate choice.

For manual/advanced provider wiring, see Custom Providers Configuration. For the deeper integration guide, see docs/INTEGRATION.md.

Send result (TransferResult)

send() resolves only when the payment is sent, with a TransferResult:

Field Meaning
status 'delivered' when the payment landed in the recipient's mailbox, or 'confirmed' when the transfer is certified and delivery is still being retried (deliveryPending === true). send() never resolves with 'completed' or 'failed': a failure throws. ('submitted' and 'failed' appear only as transfer:updated event payloads.)
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).

A resolved send() is sent, whichever of the two statuses it carries. 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: never re-send a possibly-committed payment (money-safety)

Some rejections mean the money may already have left the wallet. isPossiblyCommittedSendOutcome(err) is true for exactly these codes: SEND_SYNC_PENDING, CERTIFICATION_UNCONFIRMED, CHECKPOINT_PERSIST_FAILED, SPLIT_CHECKPOINT_LOST, CHECKPOINT_TRUSTBASE_MISMATCH and SEND_PARTIALLY_COMPLETED. Never call send() again for that payment: a new send() gets a new transfer id and pays the recipient a second time. The SDK finishes the original under its own transfer id; show it as pending (sphere.payments.pendingTransfers()), and wire any "retry" button to sphere.payments.resumeNow(). A PartialSendConflictError means part of the amount was delivered and is final; only err.remainingAmount is still owed. When isPossiblyCommittedSendOutcome(err) is false, the SDK's contract is that nothing left the wallet.

  • CERTIFICATION_UNCONFIRMED is a ProofUnconfirmedError (mayHaveCertified: true): the spend may already be on-chain but the proof fetch was inconclusive. SEND_SYNC_PENDING can mean the spend committed on-chain and the wallet-api mirror is still catching up.
  • 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). sphere.payments.resumeNow() runs that convergence now; it is the only retry verb.
  • Clean failures you can branch on: SEND_INSUFFICIENT_BALANCE (when funds are pinned by transfers still converging, its message says how much and points to pendingTransfers()), INVALID_RECIPIENT, TRANSPORT_ERROR (the recipient lookup could not reach the relay) and VALIDATION_ERROR (a bad amount). INSUFFICIENT_BALANCE is never thrown.
  • Import the error helpers from the same entry point as Sphere, and read code structurally for the clean failures: errors thrown by provider code (the ./impl/* bundles, for example the Nostr transport during the recipient lookup) are a different SphereError class copy, so isSphereError() is false for them.
// Import the error helpers from the same entry point as Sphere (here: the package root).
import { PartialSendConflictError, isPossiblyCommittedSendOutcome } from '@unicitylabs/sphere-sdk';

try {
  const result = await sphere.payments.send({ recipient: '@alice', amount: '1000000', coinId });
  // Resolved means sent: result.status is 'delivered', or 'confirmed' with deliveryPending === true.
  if (result.deliveryPending) show('Sent. Delivery to the recipient is pending and is retried automatically.');
} catch (err) {
  if (err instanceof PartialSendConflictError) {
    // Part of the amount was delivered and is final. Only err.remainingAmount is still owed:
    // if you pay it, do it as a NEW send of exactly that amount, never the original amount.
    show(`Partly sent: ${err.remainingAmount} base units were not sent.`);
  } else if (isPossiblyCommittedSendOutcome(err)) {
    // The money may already have left the wallet. Never call send() again for this payment:
    // the SDK completes it under the same transferId. Show it as pending.
    show('Sent, waiting for confirmation.');
    const pending = await sphere.payments.pendingTransfers(); // rows for a "pending" list
    // A "retry" button calls sphere.payments.resumeNow(), never send().
  } else {
    // Nothing left the wallet. Read `code` structurally: errors thrown by the providers
    // (e.g. the Nostr transport) are a different SphereError class copy, so isSphereError() is false for them.
    const code = (err as { code?: unknown } | null)?.code;
    switch (code) {
      case 'SEND_INSUFFICIENT_BALANCE': show((err as Error).message); break; // names pinned funds when transfers are converging
      case 'INVALID_RECIPIENT': show('Recipient not found'); break;
      case 'TRANSPORT_ERROR': show('Could not look up the recipient. Check the connection.'); break;
      default: show(err instanceof Error ? err.message : String(err));
    }
  }
}

TypeScript: declarations for ./impl/browser

@unicitylabs/sphere-sdk/impl/browser ships no type declarations in this release; under strict TypeScript add the declaration shim below (or a one-line declare module '@unicitylabs/sphere-sdk/impl/browser';, which types everything from that entry as any). Import createWalletApiProviders from the typed @unicitylabs/sphere-sdk/impl/shared/wallet-api subpath, as above.

// Consumer-side declarations for '@unicitylabs/sphere-sdk/impl/browser'.
// That entry ships no .d.ts (tsup builds it with dts: false), so strict
// TypeScript reports TS7016 on the import without this file. Delete it once
// the package ships declarations for ./impl/browser.
declare module '@unicitylabs/sphere-sdk/impl/browser' {
  import type {
    NetworkType, StorageProvider, TransportProvider, OracleProvider, PriceProvider,
    PricePlatform, GroupChatModuleConfig, MarketModuleConfig,
  } from '@unicitylabs/sphere-sdk';

  export interface BrowserProvidersConfig {
    /** Required: createBrowserProviders throws INVALID_CONFIG without it. */
    network: NetworkType;
    debug?: boolean;
    storage?: { prefix?: string; dbName?: string; debug?: boolean };
    transport?: {
      relays?: string[]; additionalRelays?: string[]; timeout?: number; autoReconnect?: boolean;
      debug?: boolean; reconnectDelay?: number; maxReconnectAttempts?: number;
    };
    oracle?: { url?: string; apiKey?: string; timeout?: number; skipVerification?: boolean; debug?: boolean };
    price?: { platform?: PricePlatform; apiKey?: string; baseUrl?: string; cacheTtlMs?: number; timeout?: number; debug?: boolean };
    groupChat?: { enabled?: boolean; relays?: string[] } | boolean;
    market?: { apiUrl?: string; timeout?: number } | boolean;
  }

  export interface BrowserProviders {
    storage: StorageProvider;
    transport: TransportProvider;
    oracle: OracleProvider;
    price?: PriceProvider;
    groupChat?: GroupChatModuleConfig | boolean;
    market?: MarketModuleConfig | boolean;
  }

  export function createBrowserProviders(config: BrowserProvidersConfig): BrowserProviders;
}

The shim declares only createBrowserProviders. The other ./impl/browser exports used later in this README (createLocalStorageProvider, createNostrTransportProvider, createUnicityAggregatorProvider) need their own declarations, or the one-line form.

Migrating off sphere.paymentsV2

The 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.

A wallet initialised with walletApi: 'none' has no payments at all: there sphere.payments throws PAYMENTS_NOT_COMPOSED, permanently, instead of the transient NOT_INITIALIZED. Use sphere.hasPayments to tell the two cases apart without a try/catch.

The accounting: / swap: options are not part of this cleanup: they still throw a typed INVALID_CONFIG, deliberately, because those modules were removed and a silently ignored option would hide that.

Network Configuration

The SDK ships network presets that configure all services automatically. network is required — there is no default:

network literal networkId Gateway (preset) Nostr relay (preset) wallet-api baseUrl (you pass it)
'testnet2' 4 https://gateway.testnet2.unicity.network wss://nostr-relay.testnet.unicity.network https://wallet-api.unicity.network
'mainnet' 1 https://gateway.mainnet.unicity.network the testnet relay (mainnet has none of its own yet) https://wallet-api.mainnet.unicity.network
'testnet' 4 same as testnet2 same as testnet2 none: the testnet2 wallet-api signs in only as 'testnet2', and the SDK refuses its sign-in challenge for 'testnet'. Use 'testnet2'

Live networks are testnet2 and mainnet, each with its own gateway and wallet-api deployment. testnet is a second key with testnet2's configuration (network id 4, taken from the trust base; the testnet2 token registry), but it is a different string, so it fails the network check against a 'testnet2' wallet-api config (see network placement). SPHERE_NETWORKS exposes only mainnet and testnet2. The v1 network is discontinued — the old goggregator-test testnet spoke the removed v1 protocol, and the dev network that aliased its trust base has been removed along with every other v1 pointer. On mainnet use network: 'mainnet' in createBrowserProviders/createNodeProviders, in the walletApi config and on Sphere.init, the mainnet wallet-api https://wallet-api.mainnet.unicity.network, and your mainnet gateway API key, which is a secret. Mainnet shares testnet2's Nostr relay for now, and its token registry lists no fungible coins yet. The transfer wire payload is the finished token blob — the base SDK's own Token.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 the testnet2 preset for all services
const presetOnly = createBrowserProviders({ network: 'testnet2' });

// Override specific services while using the network preset
const customGateway = createBrowserProviders({
  network: 'testnet2',
  oracle: { url: 'https://custom-gateway.example.com' }, // custom testnet2 gateway
});

API Key

The SDK bundles no default API key. Pass the gateway key via oracle: { apiKey }. Without one the token engine is still built, the SDK logs a TokenEngine warning, and gateway requests are unauthenticated; whether a gateway serves them is the gateway's policy.

const withApiKey = createBrowserProviders({
  network: 'testnet2',
  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.

Testnet2 endpoints (the values we build with)

The testnet2 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 testnet2, networkId 4 (the testnet key has the same gateway, relays and token registry, but it is a different literal and the testnet2 wallet-api signs in only as 'testnet2'; use testnet2)
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.

Mainnet (network: 'mainnet', networkId 1): gateway https://gateway.mainnet.unicity.network, wallet-api https://wallet-api.mainnet.unicity.network, the same Nostr and group-chat relays as testnet2, and token registry https://raw.githubusercontent.com/unicitynetwork/unicity-ids/refs/heads/main/unicity-ids.mainnet.json, which currently lists only the non-fungible base token type (no fungible coins yet).

Price Provider (Optional)

Enable fiat price display by adding a price config. Currently supports CoinGecko API (free and pro tiers).

// With CoinGecko (free tier, no API key)
const base = createBrowserProviders({
  network: 'testnet2',
  price: { platform: 'coingecko' },
});
// With CoinGecko Pro: price: { platform: 'coingecko', apiKey: 'CG-xxx' }

const providers = createWalletApiProviders(base, {
  baseUrl: 'https://wallet-api.unicity.network',
  network: 'testnet2',
});
const { sphere } = await Sphere.init({ ...providers, network: 'testnet2', autoGenerate: true });

// Assets with price data
const assets = await sphere.payments.assets();
// [{ coinId, symbol, totalAmount, priceUsd: 97500, fiatValueUsd: 975.00, change24h: 2.3, ... }]

// Total portfolio value in USD
const totalUsd = assets.reduce((sum, a) => sum + (a.fiatValueUsd ?? 0), 0);

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',
}));

Test Tokens on Testnet (Self-Mint)

There is no faucet. On testnet you top up your wallet by self-minting fungible tokens via the token engine — mint(coinIdHex, amount) mints a finished token directly to this wallet (journal-first: crash-safe, a replay converges idempotently):

import { TokenRegistry, getCoinIdBySymbol } from '@unicitylabs/sphere-sdk';

// Resolve the coin's hex id from the token registry (or pass a hex coinId directly).
await TokenRegistry.waitForReady(); // Sphere.init starts the registry load but does not await it
const coinId = getCoinIdBySymbol('UCT'); // string | undefined
if (!coinId) throw new Error('UCT is not in this network\'s token registry');

const result = await sphere.payments.mint(coinId, 1000n);
if (result.success) {
  console.log('Minted token:', result.tokenId);
} else {
  console.error('Mint failed:', result.error);
}

Note: Minting needs the token engine, which Sphere.init builds from the oracle's trust base and gateway URL (without them Sphere.init rejects with INVALID_CONFIG); pass the gateway key via oracle: { apiKey }. A mint that fails after it was journaled resolves { success: false, error } and is replayed by the SDK: do not call mint() again for it. See API Key above.

Multi-Address Support

The SDK supports HD (Hierarchical Deterministic) wallets with multiple addresses:

// Get current address index
const currentIndex = sphere.getCurrentAddressIndex(); // 0

// Switch to a different address
await sphere.switchToAddress(1);
console.log(sphere.identity?.directAddress); // DIRECT://... (address at index 1)

// Register nametag for this address (independent per address)
await sphere.registerNametag('bob');

// Switch back to first address
await sphere.switchToAddress(0);

// Get the nametag of a specific address. Index 1 is tracked because we switched to it.
const bobNametag = sphere.getTrackedAddress(1)?.nametag; // 'bob'
// getNametagForAddress takes the short addressId ('DIRECT_xxxxxx_yyyyyy'), not an index:
const sameNametag = sphere.getNametagForAddress(sphere.getTrackedAddress(1)?.addressId);

// All active addresses with their nametags (TrackedAddress[], sorted by index)
const active = sphere.getActiveAddresses();
// [{ index: 0, addressId: 'DIRECT_…', directAddress: 'DIRECT://…', nametag: 'alice', … }, { index: 1, …, nametag: 'bob' }]

// deriveAddress() returns keys, not an address: { privateKey, publicKey, path, index }.
const keys2 = sphere.deriveAddress(2);
console.log(keys2.publicKey, keys2.path); // never log or serialise the whole object: it holds the private key

deriveAddress(index) returns key material, { privateKey, publicKey, path, index }, not an address; never log or serialise the whole object. For the DIRECT:// address use sphere.identity?.directAddress (active address) or sphere.getTrackedAddress(index)?.directAddress (after switchToAddress(index)). getAllAddressNametags() is deprecated; it returns Map<addressId, Map<nametagIndex, nametag>>, keyed by the short addressId.

Identity Properties

Important: The DIRECT address is the primary address for the Unicity network.

interface Identity {
  chainPubkey: string;         // 33-byte compressed secp256k1 public key
  directAddress?: string;      // DIRECT address (DIRECT://...) - PRIMARY ADDRESS
  ipnsName?: string;           // legacy derived id ('12D3KooW…'); nothing in the SDK uses it
  nametag?: string;            // Registered nametag (@username)
}

// Access identity - use directAddress as primary
console.log(sphere.identity?.directAddress);    // DIRECT://0000be36... (PRIMARY)
console.log(sphere.identity?.nametag);          // alice (human-readable)
console.log(sphere.identity?.chainPubkey);      // 02abc123... (33-byte compressed)

Address Change Event

Event handlers receive the payload directly: sphere.on('identity:changed', (e) => e.addressIndex), not e.data.addressIndex. on() returns an unsubscribe function.

// Listen for address switches
const off = sphere.on('identity:changed', (event) => {
  console.log('Switched to address index:', event.addressIndex);
  console.log('L3 address:', event.directAddress);
  console.log('Chain pubkey:', event.chainPubkey);
  console.log('Nametag:', event.nametag);
});

// Nametag recoveries after init (e.g. after switchToAddress)
sphere.on('nametag:recovered', (event) => {
  console.log('Recovered nametag from Nostr:', event.nametag);
});

off(); // stop listening

Nametag recovery during Sphere.init / load / import finishes, and emits nametag:recovered, before the call returns, so a listener added afterwards does not see it. Check sphere.identity?.nametag after init. The event is useful for later recoveries, such as after switchToAddress().

Payment Requests

Request payments from others over the wallet-api rail (sphere.payments.requests). Request memos ride an encrypted recipient-ECDH envelope.

  • requests.create(to, { coinId, amount, memo? }) never throws; it resolves { success, requestId?, error? }. Check success. coinId is the 64-hex coin id (look it up with getCoinIdBySymbol()).
  • Never pay from inside the payment_request:incoming handler without the user's decision; pay() and decline() are alternatives. pay() rethrows send()'s errors: handle them as in Handling send() rejections.
  • payment_request:updated reports requests you received. The SDK does not track requests you created: detect payment through transfer:incoming or sphere.payments.history().
  • request.amount is a base-unit string and request.coinId the hex id; request.symbol is not set by the SDK event.

When the send inside pay() fails with a possibly-committed error, pay() links the request to that transfer (the error's transferId) in the payments journal and marks it 'settling' before it rethrows, so the request is not payable, and the link survives a restart. One exception: if writing that link to storage fails, pay() rejects with the storage error instead of the send error, so isPossiblyCommittedSendOutcome is false for it although the payment may have gone out; the link is then held in memory and reaches storage only with a later successful journal write. A second pay() of the same id while the first is still running joins it. The link is written after the send returns or throws, not before it starts. If the app or process stops while pay() is still waiting on the send, or before a link that failed to write reaches storage, no link exists: on the next start the request is listed as 'pending' again and payment_request:incoming fires again, even if the transfer went through (a transfer the SDK had already recorded is resumed when the wallet starts). Before paying a request again after a restart, check sphere.payments.pendingTransfers() and sphere.payments.history() for a transfer to that requester.

import { TokenRegistry, getCoinIdBySymbol } from '@unicitylabs/sphere-sdk';

// Requester side: create() never throws. It resolves { success, requestId?, error? }.
await TokenRegistry.waitForReady();
const coinId = getCoinIdBySymbol('UCT'); // the 64-hex coin id, or undefined
if (coinId) {
  const created = await sphere.payments.requests.create('@bob', {
    coinId,
    amount: '1000000',
    memo: 'Payment for order #1234',
  });
  if (!created.success) console.error(created.error);
}

// Payer side: never pay from the event handler itself. Show the request and let the user decide.
sphere.on('payment_request:incoming'(README truncated)

View on GitHub

Recent activity

commits and pull requests

Releases and announcements

2 total
  1. **178 PRs promoted from `integration/all-fixes` to `main`.** - Release PR: [#345](https://github.com/unicity-sphere/sphere-sdk/pull/345) - Tag points at: `af41bea6fc36` - Period covered: ~4 months of accumulated profile-layer, UXF, recovery, and connectivity work since the previous release lineage (PRs #128–#130 → #303–#342). ## ⚠️ Breaking: UXF wire-shape default flip Default sends now emit UXF v1.0 bundles (`senderUxf=true`). Older SDK receivers **cannot decode** them. Pin a shared SDK version across senders/receivers during the transition, or pass explicit `features: { senderUxf: false }` to fall back to the legacy single-token TXF wire shape. See `docs/uxf/UXF-TRANSFER-CUTOVER-RUNBOOK.md` for the runbook. --- ### Changed (BREAKING — wire-shape default flip) - **UXF feature flags now default-ON** (T.8.D part 1 of 2 — production cutover, NO legacy code path removal). All four UXF feature flags moved from default `false` → default `true` in `PaymentsModuleConfig.features`: - `senderUxf` — `payments.send({transferMode:'instant'})` (the public default) now routes through the new UXF instant-sender; conservative-mode also routes through the UXF orchestrator. - `recipientUxf

  2. v0.2.4v0.2.4Feb 11, 2026

    ## Bug Fixes - **CLI process exit**: Fix CLI commands hanging ~10s after completion due to lingering Nostr WebSocket handles - **Instant split change token**: Fix sender losing change token when `process.exit()` killed background proof collection before completion ## Changes - `InstantSplitExecutor.submitBackgroundV5` now returns a trackable `Promise<void>` instead of fire-and-forget - `PaymentsModule` tracks background promises and exposes `waitForPendingOperations()` for callers to await completion - CLI `send` command awaits pending background operations before exiting - `InstantSplitResult` now includes optional `backgroundPromise` field ## Testing - 914 unit tests passing - 8/8 transfer e2e tests passing (balance + tombstone verified) - 8/8 CLI e2e tests passing (instant + conservative, UCT/BTC/ETH)

Code frequency

additions and deletions
+91.8K-91.8KWeek of 2026-01-25: +52,876 linesWeek of 2026-01-25: -10,843 linesWeek of 2026-02-01: +16,619 linesWeek of 2026-02-01: -2,949 linesWeek of 2026-02-08: +44,937 linesWeek of 2026-02-08: -11,585 linesWeek of 2026-02-15: +15,983 linesWeek of 2026-02-15: -1,550 linesWeek of 2026-02-22: +9,797 linesWeek of 2026-02-22: -9,445 linesWeek of 2026-03-01: +1,663 linesWeek of 2026-03-01: -829 linesWeek of 2026-03-08: +3,746 linesWeek of 2026-03-08: -219 linesWeek of 2026-03-15: +1,190 linesWeek of 2026-03-15: -242 linesWeek of 2026-03-22: +518 linesWeek of 2026-03-22: -31 linesWeek of 2026-03-29: +250 linesWeek of 2026-03-29: -37 linesWeek of 2026-04-05: +286 linesWeek of 2026-04-05: -26 linesWeek of 2026-04-12: +73,857 linesWeek of 2026-04-12: -1,216 linesWeek of 2026-04-19: +0 linesWeek of 2026-04-19: -0 linesWeek of 2026-04-26: +0 linesWeek of 2026-04-26: -0 linesWeek of 2026-05-03: +3,839 linesWeek of 2026-05-03: -8,238 linesWeek of 2026-05-10: +433 linesWeek of 2026-05-10: -350 linesWeek of 2026-05-17: +0 linesWeek of 2026-05-17: -0 linesWeek of 2026-05-24: +0 linesWeek of 2026-05-24: -0 linesWeek of 2026-05-31: +6,417 linesWeek of 2026-05-31: -4,736 linesWeek of 2026-06-07: +22,615 linesWeek of 2026-06-07: -22,027 linesWeek of 2026-06-14: +9,280 linesWeek of 2026-06-14: -30,947 linesWeek of 2026-06-21: +1,979 linesWeek of 2026-06-21: -3,921 linesWeek of 2026-06-28: +924 linesWeek of 2026-06-28: -97 linesWeek of 2026-07-05: +2,865 linesWeek of 2026-07-05: -211 linesWeek of 2026-07-12: +2,724 linesWeek of 2026-07-12: -398 linesWeek of 2026-07-19: +2,383 linesWeek of 2026-07-19: -849 linesWeek of 2026-07-26: +36,517 linesWeek of 2026-07-26: -21,175 linesWeek of 2026-08-02: +10,829 linesWeek of 2026-08-02: -91,847 linesWeek of 2026-08-09: +2,270 linesWeek of 2026-08-09: -402 linesWeek of 2026-08-16: +634 linesWeek of 2026-08-16: -49 linesWeek of 2026-08-23: +3,362 linesWeek of 2026-08-23: -784 linesWeek of 2026-08-30: +8,102 linesWeek of 2026-08-30: -865 linesWeek of 2026-09-06: +3,930 linesWeek of 2026-09-06: -602 linesWeek of 2026-09-13: +0 linesWeek of 2026-09-13: -0 linesJan 25, 2026Sep 13, 2026
+340.8K lines added, -226.5K removed over the last year.

Commits per week

last 52 weeks
1310Week of 2025-10-04: 0 commitsWeek of 2025-10-11: 0 commitsWeek of 2025-10-18: 0 commitsWeek of 2025-10-25: 0 commitsWeek of 2025-11-01: 0 commitsWeek of 2025-11-09: 0 commitsWeek of 2025-11-16: 0 commitsWeek of 2025-11-23: 0 commitsWeek of 2025-11-30: 0 commitsWeek of 2025-12-07: 0 commitsWeek of 2025-12-14: 0 commitsWeek of 2025-12-21: 0 commitsWeek of 2025-12-28: 0 commitsWeek of 2026-01-04: 0 commitsWeek of 2026-01-11: 0 commitsWeek of 2026-01-18: 0 commitsWeek of 2026-01-25: 54 commitsWeek of 2026-02-01: 69 commitsWeek of 2026-02-08: 131 commitsWeek of 2026-02-15: 48 commitsWeek of 2026-02-22: 21 commitsWeek of 2026-03-01: 28 commitsWeek of 2026-03-08: 11 commitsWeek of 2026-03-15: 17 commitsWeek of 2026-03-22: 3 commitsWeek of 2026-03-29: 11 commitsWeek of 2026-04-05: 7 commitsWeek of 2026-04-12: 8 commitsWeek of 2026-04-19: 0 commitsWeek of 2026-04-26: 0 commitsWeek of 2026-05-03: 13 commitsWeek of 2026-05-10: 11 commitsWeek of 2026-05-17: 0 commitsWeek of 2026-05-24: 0 commitsWeek of 2026-05-31: 49 commitsWeek of 2026-06-07: 84 commitsWeek of 2026-06-14: 44 commitsWeek of 2026-06-21: 17 commitsWeek of 2026-06-28: 8 commitsWeek of 2026-07-05: 17 commitsWeek of 2026-07-12: 21 commitsWeek of 2026-07-19: 10 commitsWeek of 2026-07-26: 52 commitsWeek of 2026-08-02: 44 commitsWeek of 2026-08-09: 17 commitsWeek of 2026-08-16: 3 commitsWeek of 2026-08-23: 5 commitsWeek of 2026-08-30: 5 commitsWeek of 2026-09-06: 5 commitsWeek of 2026-09-13: 16 commitsWeek of 2026-09-20: 12 commitsWeek of 2026-09-27: 1 commitsOct 4, 2025Sep 27, 2026
842 commits in the last 52 weeks.

When work happens

weekday and hour
SunMonTueWedThuFriSat036912151821Sun 0:00 — 5 commitsSun 1:00 — 4 commitsSun 2:00 — 4 commitsSun 3:00 — 3 commitsSun 4:00 — 0 commitsSun 5:00 — 0 commitsSun 6:00 — 0 commitsSun 7:00 — 0 commitsSun 8:00 — 1 commitsSun 9:00 — 1 commitsSun 10:00 — 0 commitsSun 11:00 — 0 commitsSun 12:00 — 1 commitsSun 13:00 — 1 commitsSun 14:00 — 0 commitsSun 15:00 — 0 commitsSun 16:00 — 1 commitsSun 17:00 — 1 commitsSun 18:00 — 1 commitsSun 19:00 — 4 commitsSun 20:00 — 5 commitsSun 21:00 — 6 commitsSun 22:00 — 5 commitsSun 23:00 — 4 commitsMon 0:00 — 1 commitsMon 1:00 — 4 commitsMon 2:00 — 4 commitsMon 3:00 — 5 commitsMon 4:00 — 2 commitsMon 5:00 — 2 commitsMon 6:00 — 2 commitsMon 7:00 — 1 commitsMon 8:00 — 1 commitsMon 9:00 — 2 commitsMon 10:00 — 2 commitsMon 11:00 — 8 commitsMon 12:00 — 7 commitsMon 13:00 — 5 commitsMon 14:00 — 5 commitsMon 15:00 — 6 commitsMon 16:00 — 12 commitsMon 17:00 — 7 commitsMon 18:00 — 8 commitsMon 19:00 — 1 commitsMon 20:00 — 5 commitsMon 21:00 — 3 commitsMon 22:00 — 6 commitsMon 23:00 — 3 commitsTue 0:00 — 6 commitsTue 1:00 — 4 commitsTue 2:00 — 4 commitsTue 3:00 — 5 commitsTue 4:00 — 6 commitsTue 5:00 — 2 commitsTue 6:00 — 2 commitsTue 7:00 — 0 commitsTue 8:00 — 1 commitsTue 9:00 — 1 commitsTue 10:00 — 1 commitsTue 11:00 — 6 commitsTue 12:00 — 7 commitsTue 13:00 — 9 commitsTue 14:00 — 6 commitsTue 15:00 — 16 commitsTue 16:00 — 12 commitsTue 17:00 — 10 commitsTue 18:00 — 13 commitsTue 19:00 — 8 commitsTue 20:00 — 9 commitsTue 21:00 — 10 commitsTue 22:00 — 7 commitsTue 23:00 — 10 commitsWed 0:00 — 7 commitsWed 1:00 — 9 commitsWed 2:00 — 7 commitsWed 3:00 — 4 commitsWed 4:00 — 5 commitsWed 5:00 — 11 commitsWed 6:00 — 3 commitsWed 7:00 — 3 commitsWed 8:00 — 3 commitsWed 9:00 — 4 commitsWed 10:00 — 7 commitsWed 11:00 — 6 commitsWed 12:00 — 14 commitsWed 13:00 — 8 commitsWed 14:00 — 5 commitsWed 15:00 — 12 commitsWed 16:00 — 9 commitsWed 17:00 — 11 commitsWed 18:00 — 5 commitsWed 19:00 — 5 commitsWed 20:00 — 13 commitsWed 21:00 — 22 commitsWed 22:00 — 8 commitsWed 23:00 — 13 commitsThu 0:00 — 14 commitsThu 1:00 — 1 commitsThu 2:00 — 1 commitsThu 3:00 — 2 commitsThu 4:00 — 0 commitsThu 5:00 — 0 commitsThu 6:00 — 0 commitsThu 7:00 — 1 commitsThu 8:00 — 1 commitsThu 9:00 — 2 commitsThu 10:00 — 3 commitsThu 11:00 — 2 commitsThu 12:00 — 7 commitsThu 13:00 — 5 commitsThu 14:00 — 3 commitsThu 15:00 — 5 commitsThu 16:00 — 10 commitsThu 17:00 — 18 commitsThu 18:00 — 12 commitsThu 19:00 — 14 commitsThu 20:00 — 7 commitsThu 21:00 — 19 commitsThu 22:00 — 7 commitsThu 23:00 — 17 commitsFri 0:00 — 8 commitsFri 1:00 — 16 commitsFri 2:00 — 1 commitsFri 3:00 — 1 commitsFri 4:00 — 1 commitsFri 5:00 — 1 commitsFri 6:00 — 6 commitsFri 7:00 — 5 commitsFri 8:00 — 1 commitsFri 9:00 — 1 commitsFri 10:00 — 3 commitsFri 11:00 — 2 commitsFri 12:00 — 6 commitsFri 13:00 — 3 commitsFri 14:00 — 1 commitsFri 15:00 — 6 commitsFri 16:00 — 4 commitsFri 17:00 — 8 commitsFri 18:00 — 8 commitsFri 19:00 — 13 commitsFri 20:00 — 12 commitsFri 21:00 — 7 commitsFri 22:00 — 4 commitsFri 23:00 — 8 commitsSat 0:00 — 7 commitsSat 1:00 — 6 commitsSat 2:00 — 3 commitsSat 3:00 — 2 commitsSat 4:00 — 3 commitsSat 5:00 — 0 commitsSat 6:00 — 1 commitsSat 7:00 — 0 commitsSat 8:00 — 0 commitsSat 9:00 — 1 commitsSat 10:00 — 0 commitsSat 11:00 — 0 commitsSat 12:00 — 0 commitsSat 13:00 — 1 commitsSat 14:00 — 6 commitsSat 15:00 — 3 commitsSat 16:00 — 3 commitsSat 17:00 — 6 commitsSat 18:00 — 6 commitsSat 19:00 — 2 commitsSat 20:00 — 2 commitsSat 21:00 — 1 commitsSat 22:00 — 6 commitsSat 23:00 — 10 commits
Commit volume by weekday and hour (UTC). Larger dots mean more commits.
DateListRankStars gained
May 28, 2026daily#3+164
May 27, 2026daily#13+80
  • freeCodeCamp/freeCodeCamp

    freeCodeCamp.org's open-source codebase and curriculum. Learn math, programming, and computer science for free.

    456.7K stars · TypeScript

  • openclaw/openclaw

    The AI that really does things. Any OS. Any Platform. The lobster way. 🦞

    391.3K stars · TypeScript

  • affaan-m/ECC

    The agent harness performance optimization system. Skills, instincts, memory, security, and research-first development for Claude Code, Codex, Opencode, Cursor and beyond.

    272.8K stars · JavaScript

  • NousResearch/hermes-agent

    The agent that grows with you

    251.2K stars · Python

  • anomalyco/opencode

    The open source coding agent.

    211.7K stars · TypeScript

  • n8n-io/n8n

    Fair-code workflow automation platform with native AI capabilities. Combine visual building with custom code, self-host or cloud, 400+ integrations.

    206.7K stars · TypeScript