npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@tari-project/ootle.ts

v0.14.6

Published

TypeScript SDK for building on the Tari Ootle network

Readme

ootle.ts

TypeScript SDK for building on the Tari Ootle network.

License

ootle.ts is a modular, strongly-typed SDK that lets you connect to wallets, query chain state, build transactions, and submit them to the Tari Ootle network — all from TypeScript or JavaScript.


Packages

The SDK is split into four focused packages — each will be published to npm under the @tari-project scope. Install only what you need.

| Package | Description | | ------------------------------------------------------------------------------------- | ---------------------------------------------------------------- | | @tari-project/ootle | Core interfaces, transaction builder, and flow helpers | | @tari-project/ootle-indexer | Indexer REST provider — read chain state and submit transactions | | @tari-project/ootle-secret-key-wallet | Local in-memory signer backed by WASM crypto | | @tari-project/ootle-wallet-daemon-signer | Remote signer — delegates signing to a running wallet daemon |

Cryptographic operations (key generation, Schnorr signing, BOR encoding) are provided by @tari-project/ootle-wasm, an external dependency used internally by ootle and ootle-secret-key-wallet.


Runtime support

Every package in this repo ships one universal artifact that runs in both browsers (via a bundler such as Vite) and Node ≥ 22 (via tsx or plain node).

| Package | Browser | Node ≥ 22 | Notes | | ------------------------------------------ | ------- | -------------------- | ----------------------------------------------------- | | @tari-project/ootle | ✓ | ✓ | Core; WASM crypto via @tari-project/ootle-wasm | | @tari-project/ootle-indexer | ✓ | ✓ | fetch + SSE native in both | | @tari-project/ootle-secret-key-wallet | ✓ | ✓ | Stealth scan/spend needs randomWithViewKey(network) | | @tari-project/ootle-wallet-daemon-signer | ✓ | ✓ (with authToken) | WebAuthn passkeys are browser-only |

Node note: Node ≥ 22 currently requires NODE_OPTIONS=--experimental-wasm-modules when running under tsx or plain node (the WASM ESM gating in Node will be lifted in a future release). See examples/node/README.md for the rationale and forward plan; every script in examples/node/ wires the flag into its pnpm invocation so most users never set it manually.

Choose your path

Pick a row that matches what you want to build first.

| I want to… | Use | Start at | | -------------------------- | --------------------------------------------- | --------------------------------------------------------------------------------------------------------- | | Build a web dApp | ootle + ootle-indexer + daemon signer | Quick start — browser | | Script/automate from Node | ootle + ootle-indexer + secret-key wallet | Quick start — Node | | Just read chain state | ootle-indexer | indexer-explorer example | | Send confidential payments | ootle stealth API | Stealth overview |

Choose your track Browser / dApp: Quick start — browser · Node script / server: Quick start — Node


Quick start

1. Connect to the Esmeralda testnet and read a substate

import { ProviderBuilder, Network } from "@tari-project/ootle-indexer";

const provider = await ProviderBuilder.new().withNetwork(Network.Esmeralda).connect(); // uses the default public indexer URL

const substate = await provider.getSubstate("component_0x…");
console.log(substate);

2. Build and submit a transaction (wallet daemon)

import { TransactionBuilder, sendTransaction, Network } from "@tari-project/ootle";
import { ProviderBuilder } from "@tari-project/ootle-indexer";
import { WalletDaemonSigner } from "@tari-project/ootle-wallet-daemon-signer";

const provider = await ProviderBuilder.new().withNetwork(Network.LocalNet).connect();
const signer = await WalletDaemonSigner.connect({ url: "http://localhost:18103", authToken: "…" });

const unsignedTx = TransactionBuilder.new(Network.LocalNet)
  .feeTransactionPayFromComponent(await signer.getAddress(), 1000n)
  .callMethod({ componentAddress: accountAddress, methodName: "withdraw" }, [
    { Literal: resourceAddress },
    { Literal: "500" },
  ])
  .saveVar("bucket")
  .callMethod({ componentAddress: recipientAddress, methodName: "deposit" }, [{ Workspace: "bucket" }])
  .buildUnsignedTransaction();

