Package Exports
- sui.ski
- sui.ski/assets/au.png
- sui.ski/assets/basmr.mp4
- sui.ski/assets/basmr.webm
- sui.ski/assets/btc-icon.svg
- sui.ski/assets/diamond-192.png
- sui.ski/assets/iusd-256.png
- sui.ski/assets/iusd.png
- sui.ski/assets/iusd.svg
- sui.ski/assets/ski-192.png
- sui.ski/assets/ski-512.png
- sui.ski/assets/ski-idle.gif
- sui.ski/assets/ski-idle.mp4
- sui.ski/assets/ski-idle.webm
- sui.ski/assets/ski.svg
- sui.ski/assets/sol-icon.svg
- sui.ski/assets/sui-drop.svg
- sui.ski/assets/sui-icon.svg
- sui.ski/assets/sui-ski-qr.svg
- sui.ski/assets/tbai.svg
- sui.ski/assets/waap-icon.svg
- sui.ski/assets/waap-qr.svg
- sui.ski/embed
- sui.ski/styles
Readme
.SKI — .Sui Key-In
.SKI once, everywhere.
What .SKI Is
.SKI is a Sui-native messaging and cross-chain identity dApp where SuiNS names are communication endpoints. You send a thunder to @name and it lands as an encrypted storm conversation whose ciphertext lives on Walrus and whose keys live behind Seal. Every wallet, agent, and cross-chain address is IKA-native — no private keys sit on Cloudflare Workers, ever. The same UI rails carry Thunder messages, Thunder IOU transfers, shielded Pedersen commitments, Shade grace-period snipes, and SUIAMI cross-chain identity proofs.
Agents (ultron, chronicoms, t2000s) are Durable Objects that sign via IKA dWallet user shares + DWalletCap wrappers. brando.sui runs DKG in the browser and re-encrypts the user share to the agent. Either brando OR agent + IKA network = a valid signature.
For Hackathon Devs (Frontier Colosseum)
SKI is the best IKA-based Web2 social login experience you can build with today. Here's what you get out of the box:
WaaP — Wallet as a Protocol
Sign in with X.com, Google, phone, or email. No API keys. No seed phrases. No wallet extensions. It just works. If you're a dev, you know how amazing "no API keys" is.
Rumble Your Squids
One button runs IKA DKG and provisions native Bitcoin, Solana, and EVM dWallet addresses — all from a single Sui account. Not wrapped. Not bridged. Real cross-chain keys. This is built and live on mainnet today.
Anti-Spam — Stables Only
SKI is inherently anti-spam. Everything is measured in stables. You have 8 billion MONKE PENIS coins? SKI checks all exchanges, sees it's worth less than a cent, and filters it out — or auto-swaps it to USD (or Argentine Peso). No more 0.000256 CHODE in your portfolio.
Subcent Intents
The 6-7 digits after $0.01 aren't noise — they're steganographic intent tags:
- Which chain are you swapping to?
- What is the receiving address?
- Other privately encoded data only legible to the participants
One number carries the instruction. No metadata leaks.
What You Can Build With
- Tradeport — SuiNS marketplace listing proxy (
/api/tradeport/listing/:label) - Walrus — Seal-encrypted blob storage for Thunder ciphertext, Shade payloads, quilted batch writes
- Solana/Helius — Webhook-driven deposit watchers, SPL token resolution, Jupiter routing
- Seal — 2-of-3 threshold encryption (Overclock, Studio Mirai, H2O Nodes key servers)
- IKA dWallets — Native cross-chain signing, batch DKG, DWalletCap wrappers
Native Cross-Chain Wallets via IKA dWallets
Real Bitcoin, Ethereum, and Solana addresses controlled by your Sui account — no bridges, no wrapping, no custodians. Powered by IKA's 2PC-MPC threshold signatures.
What One Sui Account Controls
| Curve | Chains | Address Format |
|---|---|---|
| secp256k1 (1 DKG) | Bitcoin, Ethereum, Base, Polygon, Arbitrum, Optimism, Tron (USDT only) | bc1q..., 0x..., T... |
| ed25519 (1 DKG) | Solana | base58 |
Two DKG ceremonies. Two dWallets. Eight chains. One Sui account.
Why This Matters
- No bridges — BTC stays on Bitcoin, SOL stays on Solana. IKA generates real native addresses whose signing is governed by Sui smart contracts.
- Non-collusive security — 2PC-MPC means neither the user nor the network can sign alone. 100+ mainnet operators with Byzantine threshold.
- Quantum-ready architecture — Sui's flag-byte signature scheme lets the network add post-quantum primitives via a new flag byte — no hard fork, no address migration. See
docs/ika-quantum-resistance.md.
First Commandment: IKA-Native, Keyless Agents
- Every wallet, agent, and cross-chain address MUST be IKA-native.
- No private keys on Cloudflare Workers — ever. Agents sign via IKA dWallet user shares + DWalletCap wrapper.
- Cross-chain addresses (BTC, ETH, SOL) come from IKA dWallet DKG — always. No raw keypair re-encoding as cross-chain addresses.
brando.suiruns DKG in-browser, re-encrypts user share to the agent. Either brando OR agent + IKA network = valid signature.- Batch DKG provisioning for agents = "Rumble your squids."
- If a dWallet doesn't exist yet, the feature is blocked until DKG is run — no shortcuts.
Terminology
| Word | Meaning |
|---|---|
| Storm | A conversation (not channel/group) |
| Thunder | A message or signal |
| Quest | The act of reading/opening |
| Purge | Delete-on-read (never "decrypt") |
| Cache | Fund storage (never "treasury/reserve/dao") |
| Rumble | IKA DKG ceremony (never multi-token swap routing) |
| SKI Pass | Access/membership proof |
| Stables | Dollar-pegged value (never "stablecoins") |
| Sibyl | The predictor (never "Sybil") |
| chain@name | Address format: sol@ultron, eth@brando, btc@stables |
| encrypt / decrypt | Always verb forms, never "encrypted/encryption" |
Core Principles
- IKA-native, keyless agents. Every cross-chain address is IKA dWallet derived. No private keys on workers. Batch DKG = "Rumble your squids."
- Cache, not treasury. Funds flow through caches — high-performance temporary stores.
- Stables, not stablecoins. iUSD is a stable backed by activity yield.
- Encrypt, not encrypted. Use verb forms — encrypt/decrypt.
Domain Structure
| Domain | Purpose |
|---|---|
sui.ski |
Root — main application, embeddable widget, API endpoints |
<name>.sui.ski |
SuiNS profile pages (e.g. brando.sui.ski) |
Cross-domain session cookie (ski:xdomain) on domain=sui.ski for auth persistence across subdomains.
UI Overview
The .SKI header bar renders four elements:
- Dot — wallet status shape (green circle = unconnected, black diamond = connected, blue square = has SuiNS name). Toggles modal/menu.
- Profile pill — wallet icon, social badge, SuiNS name + squid emoji (IKA status), live balance. Clicks to
.sui.skiprofile page. - SKI button — branded button. Three-state cycle: menu → idle overlay → menu.
- Balance pill — live USD balance. Click to cycle SUI/USD display.
Idle Overlay
After 15s of inactivity (or via SKI button cycle), the menu collapses into a compact overlay with:
- Pixel art video (cached via Cache API for instant replay)
- Name search input with full SuiNS resolution
- Squids rows — styled SUI/BTC/SOL/ETH/Base/Tron address rows with chain-colored icons, per-row USD balances, toggle-select to copy
- Rumble button — runs IKA DKG to provision all chain wallets
- Thunder messaging row
- Version badge linking to npm
The overlay restores instantly on hard refresh via ski:last-address localStorage fallback, with IKA addresses cached to ski:ika-addrs:${address}.
SKI Modal
Single-column overlay with key detail pane, Splash legend (keys grouped by shape tier), wallet list, and WaaP social login row. Long-press lock (2.2s) pins a wallet. Layout toggle persists preference.
SKI Menu
Dropdown with SuiNS name management, marketplace purchase (Tradeport/kiosk), Shade orders, Thunder messaging, coin chip swaps, SUIAMI identity proofs, and key management.
Thunder — Seal-Encrypted Messaging
Thunder is a thin wrapper around @mysten/sui-stack-messaging with a custom Cloudflare Durable Object relayer (TimestreamAgent).
- Encryption — Seal 2-of-3 threshold DEK + AES-GCM envelope. Relayer never sees plaintext. Key servers verified on first encrypt (
verifyKeyServers: true). - Transport —
TimestreamAgentDO, one instance pergroupId, speaks HTTP to/api/timestream/:groupId/:action(send,fetch,fetch-one,update,delete,add-participant). - Identity — SuiNS names (
alice.sui) resolved via SuinsClient to target address / NFT owner. - Storm — on-chain
PermissionedGroup<Messaging>(upstream Move package) anchors key-version history and membership; messages live off-chain in the DO. - Global SUIAMI Storm —
0xfe23aad02ff15935b09249b4c5369bcd85f02ce157f54f94a3e7cc6dfa10a6e8(uuidsuiami-global), public identity directory. Joining is a public act by design. - Seal servers — Overclock, Studio Mirai, H2O Nodes (mainnet, open/free). 2-of-3 threshold.
Privacy Phase 1 (2026-04-10)
- No plaintext fallback —
encryptWithRetryretries up to 5× and fails the send rather than storing cleartext. Transfer notes also encrypted. - Message padding — plaintext padded to fixed buckets
[256, 1024, 4096, 16384]bytes before Seal, killing the ciphertext-length side channel. - Timestamp jitter — DO rounds
createdAt/updatedAtto 10s buckets + ±5s noise on ingest. Monotonicorderpreserves UI sort. - Sender index on the wire — new messages store
senderIndex(position in the DO's participant list) instead of the raw Sui address. A dump ofmessagesno longer reveals who authored what. - Attachment guard — DO rejects
attachments: [...]at the send boundary; our transport does not round-trip them, and silently accepting would leak blob IDs outside the Seal envelope.
Remaining Phase 2 items (sealed sender, non-derivable group IDs, encrypted membership) are deferred — see docs/superpowers/specs/2026-04-10-thunder-privacy-audit-and-roadmap.md.
Thunder IOU + Thunder IOU Shielded
Thunder carries value as well as text. A $amount in a thunder composes a token transfer into the same PTB as the signal.
- Thunder IOU — plain escrowed transfers with recall + expiry. Package:
0x5a80b9753d6ccce11dc1f9a5039d9430d3e43a216f82f957ef11df9cb5c4dc79. - Thunder IOU Shielded — BLS12-381 Pedersen commitment transfers. Amounts are hidden on-chain; the recipient decrypts a sealed opening via Seal and reveals via a zero-knowledge verification in Move. Package:
0x3b1dcced3f585157f48afd14a84f42e65ee57dd38be9dd73d7d94a0a1b690782. - Dust gate — sub-cent sends bypass the shielded path (per
Diglett Lv.18) and route to plain IOU. - Batching —
buildShieldedDepositManyTx(Dugtrio Lv.36) and the in-flight Mr. Rime Lv.52 aggregate-vault evolution batch multiple deposits in one PTB.
Active IOU work in the Pokedex covers the activate/redeem UI (Pikachu), iUSD-yield escrow (Typhlosion), iUSD gas (Ampharos), Seal-encrypted sealed_memo (Magnezone), multi-token Iou<T> (Garchomp), and the IKA cross-chain activate to ETH/SOL/BTC (Metagross).
SUIAMI
SUI-Authenticated Message Identity — cryptographic proof that a SuiNS name belongs to you. Verified server-side via /api/suiami/verify.
SUIAMI Roster
Cross-chain identity resolver. Maps SuiNS names to BTC/ETH/SOL addresses via IKA dWallets in a shared on-chain registry with reverse lookup.
v2 Package: 0xef4fa3fa12a1413cf998ea8b03348281bb9edd09f21a0a245a42b103a2e9c3b4
SUIAMI Reciprocal Roster
Seal-encrypted cross-chain identity exchange. No cleartext cross-chain addresses ever appear on-chain — only the SUI address (already public) and a Walrus blob ID.
How it works:
- User proves SUIAMI ownership (signed message + SuiNS NFT verification)
- Cross-chain addresses (BTC/ETH/SOL) are AES-GCM encrypted and uploaded to Walrus as a blob
- Only the blob ID is written to the on-chain roster — no cleartext addresses on-chain
- Decryption is Storm-gated via Seal policy (
seal_approve_roster_reader): you must hold a valid SUIAMI proof to decrypt anyone's addresses - Reciprocal: reading someone's roster entry requires your own SUIAMI proof, which auto-writes your entry if it doesn't exist yet
- Viral: every lookup adds a new roster entry, growing the network organically
Contracts:
- Roster Package:
0x2c1d63b3b314f9b6e96c33e9a3bca4faaa79a69a5729e5d2e8ac09d70e1052fa - Roster Object:
0x30b45c51a34b20b5ab99e8c493a82c332e9502e5f4380d1be6cc79e712eaab1d - Seal Policy:
seal_approve_roster_reader
npm: suiami@0.2.0
Key files:
contracts/suiami/sources/roster.move— on-chain roster contractcontracts/suiami/sources/seal_roster.move— Seal decryption policysrc/client/roster.ts— Walrus blob upload/fetch with AES-GCM encryptionsrc/suins.ts—readRosterByAddress,maybeAppendRosterscripts/deploy-suiami-storm.ts— one-time global Storm deployment
Shade
Privacy-preserving SuiNS grace-period domain sniping. Commitment-reveal hides domain/target/timing on-chain until execution. Seal encryption for payload privacy.
- ShadeExecutorAgent — Cloudflare DO auto-executes at grace expiry via alarms
execute()— permissionless; anyone with the preimage can call- Three routes: SUI→NS, SUI→USDC→NS, SUI direct fallback
Contract: 0xfcd0b2b4f69758cd3ed0d35a55335417cac6304017c3c5d9a5aaff75c367aaff
See SHIELD.md for the security model.
iUSD — Yield-Bearing Stable
Dollar-pegged stable backed by diversified reserves (gold, silver, equities, energy, dollar instruments) custodied natively across BTC, ETH, SOL, and SUI via IKA dWallet threshold signatures.
Reserve Composition
| Tranche | Assets | Target |
|---|---|---|
| Senior (60%) | USDC, BUIDL (T-bills), staked SUI/SOL | ≥100% of supply |
| Junior (40%) | XAUM, XAGM, TSLAx/NVDAx/SPYx, BTC, crude | Absorbs losses first |
150% minimum collateral ratio. 9-decimal steganographic encoding fingerprints every mint.
v2 Package: 0x2c5653668edefe2a782bf755e02bda56149e7b65b56f6245fb75b718941d2ec9
OpenCLOB — Cross-Chain Order Book
Phase 3a is live: the thunder_openclob::bundle Move package is published and the treasury-agents scanner watches bundles over GraphQL. Sub-cent steganographic tags act as order-matching keys. Phase 3b adds a client-side bundle builder (merged) and a keeper settlement path (in-flight).
Package: 0xdcbabe3d80cd9b421113f66f2a1287daa8259f5c02861c33e7cc92fc542af0d7
See docs/superpowers/specs/2026-04-11-openclob-bundle-tags.md.
Pokemon Swarm
.SKI runs a Pokedex coordinator Durable Object that watches GitHub issues, branches, and PRs as a live swarm. Per the naming convention: legendary Pokemon are releases, regular Pokemon with level tags are commits and issues, merged PRs are evolutions. Recent evolutions include Raichu Lv.40 (ultron-sponsored thunder gas), Mr. Mime Lv.42 (shielded Pedersen transfers), Alakazam Lv.36 (forward secrecy via DEK rotation), Psyduck Lv.22 (collected-pill fix), Diglett Lv.18 (dust gate), and Dugtrio Lv.36 (batched shielded deposits).
The Pokedex DO is bound as PokedexAgent and exposes /api/pokedex/* routes. See docs/superpowers/specs/2026-04-11-pokemon-swarm-agents.md.
Upcoming — Ghost Line for Colosseum Frontier
Active in-flight work for the Ika + Encrypt side track of Colosseum Frontier. Submission deadline approximately 2026-05-11; side-track prize pool $15K USDC via Superteam Earn, main track $2.5M+.
Thesis: native Ika dWallets on Solana plus Encrypt FHE claim logic equals bridgeless and encrypted capital markets on Solana, extending the existing Thunder and Storm UX to cross-chain. We keep the same storm → thunder → claim shell users already know, and swap the substrate to Ika pre-alpha on Solana (solana-pre-alpha.ika.xyz) and Encrypt (docs.encrypt.xyz).
Ghost line issues:
- #101 Gastly Lv.20 — Solana redeem UI scaffold (stage 1)
- #102 Haunter Lv.40 — Sui-side claim + swap + burn + poltergeist keeper pickup (stage 2)
- #103 Gengar Lv.55 — feature-complete Solana redeem for encrypt.xyz Colosseum (stage 3)
- #104 Shelgon Lv.45 — shadow-DKG provisioning; keeper runs DKG alone, user share encrypted to recipient pubkey
- #105 Salamence Lv.65 — user reclaims shadow dWallet on first login
Demo target: a judge opens a phone at the Colosseum stage, receives a live $5 thunder, claims it as native USDC-SPL on sol@name, and sees it land in Phantom within 30 seconds. Zero bridges. The amount is never visible on-chain until claim execution inside an Encrypt FHE function. The Solana-side dWallet is provisioned on-demand via Ika.
Sibyl — The Predictor
Custom oracle. Timestreams flow price through time. Pythia (ultron.sui) channels visions. Offerings flow to the iUSD cache. Sibyl's Court: Anthropologists (research), Hunters (iUSD yield), Rogues (IKA squid breeding).
ultron.sui — Autonomous Agent
Address: 0xa84cebfde3f0522cd893263d5208a633cd226a1585249b32f02d77438094b3c3
Keeper wallet for all server-side signing: iUSD minting, Shade execution, Thunder relay, dust sweeps, fee collection.
Deployed Contracts
| Contract | Package / Object |
|---|---|
| iUSD v2 | 0x2c5653668edefe2a782bf755e02bda56149e7b65b56f6245fb75b718941d2ec9 |
| SUIAMI Roster v3 (reciprocal) | 0x2c1d63b3b314f9b6e96c33e9a3bca4faaa79a69a5729e5d2e8ac09d70e1052fa |
| SUIAMI Roster object | 0x30b45c51a34b20b5ab99e8c493a82c332e9502e5f4380d1be6cc79e712eaab1d |
| Global SUIAMI Storm | 0xfe23aad02ff15935b09249b4c5369bcd85f02ce157f54f94a3e7cc6dfa10a6e8 |
| Shade | 0xfcd0b2b4f69758cd3ed0d35a55335417cac6304017c3c5d9a5aaff75c367aaff |
| Ignite | 0x66a44a869fe8ea7354620f7c356514efc30490679aa5cb24b453480e97790677 |
| Thunder IOU | 0x5a80b9753d6ccce11dc1f9a5039d9430d3e43a216f82f957ef11df9cb5c4dc79 |
| Thunder IOU Shielded | 0x3b1dcced3f585157f48afd14a84f42e65ee57dd38be9dd73d7d94a0a1b690782 |
| Thunder OpenCLOB (bundle) | 0xdcbabe3d80cd9b421113f66f2a1287daa8259f5c02861c33e7cc92fc542af0d7 |
Thunder uses the upstream @mysten/sui-stack-messaging Move package; no custom Thunder or Storm contract. An earlier hybrid-stack package (0xa3ed4fdf...cdf942) was published but never wired into the client and has been stripped from the source tree.
What's Next — Live Pokedex
The authoritative roadmap is the GitHub issue tracker, watched by the Pokedex DO and surfaced via /api/pokedex/*. Major in-flight lines:
- Ghost (#101-105) — Gastly / Haunter / Gengar Solana redeem for Colosseum Frontier; Shelgon / Salamence shadow-DKG provisioning
- Dragon (#78-79) — Garchomp multi-token
Iou<T>, Metagross IKA cross-chain activate - Electric (#76, #80) — Ampharos iUSD gas, Electivire decentralized activity feed via IOU events
- Fire (#75) — Typhlosion iUSD yield on escrowed IOU balances
- Psychic (#77, #94) — Magnezone Seal-encrypted
sealed_memo, Mr. Rime aggregate shielded vault - Thunder v4 (#63, #68-71) — migration to Sui Stack Messaging SDK, compose-preview UX, composable PTB alongside signal
- Security (#54-58) — session cookie hardening, innerHTML XSS sweep, rate limiting, localStorage TTL
Closed recently: Raichu Lv.40 (ultron-sponsored thunder gas), Mr. Mime Lv.42 (shielded Pedersen transfers), Alakazam Lv.36 (DEK rotation forward secrecy), Jolteon Lv.25 (WebSocket thunder subscribe), Diglett Lv.18 (dust gate), and Psyduck Lv.22 (collected pill).
Install
npm install sui.ski
# or
bun add sui.skiEmbed via script tag
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/sui.ski/public/styles.css">
<script type="module" src="https://cdn.jsdelivr.net/npm/sui.ski/public/dist/ski.js"></script>Add the widget markup:
<div class="ski-header">
<div class="ski-wallet" id="ski-wallet">
<button class="ski-btn ski-dot" id="ski-dot" style="display:none"></button>
<div id="ski-profile"></div>
<button class="ski-btn" id="ski-btn" style="display:none"></button>
<div id="ski-menu"></div>
</div>
</div>
<div id="ski-modal"></div>Auto-initializes on load.
Embed via bundler
import 'sui.ski';Same DOM markup required.
Events
window.addEventListener('ski:wallet-connected', (e: CustomEvent) => {
const { address, walletName } = e.detail;
});
window.addEventListener('ski:wallet-disconnected', () => {});
window.dispatchEvent(new CustomEvent('ski:request-signin'));Requesting a transaction
import { Transaction } from '@mysten/sui/transactions';
const tx = new Transaction();
// ... build your transaction
window.dispatchEvent(new CustomEvent('ski:sign-and-execute-transaction', {
detail: { transaction: tx, requestId: 'my-req-1' }
}));
window.addEventListener('ski:transaction-result', (e: CustomEvent) => {
const { requestId, success, digest, error } = e.detail;
});Splash sponsorship is applied automatically when active.
Self-Hosting
bun install
npx wrangler login
bun run build && npx wrangler deployNever skip the deploy step. Never use bun run deploy — only npx wrangler deploy. Two workers back the app: dotski (agents, treasury, messaging DOs) and sui-ski (subnames, auth).
Durable Objects
| Binding | Purpose |
|---|---|
SessionAgent |
Signed session verification |
SponsorAgent |
Splash sponsor state |
SplashDeviceAgent |
Per-device Splash activation |
ShadeExecutorAgent |
Auto-executes Shade orders at grace expiry |
TreasuryAgents |
ultron.sui — iUSD minting, collateral, NS acquisition, Thunder relay |
Chronicom |
Per-wallet thunder signal watcher with cached counts |
TimestreamAgent |
Per-group Thunder message storage (Seal-encrypted, one DO per groupId) |
API Routes
| Route | Purpose |
|---|---|
/agents/* |
WebSocket upgrade for DO agents |
/api/health |
Health check |
/api/shade/* |
Shade order management |
/api/suiami/verify |
SUIAMI proof verification (GraphQL-backed) |
/api/timestream/:groupId/:action |
Thunder transport — Seal-encrypted message DO (send/fetch/fetch-one/update/delete/add-participant) |
/api/thunder/chronicom |
Per-wallet signal count cache |
/api/tradeport/listing/:label |
Tradeport listing proxy |
Stack
@mysten/sui^2.13.0,@mysten/suins^1.0.2,@human.tech/waap-sdk1.3.0,@ika.xyz/sdk0.3.1- Messaging:
@mysten/sui-stack-messaging^0.0.2,@mysten/sui-groups^0.0.1 (Thunder) - Encryption:
@mysten/seal^1.1.1 (2-of-3 threshold — Overclock, Studio Mirai, H2O Nodes) - Storage:
@mysten/walrus^1.1.0 (blobs + quilted batch writes) - Solana: Helius (RPC + webhook deposit watchers), Jupiter (routing), Kamino (lending)
- DEX:
aftermath-ts-sdk(aggregation), DeepBook v3, Bluefin CLMM, Cetus CLMM - Marketplace: Tradeport (SuiNS listing proxy)
- Transport:
SuiGrpcClientprimary,SuiGraphQLClientfallback — no JSON-RPC (sunsets April 2026). Cloudflare Workers and DOs cannot speak gRPC (no HTTP/2 bidi streaming), so server code uses GraphQL. GraphQL is read-only, sosui_executeTransactionBlocksubmission falls back through PublicNode → BlockVision → Ankr. - Build:
bun build src/ski.ts --outdir public/dist --target browserwith JSON import for version injection - Deploy: Cloudflare Workers + Durable Objects (
dotski+sui-ski)
Local Development
bun install
bun run dev # watches src/ski.ts
bun run dev:wrangler # wrangler dev with hot reloadLicense
MIT