const result = await sendTransaction(provider, signer, unsignedTx);

3. Local signing (testing / scripting)

import { SecretKeyWallet } from "@tari-project/ootle-secret-key-wallet";

// Generate a fresh wallet with a view-only key (for stealth output scanning)
const wallet = SecretKeyWallet.randomWithViewKey(Network.Esmeralda);

// Or restore from an existing key (Uint8Array)
const wallet = SecretKeyWallet.fromSecretKey(ownerSecretKey, Network.Esmeralda);

4. 5-minute Node quickstart

The browser-flavoured blocks above use a wallet daemon. From a headless Node script the canonical signer is SecretKeyWallet — no daemon, no React. Save the following as transfer.ts and run it against a LocalNet:

import {
  AccountInvokeBuilder,
  FaucetInvokeBuilder,
  Network,
  TARI_RESOURCE_ADDRESS,
  XTR_FAUCET_COMPONENT_ADDRESS,
  defaultIndexerUrl,
  sendTransaction,
} from "@tari-project/ootle";
import { IndexerProvider } from "@tari-project/ootle-indexer";
import { EphemeralKeySigner, SecretKeyWallet } from "@tari-project/ootle-secret-key-wallet";

const url = process.env.OOTLE_INDEXER_URL ?? defaultIndexerUrl(Network.LocalNet);
const provider = await IndexerProvider.connect({ url, network: Network.LocalNet });

const sender = SecretKeyWallet.randomWithViewKey(Network.LocalNet);
const recipient = EphemeralKeySigner.generate(Network.LocalNet);

const faucetTx = new FaucetInvokeBuilder(Network.LocalNet, XTR_FAUCET_COMPONENT_ADDRESS)
  .feeTransactionPayFromComponent(await sender.getAddress(), 1000n)
  .takeFaucetFunds(await sender.getAddress(), 10_000_000n)
  .build();
await sendTransaction(provider, sender, faucetTx);

const transferTx = new AccountInvokeBuilder(Network.LocalNet, await sender.getAddress())
  .feeTransactionPayFromComponent(await sender.getAddress(), 1000n)
  .publicTransfer(await sender.getAddress(), TARI_RESOURCE_ADDRESS, 2_000_000n, await recipient.getAddress())
  .build();
await sendTransaction(provider, sender, transferTx);

provider.stopWatcher();

Run it:

OOTLE_INDEXER_URL=http://localhost:12500 \
  NODE_OPTIONS=--experimental-wasm-modules tsx transfer.ts

For the production-grade pattern (multi-signer co-authorisation, dry-run fee estimation, receipt-diff parsing) see examples/node/src/fungible-transfer.ts and the full Quick start — Node guide.


Architecture

ootle.ts uses two core abstractions:

Provider — reads chain state

Implemented by IndexerProvider. Provides:

  • getSubstate(id) / fetchSubstates(ids)
  • resolveInputs(inputs) — fills in missing versions before signing
  • submitTransaction(envelope)
  • getTransactionResult(txId)
  • listRecentTransactions(params) / getTemplateDefinition(address)

Signer — produces signatures

Implemented by SecretKeyWallet, WalletDaemonSigner, and EphemeralKeySigner. Provides:

  • getAddress() / getPublicKey()
  • signTransaction(unsignedTx)

Transaction flow

unsignedTx
  → resolveTransaction(provider, …)   // fill in substate versions
  → signTransaction(signers, …)        // generate seal keypair, collect Schnorr signatures
  → sealTransaction(signed)            // BOR-encode into a TransactionEnvelope
  → submitTransaction(provider, …)     // submit the envelope to the network
  → watchTransaction(provider, txId)   // wait for finalization

Or use the sendTransaction / sendDryRun convenience helpers which chain all steps.

Note: WASM crypto operations (hashing, signing, encoding) are handled internally by signTransaction, sealTransaction, and sendTransaction. You do not need to manage a WASM module or encoder — @tari-project/ootle-wasm is a dependency of the core package.


Package Reference

@tari-project/ootle

Core package. Everything else depends on it.

import {
  Network,
  TransactionBuilder,
  literalArg,
  resolveTransaction,
  signTransaction,
  sealTransaction,
  submitTransaction,
  watchTransaction,
  sendTransaction,
  sendDryRun,
  classifyOutcome,
  OotleWallet,
  WalletStealthAuthorizer,
  StealthTransfer,
  AccountInvokeBuilder,
  FaucetInvokeBuilder,
  defaultIndexerUrl,
} from "@tari-project/ootle";

Network

enum Network {
  MainNet = 0x00,
  StageNet = 0x01,
  NextNet = 0x02,
  LocalNet = 0x10,
  Igor = 0x24,
  Esmeralda = 0x26,
}

TransactionBuilder

Fluent builder for UnsignedTransactionV1.

const unsignedTx = TransactionBuilder.new(Network.Esmeralda)
  .feeTransactionPayFromComponent(accountAddress, 1000n)
  .callFunction({ templateAddress, functionName: "new" }, [literalArg("hello")])
  .saveVar("component")
  .callMethod({ componentAddress, methodName: "do_something" }, [{ Workspace: "component" }])
  .withMinEpoch(10)
  .addInput({ substate_id: vaultId, version: 3 })
  .buildUnsignedTransaction();

Key methods:

| Method | Description | | --------------------------------------------------------- | ---------------------------------------------- | | callFunction(func, args) | Call a template function | | callMethod(method, args) | Call a component method | | createAccount(ownerPublicKey) | Create a new account component | | saveVar(name) | Save last output to a named workspace variable | | feeTransactionPayFromComponent(addr, amount) | Add fee instruction | | feeTransactionPayFromComponentConfidential(addr, proof) | Confidential fee | | claimBurn(claim, output_data) | Claim a Minotari burn | | allocateAddress(type, name) | Pre-allocate an address | | addInput(req) / withInputs(reqs) | Add substate inputs | | withMinEpoch(n) / withMaxEpoch(n) | Set epoch bounds | | buildUnsignedTransaction() | Return the finished UnsignedTransactionV1 |

Transaction flow functions

// Individual steps
const resolved = await resolveTransaction(provider, unsignedTx);
const signed = await signTransaction([signer], resolved);       // returns a signed Transaction
const envelope = sealTransaction(signed);                        // BOR-encode into TransactionEnvelope
const txId = await submitTransaction(provider, envelope);        // submit to network
const receipt = await watchTransaction(provider, txId, { timeoutMs: 30_000 });

// All-in-one
const receipt = await sendTransaction(provider, signer, unsignedTx);

// Dry-run (simulates without committing)
const result = await sendDryRun(provider, signer, unsignedTx);

// Inspect the outcome
const outcome = classifyOutcome(receipt.result);
// outcome: { outcome: "Commit" }
//        | { outcome: "FeeIntentCommit", reason: string }
//        | { outcome: "Reject", reason: string }

OotleWallet

Multi-signer wallet that manages multiple key providers — one per address. Useful when a transaction requires authorizations from several components.

import { OotleWallet } from "@tari-project/ootle";

const wallet = new OotleWallet();
wallet.registerKeyProvider(address, secretKeyWallet);
wallet.setDefaultSigner(address);

// Sign on behalf of any registered signer
const auth = await wallet.authorizeTransaction(address, unsignedTx);

// Sign with the default signer
const signatures = await wallet.signTransaction(unsignedTx);

Builtin template helpers

Pre-built builders for the standard account and faucet templates.

import { AccountInvokeBuilder, FaucetInvokeBuilder } from "@tari-project/ootle";

// Withdraw from account
const tx = new AccountInvokeBuilder(Network.Esmeralda, accountAddress)
  .feeTransactionPayFromComponent(accountAddress, 1000n)
  .publicTransfer(accountAddress, resourceAddress, 500n, recipientAddress)
  .build();

// Take faucet funds
const tx = new FaucetInvokeBuilder(Network.Esmeralda, faucetAddress)
  .feeTransactionPayFromComponent(accountAddress, 1000n)
  .takeFaucetFunds(accountAddress, 10_000n)
  .build();

defaultIndexerUrl(network)

Returns the well-known indexer URL for a network. Currently returns URLs for LocalNet and Esmeralda; throws for others.

import { defaultIndexerUrl, Network } from "@tari-project/ootle";

const url = defaultIndexerUrl(Network.Esmeralda);
// "https://ootle-indexer-a.tari.com"

@tari-project/ootle-indexer

Provider implementation backed by the indexer REST API. Wraps @tari-project/indexer-client with the SDK's Provider interface and adds SSE-based transaction watching.

import {
  IndexerProvider,
  ProviderBuilder,
  IndexerClient,
  TransactionWatcher,
  PendingTransaction,
  resolveWantInputs,
} from "@tari-project/ootle-indexer";
import type { WantInput, TransactionEntry, TemplateMetadata } from "@tari-project/ootle-indexer";

ProviderBuilder

Fluent factory for IndexerProvider. Falls back to defaultIndexerUrl when no URL is set.

const provider = await ProviderBuilder.new()
  .withNetwork(Network.Esmeralda)
  .withUrl("http://my-indexer:18300") // optional — defaults to known URL
  .withTransactionTimeoutMs(60_000)
  .connect();

IndexerProvider

// Connect
const provider = await IndexerProvider.connect({ url, network });

// Read chain state
const substate = await provider.getSubstate("component_0x…");
const substates = await provider.fetchSubstates([id1, id2]);
const template = await provider.getTemplateDefinition(templateAddress);
const list = await provider.listRecentTransactions({ limit: 5, last_id: null });

// Submit
const { transaction_id } = await provider.submitTransaction(envelope);

// Watch for finalization via SSE (falls back to polling on timeout)
const outcome = await provider.watchTransactionSSE(transaction_id).watch();

// Full receipt (after watching)
const receipt = await provider.watchTransactionSSE(transaction_id).getReceipt();

// Stop the SSE watcher when done
provider.stopWatcher();

TransactionWatcher and PendingTransaction

The TransactionWatcher maintains a persistent SSE connection to the indexer's /events endpoint and routes TransactionFinalized events to registered waiters. It starts lazily on the first watch() call and can be shared across many transactions.

import { TransactionWatcher } from "@tari-project/ootle-indexer";

const watcher = new TransactionWatcher("http://localhost:18300");
watcher.start();

// Submit your transaction, then:
const pending = watcher.watch(txId, client, 32_000);
const outcome = await pending.watch(); // SSE-first, poll fallback
const receipt = await pending.getReceipt(); // raw indexer response

watcher.stop();

PendingTransaction.watch() returns a TransactionOutcome and does not throw on FeeIntentCommit or Reject — the caller decides how to handle each outcome.

WantInput and resolveWantInputs

Lazily resolve inputs by querying the indexer rather than supplying exact versions upfront.

import { resolveWantInputs } from "@tari-project/ootle-indexer";
import type { WantInput } from "@tari-project/ootle-indexer";

const wants: WantInput[] = [
  { type: "SpecificSubstate", substateId: "component_0x…" },
  { type: "VaultForResource", resourceAddress: "resource_0x…" },
];

const inputs = await resolveWantInputs(provider.getClient(), wants);
// inputs: SubstateRequirement[] with versions filled in

@tari-project/ootle-secret-key-wallet

Local signer that holds secret key material in JavaScript memory and uses @tari-project/ootle-wasm for all cryptographic operations.

Warning: The secret key lives unencrypted in memory. For production use, prefer WalletDaemonSigner so the key never touches JavaScript.

import { SecretKeyWallet, EphemeralKeySigner } from "@tari-project/ootle-secret-key-wallet";

SecretKeyWallet

Implements Signer. Holds an account secret key and an optional view-only key (required for stealth output scanning).

// Generate a new random wallet with a view-only key (for stealth support)
const wallet = SecretKeyWallet.randomWithViewKey(Network.Esmeralda);

// Restore from a stored secret key (Uint8Array)
const wallet = SecretKeyWallet.fromSecretKey(ownerSecretKey, Network.Esmeralda);

// Restore with both account key and view-only key
const wallet = SecretKeyWallet.fromSecretKey(ownerSecretKey, Network.Esmeralda, viewOnlySecretKey);

// Restore from both secret and public keys (e.g. from a keystore)
const wallet = SecretKeyWallet.fromKeypair(ownerSecretKey, publicKey, Network.Esmeralda);

// With view-only key for stealth
const wallet = SecretKeyWallet.fromKeypair(ownerSecretKey, publicKey, Network.Esmeralda, viewOnlySecretKey);

// Sign a transaction
const signatures = await wallet.signTransaction(unsignedTx);

// Access view-only key (for scanning stealth outputs)
const viewKey = wallet.getViewOnlySecret();

EphemeralKeySigner

Generates a one-time throwaway keypair. Used in privacy-preserving transactions where no link to the sender's identity should exist. The key is discarded when the object is garbage-collected.

const signer = EphemeralKeySigner.generate(); // defaults to Esmeralda
const signed = await signTransaction([signer], unsignedTx);

@tari-project/ootle-wallet-daemon-signer

Delegates signing to a running tari_ootle_walletd process via @tari-project/wallet_jrpc_client. The secret key never enters JavaScript memory.

import { WalletDaemonSigner } from "@tari-project/ootle-wallet-daemon-signer";
import type { WalletDaemonSignerOptions } from "@tari-project/ootle-wallet-daemon-signer";
const options: WalletDaemonSignerOptions = {
  url: "http://localhost:18103",
  authToken: "your-auth-token",
};

// Connect and cache account info
const signer = await WalletDaemonSigner.connect(options);

const address = await signer.getAddress();
const publicKey = await signer.getPublicKey();

// Sign a transaction — the daemon returns signatures, the key stays on the daemon
const signatures = await signer.signTransaction(unsignedTx);

To start the wallet daemon:

./tari_ootle_walletd --network esme

Stealth transfers

ootle.ts includes a WASM-backed confidential (stealth) transfer stack. Amounts are hidden in Pedersen commitments and each output carries an encrypted payload only the recipient (who holds the matching view-only key) can scan and unblind.

import {
  StealthTransfer,
  WalletStealthAuthorizer,
  OotleWallet,
  createOutput,
  submitTransaction,
  watchTransaction,
} from "@tari-project/ootle";

// 1. Build: withdraw revealed funds, emit a confidential output (+ optional revealed change).
const spec = await new StealthTransfer(provider, resourceAddress)
  .spendRevealedInput(sourceAccount, 5_000_000n)
  .toStealthOutput(createOutput({ destination: recipientAddress, amount: 3_000_000n, resourceAddress }))
  .toRevealedOutput(2_000_000n)
  .payFeeFromRevealed(1_000n)
  .prepare();

// 2. Authorize with a multi-signer wallet (supplies the account-key signature).
const wallet = new OotleWallet();
wallet.registerKeyProvider(senderAddress, secretKeyWallet);
wallet.setDefaultSigner(senderAddress);

const authorizer = WalletStealthAuthorizer.fromSpec(wallet, spec);

// 3. Prepare (hydrate the balance proof), seal, submit, watch.
await authorizer.prepare(provider);
const envelope = await authorizer.seal(provider);
const txId = await submitTransaction(provider, envelope);
await watchTransaction(provider, txId);

To receive, decrypt a fetched UTXO with your view secret via decryptOwnedUtxo (returns null when the output is not yours). To spend a confidential UTXO, register it with .spendStealthInput(ownerAddress, commitment) and pass the owner's viewSecret to WalletStealthAuthorizer.fromSpec. Client-side scan/spend requires a SecretKeyWallet created with a view key (SecretKeyWallet.randomWithViewKey(network)).

See the Stealth Transfers guide for the full receive / send / spend walkthrough.


Examples

Four React + Vite example apps are included under examples/.

connect-button

Minimal wallet connection UI. Connects to a running wallet daemon and displays the account address and public key.

cd examples/connect-button
pnpm dev

Requires tari_ootle_walletd running locally. Default endpoint: http://127.0.0.1:9000/json_rpc.

indexer-explorer

Browse on-chain state. Look up substates by ID, or browse recent transactions from the indexer.

cd examples/indexer-explorer
pnpm dev

Pre-configured to connect to the public Esmeralda testnet indexer. No local setup required.

template-inspector

Browse published template ABIs. Lists all templates cached by the indexer and renders their function definitions, argument types, and return values.

cd examples/template-inspector
pnpm dev

stealth-wallet

Stealth receive/decrypt/send demo backed by SecretKeyWallet. Generates a fresh stealth-capable wallet, faucets a confidential deposit, decrypts the owned UTXO, and sends a stealth transfer.

cd examples/stealth-wallet
pnpm dev

Requires a LocalNet indexer + faucet reachable from the browser. See examples/stealth-wallet/README.md for prerequisites.


Development

This repo uses pnpm workspaces. You'll need Node.js 22+ and pnpm 10+.

Setup

# Clone and install
git clone https://github.com/tari-project/ootle.ts.git
cd ootle.ts
pnpm install

Build

# Build all SDK packages
pnpm -r build

# Build a specific package
pnpm --filter @tari-project/ootle build

Test

# Run all package tests
pnpm -r test

# Run a single package's tests
pnpm --filter @tari-project/ootle run test

# Watch mode (from a package directory)
cd packages/ootle && pnpm vitest

Lint and format

# ESLint + Prettier across all packages
pnpm lint

# Check for unused exports and dependencies
pnpm knip

Documentation

The documentation site uses Starlight (Astro) with auto-generated API reference via TypeDoc.

# Build the docs site (outputs to docs/dist/)
pnpm docs

# Run the docs dev server with hot reload
pnpm docs:dev

The API reference is generated from the TypeScript source of all four SDK packages using a dedicated tsconfig.typedoc.json. Hand-written guides live in docs/src/content/docs/.

Run an example

cd examples/connect-button   # or indexer-explorer, template-inspector
pnpm install
pnpm dev

See each example's own README for prerequisites (e.g. running a wallet daemon).

Clean everything

./scripts/clean_everything.sh

Repository structure

ootle.ts/
├── packages/
│   ├── ootle/                        Core SDK (builder, types, transaction flow)
│   ├── ootle-indexer/                Indexer REST provider
│   ├── ootle-secret-key-wallet/      Local in-memory signer (testing)
│   └── ootle-wallet-daemon-signer/   Remote wallet daemon signer
├── examples/
│   ├── connect-button/               Wallet connection demo
│   ├── indexer-explorer/             Read-only transaction/substate browser
│   └── template-inspector/           Template ABI viewer
├── docs/                             Starlight documentation site
├── scripts/                          CI and utility scripts
└── .github/workflows/                CI, docs deploy, npm publish

Contributing

Contributions are welcome! Here's how to get involved.

Workflow

  1. Fork the repo and create a branch from main
  2. pnpm install && pnpm -r build — make sure the baseline builds
  3. Make your changes
  4. pnpm lint — fix any lint errors
  5. pnpm -r test — ensure all tests pass
  6. pnpm knip — check for unused exports or dependencies
  7. Open a pull request against main

CI checks

Every PR runs these GitHub Actions automatically:

| Workflow | What it does | | ------------------ | ---------------------------------------------------------------------------- | | CI | Builds all packages | | Lint | Runs ESLint + Prettier | | Docs test | Verifies the documentation site builds | | PR title | Enforces Conventional Commits format | | Signed commits | Verifies commits are signed |

Conventions

  • TypeScript strict mode — all packages use "strict": true
  • ESLint flat config — shared root config extended by each package
  • Prettier — 120-char lines, double quotes, trailing commas
  • Commit messages — follow Conventional Commits (enforced by CI)
  • No default exports — use named exports everywhere

Deployment

  • npm: packages are auto-published to npm on push to main when their version number changes
  • Docs: the documentation site auto-deploys to GitHub Pages on push to main

License

BSD 3-Clause — see LICENSE.