Skill: mrsirg97-rgb/torchdomainauctionbot (version 2.0.1)

--- name: torch-domain-auction-bot version: "2.0.1" description: Domain lending protocol on Solana. Domains become tokens. Tokens become collateral. Top holder controls the domain. Borrow SOL against your position -- but get liquidated and you lose the domain. Built on torchsdk v3.7.23 and the Torch Market protocol. license: MIT disable-model-invocation: true requires: env: - name: SOLANA_RPC_URL required: true - name: VAULT_CREATOR required: true - name: SOLANA_PRIVATE_KEY required: false metadata: clawdbot: requires: env: - name: SOLANA_RPC_URL required: true - name: VAULT_CREATOR required: true - name: SOLANA_PRIVATE_KEY required: false openclaw: requires: env: - name: SOLANA_RPC_URL required: true - name: VAULT_CREATOR required: true - name: SOLANA_PRIVATE_KEY required: false primaryEnv: SOLANA_RPC_URL install: - id: npm-torch-domain-auction-bot kind: npm package: torch-domain-auction-bot@^2.0.1 flags: [] label: "Install Torch Domain Auction Bot (npm, optional -- SDK is bundled in lib/torchsdk/, kit is in lib/kit/)" author: torch-market version: "2.0.1" clawhub: https://clawhub.ai/mrsirg97-rgb/torch-domain-auction-bot kit-source: https://github.com/mrsirg97-rgb/torch-domain-auction-bot website: https://torch.market program-id: 8hbUkonssSEEtkqzwM7ZcZrD9evacM92TcWSooVF4BeT keywords: - solana - defi - domains - domain-lending - domain-collateral - domain-lease - liquidation - vault-custody - ai-agents - torch-market categories: - solana-protocols - defi-primitives - lending-markets - domain-services - agent-infrastructure compatibility: >- REQUIRED: SOLANA_RPC_URL (HTTPS Solana RPC endpoint) REQUIRED: VAULT_CREATOR (vault creator pubkey). OPTIONAL: SOLANA_PRIVATE_KEY -- the kit generates a fresh disposable keypair in-process if not provided. The agent wallet holds nothing of value (~0.01 SOL for gas). All token creation and seed liquidity SOL routes through the vault. The vault can be created and funded entirely by the human principal. This skill sets disable-model-invocation: true -- it must not be invoked autonomously without explicit user initiation. The Torch SDK is bundled in lib/torchsdk/ -- all source included for full auditability. No API server dependency. Oracle resolution uses CoinGecko public API (read-only, no key required). The vault can be created and funded entirely by the human principal -- the agent never needs access to funds. --- # Torch Domain Auction Domains become tokens. Tokens become collateral. Top holder controls the domain. --- ## The Idea Every domain has a name. Every name has a price. But who decides that price? On Torch Market, domains are launched as tokens with bonding curves. The market decides the price. After bonding completes and the token migrates to Raydium, the domain is permanently linked to that token. From that point forward: - **The top token holder controls the domain.** Buy enough tokens, you get the domain. Someone buys more, they get it. - **Holders can borrow SOL against their tokens.** Lock your domain tokens as collateral and extract liquidity — up to 50% LTV, 2% weekly interest. - **Get liquidated, lose the domain.** If your loan goes underwater (LTV > 65%), anyone can liquidate you. Your collateral changes hands. The domain lease rotates to whoever is now the top holder. This creates something that doesn't exist in traditional domain markets: **a continuously-priced, borrowable, liquid domain asset** with built-in consequences for overleveraging. The bot in this kit runs the infrastructure side. It discovers promising domains, launches them as tokens, monitors all active lending positions, and liquidates underwater loans through a Torch Vault. When a liquidation happens, the domain automatically rotates. --- ## How Domains Work on Torch ### Phase 1: Launch (Bonding Curve) A domain is launched as a Torch Market token. The bonding curve creates the initial market — early buyers get a lower price, the curve rises as demand increases. This is the price discovery phase. ``` domain "pixel.art" → token PIXEL → bonding curve → market cap grows ``` Anyone can buy in. The curve sets the price. Trading fees flow to the community treasury. ### Phase 2: Migration (Domain Becomes Permanent) When the bonding curve completes, the token migrates to a Raydium liquidity pool. At this point, the domain is **permanently linked** to the token. There is no delisting, no expiry, no central authority. The token IS the domain. ### Phase 3: Lending (Borrow Against Your Domain) After migration, Torch's built-in lending market activates. Any holder can lock their tokens as collateral and borrow SOL from the community treasury. ``` holder locks 10,000 PIXEL tokens → borrows 0.5 SOL → LTV = 40% ``` The loan accrues 2% interest per epoch (~weekly). As long as the loan stays healthy (LTV < 65%), the borrower keeps their position and their domain control. ### Phase 4: Liquidation (Domain Rotates) If the token price drops or interest accrues enough to push LTV past 65%, the position becomes liquidatable. A keeper (this bot) pays off the debt using vault SOL and receives the borrower's collateral tokens at a 10% discount. ``` PIXEL price drops → LTV hits 68% → keeper liquidates → collateral moves to vault → top holder changes → domain lease rotates ``` The borrower loses their tokens. The domain rotates to whoever is now the largest holder. The keeper's vault profits from the 10% bonus. **This is the key insight: liquidation has real consequences beyond money.** The borrower doesn't just lose capital — they lose control of the domain. This natural tension makes the lending market meaningful. Borrowers are incentivized to maintain healthy positions because the domain itself is at stake. --- ## What the Bot Does ``` ┌─────────────────────────────────────────────────────────────────┐ │ DOMAIN AUCTION BOT │ │ │ │ DISCOVER │ │ scrape expiring domains → evaluate quality → generate tickers │ │ │ │ LAUNCH │ │ buildCreateTokenTransaction → bonding curve → domain = token │ │ │ │ MONITOR │ │ for each domain token: │ │ scan lending markets (getLendingInfo) │ │ discover borrowers (getHolders → getLoanPosition) │ │ profile wallets (SAID verification, trade history) │ │ score risk (LTV proximity, momentum, wallet, interest) │ │ │ │ LIQUIDATE │ │ if position.health === 'liquidatable' && risk > threshold: │ │ buildLiquidateTransaction(vault = creator) │ │ sign with agent keypair → submit → confirm │ │ │ │ ROTATE │ │ after liquidation or any holder change: │ │ check top holder → update domain lease │ │ │ │ All SOL from vault. All collateral to vault. Agent holds nothing. │ └─────────────────────────────────────────────────────────────────┘ ``` ### The Scan Loop The bot runs a continuous loop: 1. **Scan** — discover all migrated domain tokens with active lending markets 2. **Score** — for each borrower, compute a risk score across four weighted factors: - LTV proximity (35%) — how close to the 65% liquidation threshold - Price momentum (25%) — recent token price trend (drops increase risk) - Wallet reputation (20%) — SAID verification status and trade history - Interest burden (20%) — accrued interest relative to principal 3. **Liquidate** — execute vault-routed liquidation for positions above the risk threshold 4. **Rotate** — check top holders and update domain leases 5. **Sleep** — wait for the next cycle ### The Agent Keypair The bot generates a fresh `Keypair` in-process on every startup. No private key file needed. The keypair is disposable — it signs transactions but holds nothing of value (~0.01 SOL for gas). On first run, the bot verifies vault linkage. If the agent isn't linked, it prints instructions: ``` === torch domain auction bot === agent wallet: 7xK9... vault creator: 4yN2... scan interval: 60000ms --- ACTION REQUIRED --- agent wallet is NOT linked to the vault. link it by running (from your authority wallet): buildLinkWalletTransaction(connection, { authority: "<your-authority-pubkey>", vault_creator: "4yN2...", wallet_to_link: "7xK9..." }) then restart the bot. ----------------------- ``` ### The Vault All economic activity routes through a Torch Vault: | Direction | What Happens | |-----------|-------------| | **Liquidation: SOL out** | Vault SOL pays off the borrower's debt | | **Liquidation: tokens in** | Borrower's collateral tokens go to vault ATA at 10% discount | | **Net per liquidation** | Vault receives collateral worth 110% of SOL spent | | **Domain rotation** | Collateral changes hands → top holder changes → lease rotates | The human principal retains full control: - **Withdraw SOL** from the vault at any time - **Withdraw tokens** (liquidated collateral) at any time - **Unlink the agent** — revoke bot access in one transaction If the agent keypair is compromised, the attacker gets dust and vault access you revoke instantly. --- ## Getting Started ### 1. Install ```bash npm install torch-domain-auction-bot@2.0.1 ``` ### 2. Create and Fund a Vault From your authority wallet: ```typescript import { Connection } from "@solana/web3.js"; import { buildCreateVaultTransaction, buildDepositVaultTransaction } from "torchsdk"; const connection = new Connection(process.env.SOLANA_RPC_URL); // Create vault const { transaction: createTx } = await buildCreateVaultTransaction(connection, { creator: authorityPubkey, }); // sign and submit... // Fund with SOL for liquidations const { transaction: depositTx } = await buildDepositVaultTransaction(connection, { depositor: authorityPubkey, vault_creator: authorityPubkey, amount_sol: 5_000_000_000, // 5 SOL }); // sign and submit... ``` ### 3. Run ```bash VAULT_CREATOR=<vault-creator-pubkey> SOLANA_RPC_URL=<rpc-url> npx torch-domain-auction-bot monitor ``` First run prints the agent wallet. Link it from your authority wallet, restart. ### 4. Configuration | Variable | Required | Default | Description | |----------|----------|---------|-------------| | `SOLANA_RPC_URL` | **Yes** | -- | Solana RPC endpoint. Fallback: `BOT_RPC_URL` | | `VAULT_CREATOR` | **Yes** | -- | Vault creator pubkey | | `SOLANA_PRIVATE_KEY` | No | -- | Agent keypair (base58 or JSON). Omit to generate fresh on startup | | `BOT_SCAN_INTERVAL_MS` | No | `60000` | Scan cycle interval (min 5000) | | `BOT_SCORE_INTERVAL_MS` | No | `15000` | Score cycle interval | | `BOT_MIN_PROFIT_LAMPORTS` | No | `10000000` | Min profit to liquidate (0.01 SOL) | | `BOT_RISK_THRESHOLD` | No | `60` | Min risk score to liquidate (0-100) | | `BOT_PRICE_HISTORY_DEPTH` | No | `20` | Price snapshots to retain | | `BOT_LOG_LEVEL` | No | `info` | `debug`, `info`, `warn`, `error` | --- ## Architecture ``` packages/ └── kit/ Domain auction kit (bot + scraper) ├── src/ │ ├── index.ts Entry — vault verification, CLI, all exports │ ├── config.ts Env validation, ephemeral keypair │ ├── monitor.ts Scan loop (scan → score → liquidate → rotate) │ ├── scanner.ts Lending market discovery │ ├── liquidator.ts Vault-routed liquidation │ ├── risk-scorer.ts Four-factor risk scoring │ ├── wallet-profiler.ts SAID + trade history analysis │ ├── launcher.ts Domain token creation │ ├── domain-manager.ts Lease tracking + rotation │ ├── ticker.ts Symbol generation │ ├── logger.ts Structured logging (shared across all modules) │ ├── types.ts All type definitions │ ├── utils.ts Helpers + base58 decoder │ └── scraper/ Domain discovery pipeline │ ├── index.ts CLI entry, pipeline orchestration │ ├── scanner.ts Domain scanning orchestrator │ ├── evaluator.ts Scores domain quality │ ├── ticker.ts Generates token symbols │ ├── config.ts Scraper config │ ├── types.ts Scraper types (re-exports LogLevel from parent) │ └── providers/ Data sources (expiring domains, availability) └── tests/ ├── setup.ts Shared test helpers (Surfpool) ├── test_bot.ts Bot E2E tests └── test_scraper.ts Scraper unit tests clawhub/ ├── agent.json Manifest ├── SKILL.md This file ├── design.md Architecture and design ├── audit.md Security audit └── lib/ ├── kit/ Compiled kit output (tsconfig outDir) └── torchsdk/ Bundled SDK source ``` --- ## The Domain Lending Primitive This kit demonstrates a composable pattern using Torch Market: ``` ┌──────────────┐ │ DOMAIN │ │ (pixel.art) │ └──────┬───────┘ │ launch ▼ ┌──────────────┐ │ TOKEN │ │ (PIXEL) │ ← bonding curve → migration → permanent └──────┬───────┘ │ hold ▼ ┌──────────────┐ │ COLLATERAL │ │ (lock tokens)│ ← borrow SOL against domain position └──────┬───────┘ │ borrow ▼ ┌──────────────┐ │ LOAN │ │ (SOL debt) │ ← 2% weekly interest, 65% liquidation threshold └──────┬───────┘ │ if underwater ▼ ┌──────────────┐ │ LIQUIDATION │ │ (keeper bot)│ ← vault pays debt, receives collateral at 10% bonus └──────┬───────┘ │ collateral moves ▼ ┌──────────────┐ │ ROTATION │ │ (new holder)│ ← top holder changes → domain lease updates └──────────────┘ ``` Each step uses a different piece of Torch's infrastructure: - **Bonding curves** for price discovery - **Raydium migration** for permanent liquidity - **Community treasury** for lending capital - **Lending markets** for collateral-based borrowing - **Vaults** for safe autonomous operation - **Liquidation** for market health + domain rotation Other builders can use this pattern for any asset class — not just domains. NFTs, real-world assets, prediction markets, governance tokens. The pattern is: **tokenize → lend → liquidate → rotate control**. --- ## Vault Safety | Property | Guarantee | |----------|-----------| | **Full custody** | Vault holds all SOL and collateral. Agent holds nothing. | | **Closed loop** | Liquidation SOL from vault, collateral tokens to vault. No leakage. | | **Authority separation** | Creator (PDA seed) vs Authority (admin) vs Controller (disposable signer). | | **Instant revocation** | Authority unlinks agent in one transaction. | | **Authority-only withdrawals** | Agent cannot extract value from the vault. | --- ## Lending Parameters | Parameter | Value | |-----------|-------| | Max LTV | 50% | | Liquidation Threshold | 65% | | Interest Rate | 2% per epoch (~weekly) | | Liquidation Bonus | 10% | | Utilization Cap | 50% of treasury | | Min Borrow | 0.1 SOL | --- ## SDK Functions Used | Function | Purpose | |----------|---------| | `getTokens({ status: 'migrated' })` | Discover domain tokens | | `getToken(mint)` | Token detail (price, metadata) | | `getLendingInfo(mint)` | Lending market state | | `getHolders(mint)` | Token holders (potential borrowers) | | `getLoanPosition(mint, wallet)` | Loan health check | | `getVault(creator)` | Verify vault exists | | `getVaultForWallet(wallet)` | Verify agent linked | | `buildLiquidateTransaction(params)` | Vault-routed liquidation | | `buildCreateTokenTransaction(params)` | Launch domain token | | `verifySaid(wallet)` | Borrower reputation check | --- ## External API Calls The kit makes outbound HTTPS requests to the following services. The bot's runtime path contacts **five** of them. No credentials are sent to any external service. All requests are read-only GETs/HEADs. No private key material is ever transmitted to any external endpoint. If any service is unreachable, the bot degrades gracefully (catches errors, continues to next token or cycle). ### Scraper (direct) | Service | URL | Purpose | Data Sent | |---------|-----|---------|-----------| | **ExpiredDomains.net** | `https://www.expireddomains.net/domain-name-search/` | Scrape expiring domain listings | None (GET, User-Agent header only) | | **RDAP** | `https://rdap.org/domain/{name}.{tld}` | Check domain availability (404 = available) | Domain name in URL (public) | ### Via torchsdk (transitive) | Service | URL | Purpose | Data Sent | |---------|-----|---------|-----------| | **Solana RPC** | User-provided `SOLANA_RPC_URL` | All on-chain reads and transaction submission | Public keys, signed transactions | | **CoinGecko** | `https://api.coingecko.com/api/v3/simple/price` | SOL/USD price for token valuations | None (GET only) | | **SAID Protocol** | `https://api.saidprotocol.com/api/verify/{wallet}` | Wallet reputation / trust verification | Wallet address (public key, not sensitive) | | **Irys Gateway** | `https://gateway.irys.xyz/...` | Token metadata retrieval (fallback) | None (GET only) | --- ## Signing & Key Safety **The vault is the security boundary, not the key.** The agent keypair is generated fresh on every startup with `Keypair.generate()`. It holds ~0.01 SOL for gas fees. If the key is compromised, the attacker gets: - Dust (the gas SOL) - Vault access that the authority revokes in one transaction The agent never needs the authority's private key. The authority never needs the agent's private key. They share a vault, not keys. ### Rules 1. **Never ask a user for their private key or seed phrase.** The vault authority signs from their own device. 2. **Never log, print, store, or transmit private key material.** The agent keypair exists only in runtime memory. 3. **Never embed keys in source code or logs.** The agent pubkey is printed — the secret key is never exposed. 4. **Use a secure RPC endpoint.** Default to a private RPC provider. Never use an unencrypted HTTP endpoint for mainnet transactions. ### RPC Timeout All SDK calls are wrapped with a 30-second timeout (`withTimeout` in utils.ts). A hanging or unresponsive RPC endpoint cannot stall the bot indefinitely — the call rejects, the error is caught by the scan loop, and the bot continues to the next token or cycle. --- ## Testing Requires [Surfpool](https://github.com/nicholasgasior/surfpool) mainnet fork: ```bash surfpool start --network mainnet --no-tui cd packages/kit && pnpm test:bot cd packages/kit && pnpm test:scraper ``` Bot tests (10 passing): borrower creation, scanner, wallet profiler, risk scorer, liquidator (skip healthy), price history, lending info, cleanup. Scraper tests: ticker generation, domain evaluation, scanner integration with mock providers. --- ## Why This Matters Traditional domain markets are opaque. Prices are set by registrars or private negotiation. There's no liquidity — you buy a domain and it sits in your account until you sell it. This kit turns domains into liquid, borrowable, continuously-priced assets. The market sets the price. Anyone can buy in. Holders can extract liquidity without selling. And if they overleverage, the domain rotates to someone who values it more. That's the power of composing Torch's primitives: bonding curves, lending markets, vaults, and liquidation keepers — working together as a single coherent system. --- ## Links - Kit source: [github.com/mrsirg97-rgb/torch-domain-auction-bot](https://github.com/mrsirg97-rgb/torch-domain-auction-bot) - Torch SDK (bundled): `lib/torchsdk/` - Torch SDK (npm): [npmjs.com/package/torchsdk](https://www.npmjs.com/package/torchsdk) - Torch Market: [torch.market](https://torch.market) - Program ID: [REDACTED_SECRET] --- ## Changelog ### v2.0.1 - **Upgraded torchsdk from 3.2.3 to 3.7.23.** Major SDK update adds treasury lock PDAs (V27), dynamic Raydium network detection, auto-migration bundling on bonding curve completion (`buildBuyTransaction` now returns optional `migrationTransaction`), vault-routed Raydium CPMM swaps (`buildVaultSwapTransaction`), Token-2022 fee harvesting (`buildHarvestFeesTransaction`, `buildSwapFeesToSolTransaction`), bulk loan scanning (`getAllLoanPositions`), on-chain token metadata queries (`getTokenMetadata`), and ephemeral agent keypair factory (`createEphemeralAgent`). - **Updated env format in skill frontmatter.** Environment variable declarations now use structured `name`/`required` format for compatibility with ClawHub and OpenClaw agent runners. - **Added clawdbot metadata section.** ClawBot runner now has explicit env requirements alongside OpenClaw.

path: agent.json
content: {
  "name": "Torch Domain Auction Bot",
  "description": "Domain lending protocol on Solana. Domains become tokens. Tokens become collateral. Top holder controls the domain. Borrow SOL against your position -- but get liquidated and you lose the domain. The bot discovers domains, launches them as Torch Market tokens, monitors lending positions, and auto-liquidates underwater loans through a Torch Vault. When a borrower is liquidated, their collateral changes hands and the domain lease rotates to the new top holder. The agent keypair is disposable. All value routes through the vault. Built on torchsdk v3.7.23.",
  "version": "2.0.1",
  "type": "protocol",
  "disable-model-invocation": true,
  "website": "https://torch.market",
  "socials": {
    "twitter": "https://twitter.com/torch_market"
  },
  "distribution": {
    "clawhub": "https://clawhub.ai/mrsirg97-rgb/torch-domain-auction-bot",
    "kit": "https://github.com/mrsirg97-rgb/torch-domain-auction-bot",
    "npm_bot": "torch-domain-auction-bot",
    "npm_sdk": "torchsdk"
  },
  "requires": {
    "env": [
      {
        "name": "SOLANA_RPC_URL",
        "description": "Solana RPC endpoint (HTTPS). Fallback: BOT_RPC_URL",
        "sensitive": false,
        "required": true
      },
      {
        "name": "VAULT_CREATOR",
        "description": "Vault creator pubkey -- identifies which Torch Vault the bot operates through. Required.",
        "sensitive": false,
        "required": true
      },
      {
        "name": "SOLANA_PRIVATE_KEY",
        "description": "Disposable controller keypair (base58 or byte array JSON). Optional -- the bot generates a fresh keypair in-process if not provided (recommended).",
        "sensitive": true,
        "required": false
      }
    ],
    "install": [
      {
        "id": "npm-torch-domain-auction-bot",
        "kind": "npm",
        "package": "torch-domain-auction-bot@^2.0.1",
        "flags": [],
        "label": "Install Torch Domain Auction Bot (npm, optional -- SDK is bundled in lib/torchsdk/, kit (bot + scraper) is in lib/kit/)"
      }
    ]
  },
  "capabilities": [
    "vault-full-custody",
    "vault-escrow",
    "authority-separation",
    "domain-discovery",
    "domain-evaluation",
    "domain-token-launch",
    "domain-lease-rotation",
    "lending-market-scanner",
    "liquidation-keeper",
    "autonomous-scan-loop",
    "in-process-keypair",
    "said-verification"
  ],
  "chain": "solana",
  "network": "mainnet",
  "program_id": [REDACTED_SECRET],
  "endpoints": {
    "skill": "https://torch.market/skill.md",
    "llms_txt": "https://torch.market/llms.txt",
    "docs": "https://torch-market-docs.vercel.app"
  },
  "actions": [
    {
      "name": "TORCH_DISCOVER_DOMAINS",
      "description": "Scrape expiring and available domains, evaluate resale potential, generate ticker symbols"
    },
    {
      "name": "TORCH_LAUNCH_DOMAIN_TOKEN",
      "description": "Launch a domain as a Torch Market token -- bonding curve creates the initial market, migration links the domain to the token permanently"
    },
    {
      "name": "TORCH_SCAN_LENDING_MARKETS",
      "description": "Discover all domain tokens with active lending and check for underwater positions"
    },
    {
      "name": "TORCH_SCORE_POSITION",
      "description": "Score a borrower's loan risk across four factors: LTV proximity, price momentum, wallet reputation, interest burden"
    },
    {
      "name": "TORCH_LIQUIDATE",
      "description": "Liquidate an underwater domain loan (LTV > 65%) via vault -- collateral tokens change hands, domain lease rotates to new top holder"
    },
    {
      "name": "TORCH_ROTATE_LEASE",
      "description": "Check top holder of a domain token and rotate the domain lease if holder changed (e.g. after liquidation or large purchase)"
    },
    {
      "name": "TORCH_GET_VAULT",
      "description": "Get vault state -- SOL balance, linked wallets, authority"
    },
    {
      "name": "TORCH_GET_VAULT_FOR_WALLET",
      "description": "Reverse lookup -- find which vault a controller wallet is linked to"
    },
    {
      "name": "TORCH_GET_LENDING_INFO",
      "description": "Get lending configuration for a domain token (rates, caps, active loans)"
    },
    {
      "name": "TORCH_GET_LOAN",
      "description": "Get loan position for a wallet -- collateral, debt, LTV, health status"
    }
  ],
  "constants": {
    "lending_max_ltv_percent": 50,
    "lending_liquidation_threshold_percent": 65,
    "lending_interest_rate_per_epoch_percent": 2,
    "lending_liquidation_bonus_percent": 10,
    "lending_utilization_cap_percent": 50,
    "min_borrow_sol": 0.1,
    "default_scan_interval_ms": 60000,
    "min_scan_interval_ms": 5000,
    "default_lease_duration_days": 7
  },
  "safety": {
    "vault_full_custody": true,
    "controller_holds_no_value": true,
    "private_key_optional": true,
    "in_process_keypair_generation": true,
    "local_transaction_building": true,
    "no_api_dependency_for_core": true,
    "external_api_read_only": true,
    "external_api_no_credentials": true,
    "keys_never_transmitted": true,
    "signing_is_local_only": true,
    "vault_closed_economic_loop": true,
    "vault_authority_only_withdrawals": true,
    "vault_instant_revocation": true,
    "transaction_expiry_seconds": 60,
    "open_source": true,
    "sdk_audited": true
  },
  "tags": [
    "solana",
    "defi",
    "domains",
    "domain-lending",
    "domain-collateral",
    "domain-lease",
    "liquidation",
    "liquidation-keeper",
    "collateral-lending",
    "vault-custody",
    "ai-agents",
    "agent-wallet",
    "agent-safety",
    "bonding-curve",
    "fair-launch",
    "token-2022",
    "raydium",
    "community-treasury",
    "solana-agent-kit",
    "escrow",
    "anchor",
    "pda",
    "on-chain",
    "autonomous-agent",
    "keeper-bot",
    "torch-market"
  ],
  "verification": {
    "said": {
      "wallet": [REDACTED_SECRET],
      "pda": [REDACTED_SECRET],
      "verified": true,
      "trust_tier": "medium",
      "badge": "https://api.saidprotocol.com/api/badge/8cpWmV4kGdvxVYYNBEMPNwsJSRVQw5MQ9NXn4t293nMa.svg"
    }
  }
}


path: audit.md
content: # Torch Domain Auction Bot — Security Audit

**Audit Date:** February 28, 2026
**Auditor:** Claude Opus 4.6 (Anthropic)
**Bot Version:** 2.0.1
**SDK Version:** torchsdk 3.7.23
**On-Chain Program:** [REDACTED_SECRET] (V3.7.8)
**Language:** TypeScript
**Test Result:** 10 passed, 0 failed (Surfpool mainnet fork)

---

## Table of Contents

1. [Executive Summary](#executive-summary)
2. [Scope](#scope)
3. [Methodology](#methodology)
4. [Changes Since v1.0.2](#changes-since-v102)
5. [Keypair Safety Review](#keypair-safety-review)
6. [Vault Integration Review](#vault-integration-review)
7. [Domain Lease Security](#domain-lease-security)
8. [Risk Scoring Review](#risk-scoring-review)
9. [Scan Loop Security](#scan-loop-security)
10. [Configuration Validation](#configuration-validation)
11. [Dependency Analysis](#dependency-analysis)
12. [Threat Model](#threat-model)
13. [Findings](#findings)
14. [Prior Findings Status](#prior-findings-status)
15. [Conclusion](#conclusion)

---

## Executive Summary

This audit covers the Torch Domain Auction Bot v2.0.1, a single-package kit that implements a domain lending protocol on Torch Market. Domains are tokenized, top holders control the domain, holders can borrow SOL against their tokens, and underwater positions are liquidated through a Torch Vault — causing the domain lease to rotate.

This is a re-audit updating coverage from v1.0.2 (torchsdk 3.2.3) to v2.0.1 (torchsdk 3.7.23, on-chain program V3.7.8).

The bot was reviewed for key safety, vault integration correctness, domain lease security, risk scoring integrity, error handling, and dependency surface.

### Overall Assessment

| Category | Rating | Notes |
|----------|--------|-------|
| Key Safety | **PASS** | In-process `Keypair.generate()`, optional `SOLANA_PRIVATE_KEY`, no key logging |
| Vault Integration | **PASS** | `vault` param correctly passed to `buildLiquidateTransaction` |
| Domain Lease Logic | **PASS** | Top holder tracked correctly, leases expire and rotate |
| Risk Scoring | **PASS** | Four-factor weighted scoring, configurable threshold |
| Error Handling | **PASS** | Four-level isolation: cycle, token, borrower, liquidation |
| Config Validation | **PASS** | Required env vars checked, scan interval floored at 5000ms |
| Dependencies | **MINIMAL** | 5 runtime deps, all pinned exact. No `^` or `~` ranges. |
| Supply Chain | **LOW RISK** | No post-install hooks, no remote code fetching |

### Finding Summary

| Severity | Count |
|----------|-------|
| Critical | 0 |
| High | 0 |
| Medium | 0 |
| Low | 0 |
| Informational | 5 |

Both low findings from v1.0.2 resolved: L-1 (Wallet Profiler Cache — TTL reduced to 30s) and L-2 (No Timeout on SDK Calls — all SDK calls now wrapped with `withTimeout(promise, 30_000, label)`).

---

## Scope

### Files Reviewed

**Kit Package (`packages/kit/`):**

| File | Lines | Role |
|------|-------|------|
| `src/index.ts` | 129 | Entry: vault verification, startup banner, CLI, all exports |
| `src/config.ts` | 78 | Env validation, ephemeral keypair |
| `src/monitor.ts` | 86 | Scan loop orchestration with `getAllLoanPositions` |
| `src/scanner.ts` | 97 | Lending market discovery with bulk loan scanner |
| `src/liquidator.ts` | 88 | Vault-routed liquidation |
| `src/risk-scorer.ts` | 90 | Four-factor risk scoring |
| `src/wallet-profiler.ts` | 127 | SAID verification, trade analysis |
| `src/launcher.ts` | 40 | Domain token creation |
| `src/domain-manager.ts` | 73 | Lease tracking and rotation |
| `src/ticker.ts` | 36 | Symbol generation |
| `src/logger.ts` | 36 | Structured logging (shared across all modules) |
| `src/types.ts` | 111 | Type definitions |
| `src/utils.ts` | 35 | Helpers, base58 decoder |
| `src/scraper/index.ts` | ~43 | Scraper CLI entry |
| `src/scraper/scanner.ts` | ~36 | Domain scanning |
| `src/scraper/evaluator.ts` | ~71 | Quality scoring |
| `src/scraper/ticker.ts` | ~48 | Symbol generation |
| `src/scraper/config.ts` | ~11 | Scraper config |
| `src/scraper/types.ts` | ~30 | Scraper types |
| `src/scraper/providers/*.ts` | ~115 | Data sources |
| `tests/test_bot.ts` | ~346 | Bot E2E test suite |
| `tests/test_scraper.ts` | ~198 | Scraper unit tests |
| `package.json` | 42 | Dependencies |

**Total:** ~1,600 lines in one package.

### SDK Cross-Reference

The bot relies on `torchsdk@3.7.23` for all on-chain interaction. The SDK was independently audited. This audit focuses on the bot's usage of the SDK, not the SDK internals. The SDK is also bundled in `lib/torchsdk/` for full auditability.

---

## Methodology

1. **Line-by-line source review** of all bot source files
2. **Keypair lifecycle analysis** — generation, usage, exposure surface
3. **Vault integration verification** — correct params on ALL liquidation calls
4. **Domain lease logic review** — rotation correctness, edge cases
5. **Risk scoring validation** — factor computation, weight normalization, bounds
6. **Error handling analysis** — crash paths, retry behavior, log safety
7. **Dependency audit** — runtime deps, dev deps, post-install hooks, version pinning
8. **E2E test review** — coverage, assertions, false positives
9. **Delta analysis** — changes from v1.0.2 to v2.0.1

---

## Changes Since v1.0.2

### SDK Upgrade: torchsdk 3.2.3 → 3.7.23

The SDK was upgraded from 3.2.3 to 3.7.23. SDK additions used by the bot:

- `getAllLoanPositions(connection, mint)` — bulk loan scanning replaces holder-based borrower discovery. Used in `scanner.ts:24` and `monitor.ts:60`.

SDK additions **not used** by the bot (no new attack surface): treasury lock PDAs, auto-migration bundling, vault-routed Raydium CPMM swaps, Token-2022 fee harvesting, on-chain token metadata queries, ephemeral agent keypair factory.

### Bot Code Changes

| Change | Files | Impact |
|--------|-------|--------|
| Bulk loan scanning | `scanner.ts:18-30`, `monitor.ts:60` | `getAllLoanPositions` replaces holder-based discovery — more complete borrower coverage |
| Env format update | `SKILL.md`, `agent.json` | Structured `name`/`required` format for ClawHub/OpenClaw compatibility |
| ClawBot metadata | `agent.json` | Added `clawdbot` metadata section alongside OpenClaw config |

### Borrower Discovery Improvement

v1.0.2 used `getHolders(connection, mint, 20)` to discover potential borrowers, limited to 20 holders. v2.0.1 uses `getAllLoanPositions(connection, mint)` which returns all active loan positions directly — no holder limit, no missed borrowers.

```typescript
// scanner.ts:18-30 (v2.0.1)
const discoverBorrowers = async (connection, mint, log) => {
  const { positions } = await getAllLoanPositions(connection, mint)
  return positions.map((p) => p.borrower)
}
```

This resolves informational finding I-1 from the v1.0.2 audit.

---

## Keypair Safety Review

### Generation

The keypair is created via `loadKeypair()` in `config.ts:29-45`:

```typescript
export const loadKeypair = (): { keypair: Keypair; generated: boolean } => {
  const privateKey = process.env.SOLANA_PRIVATE_KEY ?? null
  if (privateKey) {
    // try JSON byte array, then base58
    return { keypair: Keypair.fromSecretKey(...), generated: false }
  }
  return { keypair: Keypair.generate(), generated: true }
}
```

Two paths:
1. **Default (recommended):** `Keypair.generate()` — fresh Ed25519 keypair from system entropy
2. **Optional:** `SOLANA_PRIVATE_KEY` env var — JSON byte array or base58 via inline `decodeBase58`

The keypair is:
- **Not persisted** to disk (unless user provides `SOLANA_PRIVATE_KEY`)
- **Not exported** — `keypair` is embedded in `BotConfig`, not in the public API surface
- **Not logged** — only the public key is printed at startup (`index.ts:64`)
- **Not transmitted** — the secret key never leaves the process
- Marked `"sensitive": true` in `agent.json`

### Usage Points

The keypair is used in exactly three places:
1. **Public key extraction** — startup logging, vault link check, liquidation/launch params (safe)
2. **Liquidation signing** — `result.transaction.partialSign(this.config.walletKeypair)` in `liquidator.ts:65`
3. **Token launch signing** — `result.transaction.partialSign(wallet)` in `launcher.ts:28`

Both signing operations are local only.

**Verdict:** Key safety is correct. No key material leaks from the process.

---

## Vault Integration Review

### CRITICAL: Liquidation Transaction

```typescript
// liquidator.ts:58-63
const result = await buildLiquidateTransaction(connection, {
  mint: scored.mint,
  liquidator: this.config.walletKeypair.publicKey.toBase58(),
  borrower: scored.borrower,
  vault: this.config.vaultCreator,
})
```

The `vault` parameter is correctly passed. Per the SDK, when `vault` is provided:
- Vault PDA derived from `vaultCreator` (`["torch_vault", creator]`)
- Wallet link PDA derived from `liquidator` (`["vault_wallet", wallet]`)
- SOL debited from vault to pay borrower's debt
- Collateral tokens credited to vault ATA at 10% bonus

**There is exactly one call to `buildLiquidateTransaction` in the codebase. It correctly includes the `vault` parameter.**

### Startup Verification

```typescript
// index.ts:70-93
const vault = await getVault(connection, config.vaultCreator)
if (!vault) throw new Error(...)

const link = await getVaultForWallet(connection, config.walletKeypair.publicKey.toBase58())
if (!link) { /* print instructions, exit */ }
```

Both checks execute before any command (monitor, launch, info). The bot cannot operate without a valid vault and linked agent.

**Verdict:** Vault integration is correct. All liquidation value routes through the vault PDA.

---

## Domain Lease Security

### Lease Rotation Logic

```typescript
// domain-manager.ts:43-56
if (activeLease.lessee !== topHolder) {
  activeLease.active = false
  updated.push({
    domain: dt.domain,
    mint: dt.mint,
    lessee: topHolder,
    startedAt: now,
    expiresAt: now + DEFAULT_LEASE_DURATION_MS,
    active: true,
  })
}
```

The lease system:
1. Checks top holder via `getHolders(connection, mint, 1)` (`domain-manager.ts:12`)
2. If top holder changed, expires old lease and creates new one
3. Leases have a 7-day default duration (`DEFAULT_LEASE_DURATION_MS`)
4. Expired leases are cleaned up at the start of each cycle (`domain-manager.ts:27-32`)

### Edge Cases

- **No holders:** `checkTopHolder` returns `null`, no lease created (correct)
- **Same holder:** No rotation, existing lease continues (correct)
- **Liquidation rotation:** After collateral moves to vault ATA, the vault may become top holder. This is by design — the vault operator controls the domain until they withdraw or sell the tokens.
- **Concurrent purchases:** The lease checks once per scan cycle. Rapid trading may cause temporary lag, but the lease always converges to the current top holder.

**Verdict:** Lease logic is correct. Rotation tracks actual token ownership accurately with acceptable latency.

---

## Risk Scoring Review

### Factor Computation

| Factor | Implementation | Bounds | File |
|--------|---------------|--------|------|
| LTV Proximity | `(currentLtv / threshold) * 100` | Clamped 0-100 | `risk-scorer.ts:12-19` |
| Price Momentum | `50 - priceChange * 100` | Clamped 0-100 | `risk-scorer.ts:22-34` |
| Wallet Risk | SAID + trade stats composite | Clamped 0-100 | `wallet-profiler.ts:103-124` |
| Interest Burden | `interestRatio * 500` | Clamped 0-100 | `risk-scorer.ts:37-43` |

### Weight Normalization

```typescript
// risk-scorer.ts:5-10
const WEIGHTS = {
  ltvProximity: 0.35,
  priceMomentum: 0.25,
  walletRisk: 0.2,
  interestBurden: 0.2,
}
```

Weights sum to 1.0. Final score is clamped to 0-100 (`risk-scorer.ts:84`).

### Safety Gates in Liquidator

The `tryLiquidate` method applies four sequential gates before executing:
1. `position.health === 'healthy'` → skip (`liquidator.ts:24`)
2. `riskScore < riskThreshold` → skip (`liquidator.ts:30`)
3. `estimatedProfitLamports < minProfitLamports` → skip (`liquidator.ts:38`)
4. `position.health !== 'liquidatable'` → skip (`liquidator.ts:46`)

A position must be `liquidatable`, above the risk threshold, AND profitable to trigger execution.

**Verdict:** Scoring is mathematically sound. All factors bounded. Liquidation has appropriate safety gates.

---

## Scan Loop Security

### Four-Level Error Isolation

**Level 1 — Cycle level** (`monitor.ts:26-81`):
```typescript
while (true) {
  try {
    // scan, score, liquidate, rotate
  } catch (err) {
    log.error('monitor tick failed', err)
  }
  await sleep(config.scoreIntervalMs)
}
```

**Level 2 — Token level** (`scanner.ts:55-89`):
```typescript
for (const summary of result.tokens) {
  try { ... } catch (err) {
    log.debug(`skipping ${summary.mint}: ${err}`)
  }
}
```

**Level 3 — Borrower level** (`monitor.ts:62-73`):
```typescript
for (const pos of positions) {
  try { ... } catch (err) {
    log.debug(`error scoring ${pos.borrower.slice(0, 8)}...`, err)
  }
}
```

**Level 4 — Liquidation level** (`liquidator.ts:57-85`):
```typescript
try {
  const result = await buildLiquidateTransaction(...)
  // sign, send, confirm
} catch (err) {
  this.log.error(`liquidation failed...`, err)
  return null
}
```

No single failure can crash the bot. Each level catches independently.

**Verdict:** Error handling is robust. The bot degrades gracefully at every level.

---

## Configuration Validation

### Required Variables

| Variable | Validation | Failure Mode |
|----------|-----------|--------------|
| `SOLANA_RPC_URL` | Must be set (fallback: `BOT_RPC_URL`) | Throws on startup |
| `VAULT_CREATOR` | Must be set | Throws on startup |
| `BOT_SCAN_INTERVAL_MS` | Must be >= 5000 | Throws on startup |
| `BOT_LOG_LEVEL` | Must be `debug\|info\|warn\|error` | Throws on startup |

### Security Notes

- `SOLANA_RPC_URL` used only for Solana RPC calls — never logged or transmitted externally
- `VAULT_CREATOR` is a public key (not sensitive)
- `SOLANA_PRIVATE_KEY` is optional, read once at startup, never logged or transmitted. Marked `"sensitive": true` in `agent.json`.

**Verdict:** Configuration properly validated. Sensitive values handled safely.

---

## Dependency Analysis

### Runtime Dependencies

| Package | Version | Pinning | Post-Install | Risk |
|---------|---------|---------|-------------|------|
| `@solana/web3.js` | 1.98.4 | Exact | None | Low |
| `torchsdk` | 3.7.23 | Exact | None | Low — audited separately, bundled in `lib/torchsdk/` |
| `@coral-xyz/anchor` | 0.32.1 | Exact | None | Low |
| `@solana/spl-token` | 0.4.14 | Exact | None | Low |
| `bs58` | 6.0.0 | Exact | None | Low |

### Dev Dependencies

| Package | Version | Purpose |
|---------|---------|---------|
| `@types/node` | 20.19.33 | TypeScript definitions |
| `prettier` | 3.8.1 | Code formatting |
| `typescript` | 5.9.3 | Compiler |

Dev dependencies are not shipped in the runtime bundle and have no security impact.

### Supply Chain

- **No `^` or `~` version ranges** on runtime dependencies — all pinned to exact versions
- **No post-install hooks** — scripts contain only `build`, `clean`, `test:scraper`, `test:bot`, `format`
- **No remote code fetching** — no dynamic `import()`, no `eval()`, no fetch-and-execute
- **Lockfile present** — `pnpm-lock.yaml` pins transitive dependencies
- **SDK bundled** — `lib/torchsdk/` includes full SDK source for auditability

### External Runtime Dependencies

| Service | Purpose | Data Sent | Bot Uses? |
|---------|---------|-----------|-----------|
| **CoinGecko** (`api.coingecko.com`) | SOL/USD price | None (GET only) | Yes via `getToken()` |
| **Irys Gateway** (`gateway.irys.xyz`) | Token metadata fallback | None (GET only) | Yes via `getToken()` |
| **SAID Protocol** (`api.saidprotocol.com`) | Wallet reputation | Wallet address (public) | Yes via `verifySaid()` |
| **ExpiredDomains.net** | Expired domain listings | None (GET, User-Agent only) | Yes (scraper) |
| **RDAP** (`rdap.org`) | Domain availability | Domain name in URL (public) | Yes (scraper) |

No private key material is ever transmitted. All requests are read-only GETs/HEADs. No credentials are sent to any external service. If any service is unreachable, the bot degrades gracefully — the error is caught by the four-level isolation and the bot continues to the next token or cycle.

All external API calls are wrapped with `withTimeout` — 30s for SDK calls (RPC-backed), 10s for `verifySaid` (external API). A hanging or unresponsive endpoint cannot stall the bot.

**Verdict:** Minimal and locked dependency surface. No supply chain concerns. All external calls are read-only, credential-free, and timeout-bounded.

---

## Threat Model

### Threat: Compromised Agent Keypair

**Attack:** Attacker obtains the agent's private key from process memory.
**Impact:** Attacker can sign vault-routed transactions.
**Mitigation:** Agent holds ~0.01 SOL. Authority unlinks in one transaction. Cannot call `withdrawVault` or `withdrawTokens`.
**Residual risk:** Attacker could execute vault-routed liquidations until unlinked. Limited by vault SOL balance.

### Threat: Malicious RPC Endpoint

**Attack:** RPC returns fabricated positions to trigger unprofitable liquidations.
**Impact:** Bot liquidates positions that aren't actually underwater.
**Mitigation:** On-chain program validates all liquidation preconditions. Fabricated RPC data produces transactions that fail on-chain.
**Residual risk:** None — on-chain validation is the security boundary.

### Threat: Domain Lease Manipulation

**Attack:** Attacker rapidly buys/sells tokens to flip domain control.
**Impact:** Domain lease rotates frequently.
**Mitigation:** 7-day lease duration acts as debounce. Lease only rotates when top holder actually changes. Market depth (after migration) makes manipulation expensive.
**Residual risk:** For low-liquidity tokens, lease manipulation is cheaper. This is inherent to the model.

### Threat: Risk Score Gaming

**Attack:** Borrower maintains SAID verification and trade history to lower risk score, avoiding liquidation.
**Impact:** Underwater position liquidated later than optimal.
**Mitigation:** LTV proximity has the highest weight (35%). Risk score is a secondary filter — the primary gate is `position.health === 'liquidatable'` which is computed on-chain from actual collateral/debt ratios.
**Residual risk:** Delayed liquidation in edge cases. No capital loss — the position is still liquidatable.

### Threat: Front-Running

**Attack:** MEV bot observes the liquidation transaction and front-runs it.
**Impact:** Bot's transaction fails (`NOT_LIQUIDATABLE`).
**Mitigation:** Error caught, bot moves to next position. No vault SOL lost on failed liquidation.
**Residual risk:** Reduced success rate in competitive MEV environments.

---

## Findings

### L-1: Wallet Profiler Caches May Bias Risk Scores — **RESOLVED**

**Severity:** Low
**File:** `wallet-profiler.ts:6`
**Description:** Wallet profiles were cached for 60 seconds (`CACHE_TTL_MS`). During volatile markets, a profile cached before a rapid sell-off could understate wallet risk, slightly delaying liquidation.
**Resolution:** Cache TTL reduced from 60s to 30s (`CACHE_TTL_MS = 30_000`). The primary liquidation gate remains on-chain health status — the risk score is a secondary filter.

### I-1: Lease Duration Not Configurable

**Severity:** Informational
**File:** `domain-manager.ts:6`
**Description:** `DEFAULT_LEASE_DURATION_MS` is hardcoded to 7 days. Different use cases may want shorter or longer lease periods.
**Impact:** Inflexible for operators wanting different rotation speeds.

### I-2: Price Momentum Assumes Linear Price History

**Severity:** Informational
**File:** `risk-scorer.ts:22-34`
**Description:** Price momentum compares first and last entries in the price history array. This misses intra-period volatility (e.g., a V-shaped recovery looks the same as flat).
**Impact:** Risk scoring may underweight volatile tokens that recovered.

### I-3: Vault May Become Top Holder After Liquidation

**Severity:** Informational
**File:** `domain-manager.ts`
**Description:** After liquidation, collateral tokens move to the vault ATA. If the vault holds more tokens than any other holder, the vault becomes the domain lessee. This is by design but worth noting — the vault operator effectively controls the domain until tokens are withdrawn or sold.
**Impact:** None — this is the intended behavior.

### I-4: No Deduplication of Failed Liquidation Attempts

**Severity:** Informational
**File:** `liquidator.ts`
**Description:** If a liquidation fails (e.g., insufficient vault SOL), the same position will be retried every scan cycle.
**Impact:** Repeated log noise. No financial impact — failed transactions don't consume vault SOL.

### I-5: CoinGecko Rate Limiting

**Severity:** Informational
**Description:** CoinGecko's free tier allows ~10-30 requests per minute. With many monitored tokens, the bot could hit rate limits during price updates.
**Impact:** Some tokens may fail to update price in a single cycle. They'll retry next cycle.

---

## Prior Findings Status

### L-1: Wallet Profiler Caches May Bias Risk Scores (v1.0.2) — **RESOLVED**

Cache TTL reduced from 60s to 30s (`CACHE_TTL_MS = 30_000` in `wallet-profiler.ts:6`). The primary liquidation gate remains on-chain health status — the risk score is a secondary filter. With 30s TTL, profile staleness is bounded to half a scan cycle.

### L-2: No Timeout on SDK Calls (v1.0.2) — **RESOLVED**

All SDK calls are now wrapped with `withTimeout(promise, 30_000, label)` from `utils.ts`. A hanging or unresponsive RPC endpoint cannot stall the bot — the call rejects after 30 seconds, the error is caught by the four-level error isolation, and the bot continues to the next token or cycle.

**Evidence:**
- `scanner.ts` — `getTokens`, `getToken`, `getLendingInfo`, `getAllLoanPositions` wrapped
- `monitor.ts` — `getToken`, `getAllLoanPositions` wrapped
- `liquidator.ts` — `buildLiquidateTransaction` wrapped
- `launcher.ts` — `buildCreateTokenTransaction` wrapped
- `domain-manager.ts` — `getHolders` wrapped
- `wallet-profiler.ts` — `verifySaid` wrapped (10s timeout for external API)
- `index.ts` — `getVault`, `getVaultForWallet` wrapped

### I-1: Holder Discovery Limited to 20 (v1.0.2) — **RESOLVED**

The bot now uses `getAllLoanPositions(connection, mint)` instead of `getHolders(connection, mint, 20)` for borrower discovery. This returns all active loan positions directly with no holder limit.

**Evidence:**
- `scanner.ts:24` — `const { positions } = await getAllLoanPositions(connection, mint)`
- `monitor.ts:60` — `const { positions } = await getAllLoanPositions(connection, mint)`

---

## Conclusion

The Torch Domain Auction Bot v2.0.1 is a well-structured single-package kit with correct vault integration, robust error handling, and a sound domain lending model. Changes from v1.0.2:

1. **SDK upgraded from 3.2.3 to 3.7.23** — the bot correctly uses the new `getAllLoanPositions` bulk scanner for complete borrower discovery. No new attack surface from unused SDK features.
2. **Borrower discovery improved** — `getAllLoanPositions` replaces holder-limited discovery, resolving I-1 from v1.0.2.
3. **Key safety remains correct** — in-process `Keypair.generate()`, optional `SOLANA_PRIVATE_KEY`, no key logging or transmission.
4. **Vault integration remains correct** — `vault` param passed to `buildLiquidateTransaction`. All liquidation value routes through the vault PDA.
5. **Domain lease rotation remains correct** — top holder tracked accurately, leases expire and rotate as expected.
6. **Risk scoring remains sound** — four factors, weights sum to 1.0, all bounded 0-100, configurable threshold.
7. **Error handling is robust** — four levels of isolation. No single failure crashes the bot.
8. **SDK call timeouts added** — all SDK calls wrapped with `withTimeout(promise, 30_000, label)`. External API calls (verifySaid) use 10s timeout. No hanging call can stall the bot.
9. **Dependencies remain minimal and pinned** — 5 runtime deps, all exact versions, no `^` ranges, no post-install hooks.
10. **No critical, high, medium, or low findings** — all prior low findings resolved, 5 informational.

The bot is safe for production use as an autonomous domain lending keeper operating through a Torch Vault.

---

## Audit Certification

This audit was performed by Claude Opus 4.6 (Anthropic) on February 28, 2026. All source files were read in full and cross-referenced against the torchsdk v3.7.23 audit. The E2E test suite (10 passed) validates the bot against a Surfpool mainnet fork.

**Auditor:** Claude Opus 4.6
**Date:** 2026-02-28
**Bot Version:** 2.0.1
**SDK Version:** torchsdk 3.7.23
**On-Chain Version:** V3.7.8 (Program ID: [REDACTED_SECRET])
**Prior Audit:** v1.0.2, 2026-02-13 — 2 low findings (both resolved in v2.0.1), 5 informational (1 resolved)


path: verification.md
content: # Formal Verification Report

## TL;DR

We used [Kani](https://model-checking.github.io/kani/), a formal verification tool from AWS, to mathematically prove that torch.market's core math is correct -- not just tested, but **proven for every possible input**. This covers all fee calculations, bonding curve pricing, lending formulas, and reward distribution. No SOL can be created from nothing, no tokens can be minted from thin air, and no fees can exceed their stated rates.

This is **not** a security audit. It proves the arithmetic is correct, but does not cover access control, account validation, or economic attacks. See [What Is NOT Verified](#what-is-not-verified) for full scope limitations.

**43 proof harnesses. All passing. Zero failures.**

---

## Overview

torch_market's core arithmetic has been formally verified using [Kani](https://model-checking.github.io/kani/), a Rust model checker backed by the CBMC bounded model checker. Kani exhaustively proves properties hold for **all** valid inputs within constrained ranges -- not just sampled test cases.

**Tool:** Kani Rust Verifier 0.67.0 / CBMC 6.8.0
**Target:** `torch_market` v3.7.8
**Harnesses:** 43 proof harnesses, all passing
**Source:** `programs/torch_market/src/kani_proofs.rs`

## What Is Formally Verified

The proofs cover the **pure arithmetic layer** -- every fee calculation, bonding curve formula, lending math function, and reward distribution used by the on-chain program. Each proof harness uses symbolic (unconstrained) inputs bounded to realistic protocol ranges, and Kani exhaustively checks all possible values within those bounds.

### Buy Flow (Harnesses 1-8)

| Harness | Property | Input Range |
|---------|----------|-------------|
| `verify_buy_fee_conservation` | `protocol_fee + treasury_fee + after_fees == sol_amount` | 0.001-200 SOL |
| `verify_protocol_fee_split` | `dev_share + protocol_portion == protocol_fee_total` | 0.001-200 SOL |
| `verify_treasury_rate_bounds` | `rate in [500, 2000]` (5-20%) flat across all tiers | 0-target SOL reserves |
| `verify_treasury_rate_monotonic` | More reserves -> lower treasury rate | 0-target SOL (two symbolic) |
| [REDACTED_SECRET] | `curve + treasury + creator + dev + protocol == sol_amount` (zero SOL created or lost, V34 5-way sum) | 0.001-10 SOL per trade, 0-target SOL reserves |
| [REDACTED_SECRET] | `tokens_out < virtual_token_reserves` (can't mint from thin air) | Legacy pool state space (IVT=107.3T) |
| `verify_curve_tokens_bounded_v25` | Same property for V27 per-tier reserves | V27 pool state space (IVT=756.25M tokens) |
| `verify_token_split_conservation` | `tokens_to_buyer + tokens_to_treasury == tokens_out` | 0 to TOTAL_SUPPLY |

### Sell Flow (Harnesses 9-10)

| Harness | Property | Input Range |
|---------|----------|-------------|
| `verify_sell_sol_bounded_legacy` | `sol_out < virtual_sol_reserves` (can't drain more SOL than exists) | Legacy pool state, max wallet cap |
| `verify_sell_sol_bounded_v25` | Same property for V27 per-tier reserves | V27 pool state (IVS=3BT/8), max wallet cap |

### Transfer Fees (Harnesses 11-12)

| Harness | Property | Input Range |
|---------|----------|-------------|
| `verify_transfer_fee_bounds` | `floor <= fee <= floor + 1` (ceiling division correct) | 0.001 SOL - 100 tokens |
| [REDACTED_SECRET] | `amount - fee` never underflows | 0 to TOTAL_SUPPLY |

### Lending (Harnesses 13-18)

| Harness | Property | Input Range |
|---------|----------|-------------|
| [REDACTED_SECRET] | `collateral_value <= pool_sol` when `collateral <= pool_tokens` | 50 SOL / 50B token pool |
| [REDACTED_SECRET] | Same property at different pool scale | 500 SOL / 200T token pool |
| `verify_ltv_zero_collateral` | Zero collateral returns `u64::MAX` (instant liquidation) | All u64 debt values |
| `verify_ltv_zero_debt` | Zero debt returns 0 LTV | All u64 collateral values |
| `verify_interest_no_overflow` | Interest calculation doesn't overflow; interest <= principal | Up to 1000 SOL, 2%/epoch, 1 epoch |
| [REDACTED_SECRET] | Liquidation bonus increases collateral seized | 100 SOL pool, up to 50 SOL debt |

### Protocol Rewards (Harnesses 19-20)

| Harness | Property | Input Range |
|---------|----------|-------------|
| `verify_user_share_bounded` | `user_share <= distributable` (no user can drain reward pool) | 500 SOL epoch, 50 SOL distributable |
| `verify_min_claim_enforcement` | [V32] Claims passing MIN_CLAIM_AMOUNT check are genuinely >= 0.1 SOL; claim never exceeds distributable | 10-10,000 SOL total volume, up to 1,000 SOL distributable |

### Ratio Math & Sell Cycle (Harnesses 21-23)

| Harness | Property | Input Range |
|---------|----------|-------------|
| `verify_ratio_fits_u64` | Pool ratio `(sol * 1e9) / tokens` fits in u64 | Up to 1000 SOL, tokens >= 1 token |
| `verify_sell_threshold_fits_u64` | [V30] Sell threshold `baseline_ratio * 12000 / 10000` fits in u64 | Same bounds as ratio proof, with 1.2x multiplier |
| [REDACTED_SECRET] | Token amount remains positive after two consecutive transfer fees | 1 token to TOTAL_SUPPLY |

### Migration (Harnesses 22-26)

These harnesses verify the V26 permissionless migration: SOL wrapping conservation, price-matched pool creation, and token burn accounting. Updated for V31 per-tier virtual reserves and zero-burn migration.

| Harness | Property | Input Range |
|---------|----------|-------------|
| [REDACTED_SECRET] | [V26] `bc_debited == wsol_credited`, total lamports conserved (bonding curve SOL → payer WSOL) | 0 to 200 SOL reserves, rent up to 10M lamports |
| `verify_price_matched_pool_spark` | [V31] Pool ratio matches curve ratio (truncation error < 1 unit) | Spark tier (50 SOL), 3 representative token values |
| `verify_price_matched_pool_flame` | [V31] Pool ratio matches curve ratio (truncation error < 1 unit) | Flame tier (100 SOL), 3 representative token values |
| `verify_price_matched_pool_torch` | [V31] Pool ratio matches curve ratio (truncation error < 1 unit) | Torch tier (200 SOL), 3 representative token values |
| [REDACTED_SECRET] | [V31] `pool_tokens + burned_tokens == vault_total` (no tokens created or lost) | Spark tier, vault up to CURVE_SUPPLY |

### V31 Zero-Burn Distribution (Harnesses 27-34)

These harnesses verify the V31 token distribution model where IVS = 3*bonding_target/8, IVT = 756.25M tokens, CURVE_SUPPLY = 700M (70%), and TREASURY_LOCK_TOKENS = 300M (30%). V31 tunes the curve/lock split so that vault_remaining == tokens_for_pool at graduation — proving zero excess burn and full 1B supply preservation.

| Harness | Property | Input Range |
|---------|----------|-------------|
| [REDACTED_SECRET] | `wallets + vote_vault + pool + burned + treasury_lock == TOTAL_SUPPLY` | Spark tier (50 SOL), exact graduation state |
| [REDACTED_SECRET] | Same conservation for Flame tier | Flame tier (100 SOL), exact graduation state |
| [REDACTED_SECRET] | Same conservation for Torch tier | Torch tier (200 SOL), exact graduation state |
| [REDACTED_SECRET] | Pool tokens > 0 and <= real_token_reserves at graduation | All tiers, exact graduation state |
| [REDACTED_SECRET] | `excess_burned == 0` at graduation (zero-burn migration) | Spark tier, exact graduation state |
| [REDACTED_SECRET] | `excess_burned == 0` at graduation (zero-burn migration) | Flame tier, exact graduation state |
| [REDACTED_SECRET] | `excess_burned == 0` at graduation (zero-burn migration) | Torch tier, exact graduation state |

### Sell Fee (Harness 35)

| Harness | Property | Input Range |
|---------|----------|-------------|
| `verify_sell_fee_always_zero` | `SELL_FEE_BPS == 0` and computed fee == 0 for all valid sol_out | 0.001-200 SOL |

### Creator Revenue (Harnesses 36-39) — V34

These harnesses verify the V34 creator revenue arithmetic: bonding SOL share rate bounds, monotonicity, safety of the treasury-creator subtraction, and post-migration fee share conservation.

| Harness | Property | Input Range |
|---------|----------|-------------|
| `verify_creator_rate_bounds` | `creator_rate in [20, 100]` bps (0.2%-1%) for all bonding progress | 0-target SOL reserves |
| `verify_creator_rate_monotonic` | More reserves → higher creator rate | 0-target SOL (two symbolic) |
| [REDACTED_SECRET] | `creator_rate < treasury_rate` at all points (subtraction safety) | 0-target SOL reserves |
| [REDACTED_SECRET] | 15% share ≤ total, `creator + treasury == total` (conservation) | 0.001-200 SOL fee swap proceeds |

### Lending Lifecycle (Harnesses 40-42)

These harnesses verify end-to-end lending correctness: borrow → (optional interest accrual) → repay, proving treasury SOL conservation, correct interest-first repayment ordering, and loan zeroing.

| Harness | Property | Input Range |
|---------|----------|-------------|
| [REDACTED_SECRET] | After borrow + full repay (same slot): treasury SOL exactly restored, loan zeroed, principal_paid == sol_borrowed | 100 SOL / 50T token pool, up to 50 SOL borrow |
| [REDACTED_SECRET] | After partial repay: remaining_debt == total_owed - repaid, interest paid first, borrowed never increases | Up to 50 SOL, interest < 10% of principal |
| [REDACTED_SECRET] | After borrow + 1 epoch interest + full repay: treasury gains exactly the interest, principal fully returned | Up to 50 SOL, 2%/epoch, 1 epoch max |

## Verification Methodology

### How Kani Works

Kani translates Rust code into a mathematical model and uses a SAT/SMT solver (CaDiCaL via CBMC) to exhaustively check whether any input can violate the asserted properties. Unlike fuzz testing which samples random inputs, Kani explores **every possible execution path** within the constrained input space.

A passing harness means: "there exists no input in the constrained range that violates this property."

### Constraint Design

Each harness constrains symbolic inputs to realistic protocol bounds:

- **SOL amounts:** `MIN_SOL_AMOUNT` (0.001 SOL) to `BONDING_TARGET_LAMPORTS` (200 SOL)
- **Token amounts:** Up to `TOTAL_SUPPLY` (1 billion tokens, 6 decimals)
- **Legacy pool reserves:** `INITIAL_VIRTUAL_SOL` (30 SOL) to `INITIAL_VIRTUAL_SOL + BONDING_TARGET_LAMPORTS` (230 SOL)
- **V31 pool reserves:** `3*bonding_target/8` initial virtual SOL (18.75-75 SOL), `INITIAL_VIRTUAL_TOKENS_V27` (756.25M tokens)
- **Token reserves:** Up to `INITIAL_VIRTUAL_TOKENS` (107.3T raw units, legacy) or `INITIAL_VIRTUAL_TOKENS_V27` (756.25T raw units, V31)
- **Curve supply:** `CURVE_SUPPLY` (700M tokens) for V31 bonding curve + pool allocation
- **Lending pools:** Concrete post-migration pool states (50-500 SOL, 50B-200T tokens)
- **Interest rates:** Up to `DEFAULT_INTEREST_RATE_BPS` (2% per epoch)

Some harnesses use concrete pool states instead of fully symbolic parameters. This is a deliberate constraint design choice driven by SAT solver tractability:

- **Symbolic inputs** (e.g., `kani::any()`) allow Kani to prove properties for *all* values in a range. This is the strongest form of proof but creates exponentially larger SAT formulas when multiple symbolic u64 values flow through u128 intermediate arithmetic.
- **Concrete inputs** fix specific values (e.g., `pool_sol = 100_000_000_000`), eliminating those variables from the SAT formula entirely. Properties are verified exactly at those values rather than universally.
- **Representative concrete values** are a middle ground used for the migration price-match harnesses. Instead of a single symbolic `virtual_tokens` spanning 47 bits (which the solver cannot handle), three concrete values are tested at key pool states: bonding completion, midpoint, and maximum. This reduces solve time from intractable to sub-100ms while covering the important points.

The concrete values are chosen to represent realistic protocol conditions: post-migration pool states for lending, bonding completion states for migration, and protocol-default rates for the sell cycle.

### Dropped Harnesses (Design Rationale)

Eight harnesses were dropped during verification because they prove structurally guaranteed properties or were superseded:

| Dropped Harness | Reason |
|-----------------|--------|
| `verify_curve_monotonic_fresh/half/full` | Monotonicity of `vt * sol / (vs + sol)` is guaranteed by the formula structure for any fixed positive `vt`, `vs`. Integer floor division preserves monotonicity. |
| `verify_no_round_trip_fresh/half/full` | Round-trip loss (`buy then sell <= original`) is inherent in AMM constant-product formulas with integer truncation. Floor division always rounds down. |
| `verify_ltv_100_percent` | `(v * 10000) / v == 10000` is a mathematical tautology. SAT solvers cannot efficiently prove symbolic u128 division cancellation. |
| `verify_buyback_respects_reserve` | Buyback reserve/amount constraints are enforced by handler-level checks, not arithmetic. Property is structural given the config validation. |

These properties remain true by construction. The remaining 43 harnesses cover every non-tautological safety property.

## What Is NOT Verified

Kani proofs verify **isolated pure functions** extracted from the handlers. They do not cover:

| Category | Examples | Why Not Covered |
|----------|----------|-----------------|
| **Access control** | Who can call `migrate_to_dex`, `update_dev_wallet` | Enforced by Anchor `#[derive(Accounts)]` constraints, not arithmetic |
| **Account validation** | Fake PDAs, wrong mints, account substitution | Requires on-chain runtime context |
| **State machine transitions** | Can you sell before buying? Migrate before bonding completes? | Requires multi-instruction sequencing |
| **CPI safety** | Reentrancy via Raydium CPIs, privilege escalation | Cross-program invocation is outside arithmetic scope |
| **Economic attacks** | Sandwich attacks, oracle manipulation, flash loans | Require multi-transaction economic modeling |
| **Anchor framework correctness** | `init-if-needed` edge cases, PDA derivation | Framework-level concerns |
| **Concurrency** | Parallel transaction ordering, front-running | Solana runtime behavior |

### Recommendation for Auditors

The arithmetic layer is formally verified. Audit effort should focus on:

1. **Access control and account validation** -- can unauthorized callers invoke privileged instructions?
2. **State transition integrity** -- are there invalid state transitions (e.g., double migration, selling into an empty curve)?
3. **CPI safety** -- can Raydium CPIs be exploited for reentrancy or privilege escalation?
4. **Economic attack surface** -- sandwich attacks on bonding curve buys, oracle-free lending price manipulation
5. **Token-2022 edge cases** -- transfer fee interaction with Token-2022 extensions across CPIs

## Running the Proofs

```bash
# Install Kani
cargo install --locked kani-verifier
cargo kani setup

# Run all harnesses
cd torch_market/programs/torch_market
cargo kani

# Run a specific harness
cargo kani --harness verify_buy_fee_conservation
```

All 43 harnesses pass. Most complete in under 1 second; the slowest (`verify_transfer_fee_bounds`, `verify_treasury_rate_monotonic`) take 30-55 seconds due to larger SAT formula complexity.

## Constants Reference

| Constant | Value | Description |
|----------|-------|-------------|
| `TOTAL_SUPPLY` | 1,000,000,000,000,000 | 1 billion tokens (6 decimals) |
| `BONDING_TARGET_SPARK` | 50,000,000,000 | 50 SOL bonding target (Spark tier) |
| `BONDING_TARGET_FLAME` | 100,000,000,000 | 100 SOL bonding target (Flame tier) |
| `BONDING_TARGET_TORCH` | 200,000,000,000 | 200 SOL bonding target (Torch tier, default) |
| `INITIAL_VIRTUAL_SOL` | 30,000,000,000 | 30 SOL initial virtual reserves (legacy) |
| `INITIAL_VIRTUAL_TOKENS` | 107,300,000,000,000 | Initial virtual token reserves (legacy) |
| `INITIAL_VIRTUAL_TOKENS_V27` | 756,250,000,000,000 | 756.25M tokens initial virtual reserves (V27) |
| `TREASURY_LOCK_TOKENS` | 300,000,000,000,000 | 300M tokens locked in treasury (30% of supply) |
| `CURVE_SUPPLY` | 700,000,000,000,000 | 700M tokens for curve + pool (70% of supply) |
| V27 IVS | `3 * bonding_target / 8` | 18.75 SOL (Spark), 37.5 SOL (Flame), 75 SOL (Torch) |
| `PROTOCOL_FEE_BPS` | 100 | 1% protocol fee |
| `TREASURY_FEE_BPS` | 100 | 1% token treasury fee |
| `TREASURY_SOL_MIN_BPS` | 500 | 5% min treasury SOL rate (flat, all tiers) |
| `TREASURY_SOL_MAX_BPS` | 2000 | 20% max treasury SOL rate (flat, all tiers) |
| `DEV_WALLET_SHARE_BPS` | 1000 | [V32] 10% of protocol fee to dev (was 25%) |
| `BURN_RATE_BPS` | 1000 | 10% token burn on buy |
| `TRANSFER_FEE_BPS` | 4 | [V34] 0.04% Token-2022 transfer fee (was 3 bps, old tokens retain 3) |
| `DEFAULT_INTEREST_RATE_BPS` | 200 | 2% lending interest per epoch |
| `DEFAULT_LIQUIDATION_BONUS_BPS` | 1000 | 10% liquidation bonus |
| [REDACTED_SECRET] | 7000 | [V33] 70% max treasury SOL lendable (was 50%) |
| `RATIO_PRECISION` | 1,000,000,000 | 1e9 ratio scale factor |
| `DEFAULT_SELL_THRESHOLD_BPS` | 12,000 | 120% -- sell triggers at 20% above baseline |
| `DEFAULT_SELL_PERCENT_BPS` | 1,500 | 15% of held tokens sold per call |
| `SELL_ALL_TOKEN_THRESHOLD` | 1,000,000,000,000 | 1M tokens -- sell 100% below this |
| `MIN_EPOCH_VOLUME_ELIGIBILITY` | 2,000,000,000 | [V32] 2 SOL min epoch volume for rewards (was 10 SOL) |
| `MIN_CLAIM_AMOUNT` | 100,000,000 | [V32] 0.1 SOL min claim amount |
| `CREATOR_SOL_MIN_BPS` | 20 | [V34] 0.2% creator SOL share at bonding start |
| `CREATOR_SOL_MAX_BPS` | 100 | [V34] 1% creator SOL share at bonding completion |
| `CREATOR_FEE_SHARE_BPS` | 1,500 | [V34] 15% creator share of fee swap proceeds |
| `MIN_SOL_AMOUNT` | 1,000,000 | 0.001 SOL minimum |


path: design.md
content: # Torch Domain Auction Bot -- Design Document

> Domains become tokens. Tokens become collateral. Top holder controls the domain. Version 2.0.1.

## Overview

The Torch Domain Auction Bot is a single-package kit that implements a domain lending protocol on Torch Market. Domains are launched as tokens via bonding curves, permanently linked after migration, and backed by a built-in lending market. The top token holder controls the domain. Holders can borrow SOL against their tokens, but if they're liquidated, the domain lease rotates to the new top holder.

The kit runs the keeper infrastructure: domain discovery (scraper), token launching, lending market scanning, risk scoring, vault-routed liquidation, and automatic lease rotation. Everything ships as one npm package (`torch-domain-auction-bot`).

Built on `torchsdk@3.7.23`. Targets the Torch Market program ([REDACTED_SECRET]).

---

## The Domain Lending Model

This is the core idea the architecture serves:

```
DOMAIN → TOKEN → COLLATERAL → LOAN → LIQUIDATION → ROTATION
```

1. A domain is launched as a Torch Market token (bonding curve)
2. After migration, the token is permanently linked to the domain
3. The top token holder controls the domain (perpetual lease)
4. Any holder can lock tokens as collateral and borrow SOL
5. If a loan goes underwater (LTV > 65%), it gets liquidated
6. Liquidation moves collateral → top holder changes → domain rotates

The consequence of liquidation is not just financial -- you lose the domain. This creates natural incentives for healthy lending positions and gives the domain market real dynamics.

---

## Architecture

```
┌─────────────────────────────────────────────────────────────────┐
│                    KIT PACKAGE (packages/kit)                      │
│                                                                    │
│  src/scraper/    Domain Discovery                                  │
│    providers/expired-domains.ts → scrape registrar feeds           │
│    providers/availability.ts    → check domain availability        │
│    evaluator.ts                 → score domain quality             │
│    ticker.ts                    → generate token symbols           │
│    scanner.ts                   → orchestrate the pipeline         │
│                                     │ domain candidates            │
│                                     ▼                              │
│  src/            Bot Core                                          │
│    main()                                                          │
│      ├── loadConfig()              → validate env, load keypair    │
│      ├── getVault()                → vault must exist              │
│      ├── getVaultForWallet()       → agent must be linked          │
│      └── runMonitor()                                              │
│           │                                                        │
│           ├── scanForLendingMarkets()                               │
│           │    ├── getTokens({ status: 'migrated' })               │
│           │    ├── getToken(mint) + getLendingInfo(mint)            │
│           │    └── discoverBorrowers() via getHolders + getLoanPos  │
│           │                                                        │
│           ├── updateLeases()                                       │
│           │    ├── expire stale leases                             │
│           │    ├── checkTopHolder(mint) for each domain token      │
│           │    └── create/rotate lease if holder changed           │
│           │                                                        │
│           └── for each token with active loans:                    │
│                ├── update price history                            │
│                ├── WalletProfiler.profile(borrower)                │
│                │    ├── verifySaid(address)                        │
│                │    └── analyzeTradeHistory(address)               │
│                ├── scoreLoan(token, borrower, position, profile)   │
│                │    ├── LTV proximity     (35%)                    │
│                │    ├── price momentum    (25%)                    │
│                │    ├── wallet reputation (20%)                    │
│                │    └── interest burden   (20%)                    │
│                └── Liquidator.tryLiquidate(connection, scored)     │
│                     ├── check: liquidatable? above threshold?      │
│                     ├── buildLiquidateTransaction(vault=creator)   │
│                     ├── partialSign(agentKeypair)                  │
│                     └── sendRawTransaction → confirmTransaction    │
└────────────────────────────────────┬────────────────────────────┘
                                     │
                                     ▼
┌─────────────────────────────────────────────────────────────────┐
│                      torchsdk v3.7.23                              │
│                                                                    │
│  Read queries: getTokens, getToken, getLendingInfo, getHolders,    │
│                getLoanPosition, getVault, getVaultForWallet         │
│  Builders:     buildLiquidateTransaction, buildCreateTokenTx       │
│  Identity:     verifySaid                                          │
└────────────────────────────────────┬────────────────────────────┘
                                     │
                                     ▼
┌─────────────────────────────────────────────────────────────────┐
│               Solana RPC + Raydium + Torch Program                 │
└─────────────────────────────────────────────────────────────────┘
```

---

## Module Structure

### Bot Core (`src/`)

| File | Role | Lines |
|------|------|-------|
| `index.ts` | Entry point -- vault verification, startup banner, CLI routing, all exports | ~130 |
| `config.ts` | Env validation, ephemeral keypair generation, vault creator loading | ~80 |
| `monitor.ts` | Main scan loop -- scan, score, liquidate, rotate leases | ~83 |
| `scanner.ts` | Lending market discovery, borrower probing | ~107 |
| `liquidator.ts` | Vault-routed liquidation with safety checks | ~89 |
| `risk-scorer.ts` | Four-factor weighted risk scoring | ~89 |
| `wallet-profiler.ts` | SAID verification, trade history, caching | ~127 |
| `launcher.ts` | Domain token creation via `buildCreateTokenTransaction` | ~39 |
| `domain-manager.ts` | Lease tracking, top holder checking, rotation | ~72 |
| `ticker.ts` | Token symbol generation from domain names | ~35 |
| `logger.ts` | Structured logger with child logger support (shared across all modules) | ~35 |
| `types.ts` | All interfaces -- BotConfig, MonitoredToken, ScoredLoan, etc. | ~110 |
| `utils.ts` | sleep, sol, bpsToPercent, clamp, decodeBase58 | ~34 |

### Scraper (`src/scraper/`)

| File | Role |
|------|------|
| `index.ts` | CLI entry, pipeline orchestration |
| `scanner.ts` | Domain scanning orchestrator |
| `evaluator.ts` | Quality scoring (length, TLD, keywords, market signals) |
| `ticker.ts` | Symbol generation |
| `config.ts` | Scraper config (env vars) |
| `types.ts` | Scraper types (re-exports LogLevel from parent) |
| `providers/expired-domains.ts` | Expiring domain feed |
| `providers/availability.ts` | Domain availability checking |

---

## Design Principles

### 1. Domain = Token (Permanent)

After migration, the link between domain and token is immutable. No admin can delist it. No expiry timer. The token IS the domain's on-chain identity. This is the foundation everything else builds on.

### 2. Top Holder = Domain Controller

The `domain-manager.ts` module continuously tracks the top holder for each domain token. When the top holder changes (purchase, liquidation, transfer), the lease rotates. This is checked every scan cycle.

The lease is a 7-day window by default, but the top holder can change at any time. The lease serves as a debounce -- it prevents the domain from flipping on every small trade.

### 3. Vault-First

Every liquidation routes through the Torch Vault. The `vault` parameter is passed to `buildLiquidateTransaction`, causing:
- Vault PDA derived from `vaultCreator` (`["torch_vault", creator]`)
- Wallet link PDA derived from `liquidator` (`["vault_wallet", wallet]`)
- SOL debited from vault, collateral tokens credited to vault ATA

The agent never holds value. The vault is the economic boundary.

### 4. Risk Scoring Over Simple Threshold

The liquidation-kit liquidates every position above the 65% threshold. This bot is more selective -- it scores each position across four factors and only liquidates above a configurable risk threshold. This allows the operator to tune aggressiveness.

### 5. Disposable Keypair

By default, `Keypair.generate()` on every startup. Optionally, `SOLANA_PRIVATE_KEY` (base58 or JSON byte array) persists the agent across restarts. The keypair exists only in runtime memory.

### 6. Fail-Safe Startup

Before entering the scan loop:
1. `getVault(connection, vaultCreator)` -- vault must exist
2. `getVaultForWallet(connection, agentPubkey)` -- agent must be linked

Failure exits with clear instructions. The bot never runs with an invalid vault.

---

## Scan Algorithm

```
scanForLendingMarkets():
  get migrated tokens (getTokens, limit=depth)
  for each token:
    skip if scanned <30s ago
    fetch detail + lending info
    if active_loans > 0:
      probe top 20 holders for loan positions
    build/update MonitoredToken

updateLeases():
  expire leases past duration
  for each domain token:
    check top holder (getHolders, limit=1)
    if holder != current lessee:
      expire old lease, create new

scoring + liquidation:
  for each token with borrowers:
    update price history
    for each borrower:
      get loan position
      profile wallet (SAID + trades, cached 60s)
      score: LTV proximity * 0.35
           + price momentum * 0.25
           + wallet risk    * 0.20
           + interest burden * 0.20
      if liquidatable AND risk >= threshold AND profit >= minimum:
        buildLiquidateTransaction(vault=creator)
        sign + send + confirm
```

### Risk Score Factors

| Factor | Weight | What It Measures | Range |
|--------|--------|-----------------|-------|
| LTV Proximity | 35% | How close current LTV is to 65% threshold | 0-100 |
| Price Momentum | 25% | Recent price trend (drop = higher risk) | 0-100 |
| Wallet Risk | 20% | SAID verification, trade history, win rate | 0-100 |
| Interest Burden | 20% | Accrued interest relative to borrowed amount | 0-100 |

A score of 60+ (default threshold) with the position being `liquidatable` triggers a liquidation attempt.

---

## Domain Lifecycle

```
┌─────────┐     ┌────────────┐     ┌───────────┐
│ DISCOVER │ ──→ │  EVALUATE  │ ──→ │  LAUNCH   │
│ (scraper)│     │ (quality)  │     │ (bonding) │
└─────────┘     └────────────┘     └─────┬─────┘
                                         │
                                    bonding completes
                                         │
                                         ▼
                                   ┌───────────┐
                                   │  MIGRATE  │
                                   │ (raydium) │
                                   └─────┬─────┘
                                         │
                              domain permanently linked
                                         │
                    ┌────────────────────┼──────────────────────┐
                    │                    │                      │
                    ▼                    ▼                      ▼
              ┌───────────┐      ┌────────────┐        ┌────────────┐
              │   TRADE   │      │   LEND     │        │   LEASE    │
              │ (buy/sell)│      │ (borrow)   │        │ (top holder│
              └───────────┘      └──────┬─────┘        │  controls) │
                                        │              └──────┬─────┘
                                   if underwater              │
                                        │                     │
                                        ▼                     │
                                 ┌────────────┐               │
                                 │ LIQUIDATE  │───────────────┘
                                 │ (keeper)   │  collateral moves
                                 └────────────┘  → holder changes
                                                 → lease rotates
```

---

## Vault Integration

### Liquidation Transaction

```typescript
const result = await buildLiquidateTransaction(connection, {
  mint: scored.mint,
  liquidator: this.config.walletKeypair.publicKey.toBase58(),
  borrower: scored.borrower,
  vault: this.config.vaultCreator,
})
```

### Safety Checks in Liquidator

The `tryLiquidate` method checks four conditions before executing:
1. Position health is `liquidatable` (not `healthy` or `warning`)
2. Risk score exceeds configured threshold
3. Estimated profit exceeds minimum
4. Position is not already healthy (redundant safety check)

Only after all four pass does it build and submit the transaction.

---

## Configuration

```typescript
interface BotConfig {
  rpcUrl: string            // SOLANA_RPC_URL (required)
  walletKeypair: Keypair    // generated or loaded from SOLANA_PRIVATE_KEY
  vaultCreator: string      // VAULT_CREATOR (required)
  scanIntervalMs: number    // BOT_SCAN_INTERVAL_MS (default 60000, min 5000)
  scoreIntervalMs: number   // BOT_SCORE_INTERVAL_MS (default 15000)
  minProfitLamports: number // BOT_MIN_PROFIT_LAMPORTS (default 10000000)
  riskThreshold: number     // BOT_RISK_THRESHOLD (default 60)
  priceHistoryDepth: number // BOT_PRICE_HISTORY_DEPTH (default 20)
  logLevel: LogLevel        // BOT_LOG_LEVEL (default 'info')
}
```

---

## Dependencies

All pinned to exact versions. No `^` or `~` ranges.

| Package | Version | Purpose |
|---------|---------|---------|
| `@solana/web3.js` | 1.98.4 | Solana RPC, keypair, transaction |
| `torchsdk` | 3.7.23 | Torch Market SDK |
| `@coral-xyz/anchor` | 0.32.1 | Anchor program interaction |
| `@solana/spl-token` | 0.4.14 | Token-2022 operations |
| `bs58` | 6.0.0 | Base58 encoding |

| Dev Package | Version | Purpose |
|-------------|---------|---------|
| `@types/node` | 20.19.33 | TypeScript types |
| `prettier` | 3.8.1 | Code formatting |
| `typescript` | 5.9.3 | Compilation |

---

## Why This Architecture

The domain lending model composes five Torch primitives that already exist:

1. **Bonding curves** -- price discovery for new domains
2. **Raydium migration** -- permanent liquidity after bonding
3. **Lending markets** -- borrow against domain token positions
4. **Vaults** -- safe custody for autonomous keeper operations
5. **Liquidation** -- market health enforcement + domain rotation

None of these were designed specifically for domains. They're general-purpose DeFi primitives. This kit shows how they compose into something new: a domain lending protocol where token position equals domain control, and liquidation has consequences beyond capital loss.

The same pattern works for any asset class. Replace "domain" with "NFT" or "governance seat" or "bandwidth allocation" and the architecture is identical. The bot is the keeper. The vault is the treasury. The token is the key. The lending market is the mechanism. Liquidation is the enforcement.


path: whitepaper.md
content: # torch.market

**a programmable economic substrate**

Brightside Solutions, 2026

[torch.market](https://torch.market) | [developer docs](https://torch-market-docs.vercel.app/) | [audit](https://torch.market/audit.md) | [@torch_market](https://x.com/torch_market/)

---

`torch.market` is a programmable economic substrate built on Solana. Every token launched on the protocol is its own self-sustaining economy — complete with a pricing engine, a central bank, a lending market, community governance, and optional privacy — all enclosed within a single non-extractive system where every action feeds a positive-sum feedback loop.

The protocol treats Solana not as a blockchain, but as a distributed computing substrate coupled with storage. On-chain accounts form a directed graph of economic relationships. PDA seeds define the edges. Handlers define the legal traversals. The result is a composable economic graph where anyone can launch a token and receive a complete, self-reinforcing financial ecosystem out of the box.

Unlike traditional launchpads that extract value from participants, `torch.market` is non-extractive by topology — there is no edge in the graph that removes value from the system. Fees become lending yield. Lending yield becomes community liquidity. Failed tokens become protocol rewards. Every outflow is an inflow somewhere else. This is not a zero-sum game by design.

The architecture works as follows:

---

## The Economic Graph

Every token on `torch.market` instantiates a complete economic ecosystem. The on-chain accounts form a directed acyclic graph where each node is an autonomous economic actor:

```
Per-Token Economy:

  Mint ──── Bonding Curve ──── Treasury
  │              │                 │
  │         Token Vault       Lending ──── Yield / Rewards
  │              │                 │
  │         User Positions    Lending ──── Collateral Vault
  │              │                 │
  │            Votes          Stars ──── Creator Payout
  │                                │
  │                            Migration ──── Raydium DEX Pool
  │
  └── Token-2022 Extensions
       ├── Transfer Fee (0.04%)
       └── Confidential Transfer (optional)

Protocol Layer:

  Protocol Treasury ◄── Fees + Reclaims
       │
       └── Epoch Rewards ──── Active Traders

Vault Layer (optional resolver):

  TorchVault ◄── VaultWalletLink (identity)
       │
       └── Routes to: buy, sell, star, borrow, repay, swap
```

Each node maintains its own invariants. Each edge is structurally enforced by PDA derivation — the relationships between accounts are guaranteed by the runtime, not by application logic. A treasury can only exist for a bonding curve, which can only exist for a mint. The topology is the security model.

The **Torch Vault** acts as a protocol-native graph resolver — a middleware layer that sits between any caller and any action, resolving identity, SOL source, and token destination without knowing or caring what action is being performed. Two PDAs turn every economic flow in the protocol into a custody-aware operation.

Because the graph is complete — every meaningful economic flow is already a valid traversal — new capabilities emerge from the existing structure. Optional privacy is a single extension on the mint node. Vault custody is an optional dimension on every traversal. No refactoring needed. The graph just gets deeper.

---

## 1. Token Treasury: The Core Mechanic

Every token is launched with a **token treasury**, which is a wallet that acts as an automatic market maker and depreciates token supply.

The token treasury is the core mechanic of `torch.market`. Everyone talks supply control, but in `torch.market`, the protocol *is* the supply control. During bonding, the fee structure is as follows:

```
User spends 1 SOL
        │
        ├── 1% → Protocol Fee (pre-bonding only)
        │         ├── 90% → Protocol Treasury
        │         └── 10% → Dev Wallet
        │
        ├── 1% → Token Treasury Fee (lifetime)
        │
        └── 98% → Remainder
                  ├── V2.3 Dynamic → 3-Way Split (V34)
                  │   ├── Token Treasury (19.8%→4% at start→end)
                  │   ├── Creator Wallet (0.2%→1% at start→end)
                  │   └── Total: 20% at start → 5% at completion
                  └── V2.3 Dynamic → Bonding Curve
                      └── 80% at start → 95% at completion
                                ├── 90% → User (tokens)
                                └── 10% → Community Treasury (vote vault)
```

> **Dynamic Treasury Rate**: The treasury SOL split uses inverse decay based on bonding progress. Early buyers contribute more to treasury (stronger early funding), late buyers get more tokens per SOL. The rate is flat across all tiers (Spark 50, Flame 100, Torch 200 SOL).
>
> | 0 SOL | 50% of target | 100% of target |
> |-------|---------------|----------------|
> | 20%   | 12.5%         | 5%             |

This creates a different mindset to how newly minted tokens are created. Users are not just paying into themselves, but paying into the long term growth of their communities.

### How the Token Treasury Benefits Users

The token treasury creates a positive-sum dynamic where every participant's actions strengthen the entire ecosystem:

1. **Treasury Accumulation**: The treasury accumulates SOL with every buy. Post-migration, harvested transfer fees are sold to SOL, growing the treasury's lending pool and earning yield through community borrowing.
2. **Lending Yield**: Treasury SOL is lent to token holders who borrow against their collateral. Interest paid by borrowers flows back into the treasury, compounding its value over time.
3. **No Insider Advantage**: Unlike traditional launches where team allocations can dump on retail, the treasury mechanism ensures that value flows back to all participants. There is no creator token allocation — creators earn through three revenue streams: a 0.2%→1% SOL share during bonding, 15% of post-migration fee swap proceeds, and star payouts.
4. **Community-Funded Migration**: The treasury pays the 0.15 SOL Raydium pool creation fee automatically. Early supporters collectively fund the DEX migration without any single party bearing the cost.
5. **Long-term Alignment**: Because the treasury continuously earns lending yield and epoch rewards, early sellers forfeit future treasury benefits. This incentivizes holding and community building over quick flips.

---

## 2. Community Vote: Token Holders Decide

Each user casts a vote prelaunch to determine what happens to 10% of their tokens.

Once a token reaches its bonding target (50, 100, or 200 SOL depending on tier), the community votes to decide what happens to the tokens held in the community treasury (vote vault). The voters can decide to:

- **Burn**: Destroy the tokens forever, reducing total supply from 1B to ~945M
- **Return to Treasury Lock**: Transfer the tokens to the treasury lock PDA for future governance release

Providing a group proposal solidifies project community before the DEX launch and gives all wallets a say:

```
1 wallet = 1 vote
```

The vote outcome is binding and executed automatically during migration.

---

## 3. Wallet Limits: Anti-Whale Protection

Any given wallet is restricted to at most *2% of the entire supply* of the token.

By restricting wallets to a hard limit on the total amount of tokens that they own before launch, this ensures better fairness for all wallets purchasing on the bond. Individual wallets can no longer control the entire supply of a given token at once, limiting the chance of price manipulation and dumping on incoming buyers.

New buyers may also be more likely to purchase a token seeing that it is "safer" from whale manipulation. A downside to this is that a single user may control more than 1 wallet, which could be considered a sybil attack against the protocol. However, this is partially mitigated by:

- The fee structure itself (sybiling costs more in fees)
- The community treasury (even sybil buyers fund the collective)
- Post-migration transfer fees (every transfer costs 0.04%)

---

## 4. Permissionless Migration

The token treasury pays the migration fee to DEX. Anyone can trigger it.

One of the main issues with current launchpads is that somebody has to pay the migration fee for the token to be migrated to a decentralized exchange. Because the treasury wallet is fully funded by the time the token bonds at its target (50/100/200 SOL), it is given the authority to pay the Raydium pool creation fee (0.15 SOL).

Migration is **permissionless** — any wallet can trigger the migration for any bonding-complete token. The triggering wallet pays a small rent fee (~0.02 SOL) for the WSOL account, while the treasury covers the 0.15 SOL Raydium pool creation fee. This means no single party can block a token from graduating to DEX.

The migration is executed as a two-step atomic process within a single transaction:

1. **Fund WSOL**: Wrap the bonding curve's SOL reserves into a WSOL token account
2. **Migrate to DEX**: Vote finalization, pool creation on Raydium CPMM, liquidity provision (SOL + tokens), LP token burn (liquidity locked forever), transfer fee activation (0.04% on all future transfers)

When your token bonds, anyone can complete the migration. The community is not dependent on the creator or any centralized operator.

---

## 5. Post-Migration: Treasury Accumulation Loop

Once a token migrates to Raydium, the treasury continues growing through the **0.04% transfer fee** and **lending yield**.

### The 0.04% Transfer Fee (Token-2022)

All `torch.market` tokens use Solana's Token-2022 standard with a built-in **0.04% transfer fee** (4 basis points). This fee is collected on every transfer — wallet to wallet, DEX trades, everything.

```
User transfers 100,000 tokens
        │
        └── 0.04% (4 tokens) → Withheld in mint
                            │
                            └── Harvested → Token Treasury
                                        │
                                   swap_fees_to_sol
                                        │
                                   ┌────┴────┐
                                   │         │
                                   ▼         ▼
                              Treasury   Creator
                               (85%)     (15%)
```

The transfer fee is not extracted from the sender or receiver as a separate charge — it's automatically withheld from the transferred amount at the Solana runtime level. The rate is immutable once set at token creation. Pre-V34 tokens retain their original 3 bps (0.03%) rate.

### Harvest and Sell Cycle

The accumulated transfer fees create a perpetual treasury growth engine:

1. **Harvest** (permissionless): Anyone can call `harvest_fees` to collect withheld tokens from transfers into the token treasury's token account.
2. **Swap to SOL** (permissionless): Anyone can call `swap_fees_to_sol` to sell the harvested tokens back to SOL via Raydium. Proceeds are split 85% to treasury SOL balance, 15% to creator wallet. The SDK bundles harvest + swap in one atomic transaction via `buildSwapFeesToSolTransaction`.
3. **Lend**: Treasury SOL is available for community members to borrow against their token collateral (see Section 6).
4. **Earn**: Interest from borrowers flows back into the treasury, compounding its value.

### Sell Cycle Parameters

- **Interval**: Minimum ~18 minutes between sells (cooldown)
- **Sell Amount**: 15% of held tokens per call (100% if <= 1M tokens)
- **Sell Trigger**: Price > 120% of migration baseline

The treasury accumulates SOL through fee harvesting and lending interest, creating a self-sustaining growth loop.

### Treasury Behavior Summary

| Phase | SOL Source | Token Destination |
|-------|-----------|-------------------|
| Bonding | 1% fee + 20%→5% of buys (dynamic, 3-way: treasury + creator + curve) | Community treasury (vote vault) |
| DEX | 0.04% transfer fee → sell to SOL (85% treasury, 15% creator) | Treasury SOL → lending pool + epoch rewards |

---

## 6. Treasury Lending

After migration, the token treasury holds SOL accumulated from fee harvesting. Holders can **borrow SOL against their tokens**, turning idle treasury capital into productive liquidity while earning yield for the treasury.

### How It Works

1. **Deposit Collateral**: A holder deposits tokens into the lending vault. The tokens are locked but remain the borrower's property.
2. **Borrow SOL**: The borrower receives SOL from the token treasury up to the maximum loan-to-value ratio. The borrowed amount is capped by both the LTV and the treasury's utilization cap.
3. **Repay**: The borrower returns the SOL plus accrued interest. Interest is calculated per-epoch (approximately 7 days). Upon repayment, collateral tokens are unlocked.
4. **Liquidation**: If the collateral value falls below the liquidation threshold, anyone can liquidate the position. The liquidator repays the debt and receives the collateral plus a bonus.

### Lending Parameters

- **Max LTV**: 50% — borrow up to half the value of deposited collateral
- **Liquidation Threshold**: 65% — position is liquidatable when debt exceeds 65% of collateral value
- **Interest Rate**: 2% per epoch (~7 days)
- **Liquidation Bonus**: 10% — liquidators receive collateral at a 10% discount
- **Utilization Cap**: 70% — at most 70% of treasury SOL can be lent out at any time

### Collateral Pricing

Token prices are derived from the Raydium pool reserves. The protocol reads the pool's SOL and token balances on-chain and computes the spot price. No external oracles are required — pricing is fully on-chain and permissionless.

### Virtuous Cycle

Interest paid by borrowers flows back into the token treasury, compounding its SOL balance. Community members borrow SOL → buy tokens → generate volume → generate transfer fees → fees harvested and sold to SOL → treasury grows → more SOL available for lending. The treasury becomes a self-reinforcing liquidity engine.

> **Immutable Parameters**: All lending parameters (LTV, liquidation threshold, interest rate, bonus, utilization cap) are set at pool creation and are immutable on-chain. No admin key can change them after deployment.

---

## 7. Protocol Treasury: Rewarding Active Traders

The protocol level fees don't just go to the development team, they're redistributed to active platform users.

### How It Works

During the bonding phase, 1% of every buy goes to the protocol. This is split:

- 90% → Protocol Treasury (for user rewards)
- 10% → Dev Wallet (for development)

The Protocol Treasury accumulates SOL and distributes it to active traders every *7 days (1 epoch)*.

### Epoch Reward Distribution

1. **No Reserve Floor**: All accumulated fees are distributed each epoch. No SOL held back.
2. **Volume Eligibility**: To claim rewards, a user must have traded at least *2 SOL in volume* during the previous epoch.
3. **Minimum Claim**: Claims below 0.1 SOL are rejected (prevents dust drain).
4. **Pro-Rata Share**: Eligible users receive rewards proportional to their trading volume:
   ```
   user_reward = (user_volume / total_volume) × distributable_amount
   ```
5. **Claim**: Users must actively claim their rewards. Unclaimed rewards roll into the next epoch.

### Example

If the protocol treasury has 500 SOL after an epoch:
- Distributable: 500 SOL (all of it — no reserve floor)

If total eligible volume was 50,000 SOL and you traded 5,000 SOL:
- Your share: 5,000 / 50,000 = 10%
- Your reward: **50 SOL**

This mechanism rewards the most active participants on the platform and creates an incentive loop: more trading → more fees → more rewards → more trading.

---

## 8. Token Reclaim and Revival

Not every token succeeds. `torch.market` has mechanisms to handle failed tokens and even give them a second chance.

### Reclaim: Cleaning Up Failed Tokens

If a token fails to reach its bonding target (50/100/200 SOL depending on tier) and becomes inactive for *7 days*, anyone can trigger a reclaim:

```
Conditions for reclaim:
  ✓ Bonding not complete (target not reached)
  ✓ No trading activity for 7+ days
  ✓ At least 0.01 SOL in reserves (not dust)
```

When reclaimed:

1. All SOL from the bonding curve is transferred to the protocol treasury
2. All SOL from the token treasury is transferred to the protocol treasury
3. The token is marked as "reclaimed" and trading is disabled

The reclaimed SOL joins the protocol treasury and is distributed to active traders in the next epoch. Failed tokens become rewards for successful traders.

### Revival: Second Chances

A reclaimed token can be **revived** if the community believes in it. Anyone can contribute SOL to a reclaimed token:

```
Revival threshold (V27): 3BT/8 (18.75 SOL Spark, 37.5 SOL Flame, 75 SOL Torch)
Legacy tokens: BT/8 (6.25 SOL Spark, 12.5 SOL Flame, 25 SOL Torch)
```

Contributors are patrons — they do NOT receive tokens for their contribution. They're simply signaling belief that the token deserves another chance. Once the revival threshold is reached:

1. The `reclaimed` flag is removed
2. Trading is re-enabled
3. The token continues from where it left off

This creates a natural market for "distressed" tokens. If a token had real community support but just needed more time, revival gives it that chance.

---

## 9. On-Chain Messages

Every token on `torch.market` has a **message board**. Messages are stored on-chain using the SPL Memo program, making them permanent and censorship-resistant.

### Skin in the Game

Messages can be bundled with trades. When a user buys or sells a token, they can attach a message to the transaction. This ties commentary directly to economic action — every message comes from someone with skin in the game.

### Standalone Messages

Users can also post standalone messages without trading. These are recorded via the SPL Memo program and associated with the token's message board. Standalone messages still require a wallet signature, ensuring accountability.

### Why On-Chain?

- **Permanence**: Messages cannot be deleted or altered after posting
- **Attribution**: Every message is signed by the sender's wallet
- **Context**: Trade-attached messages show what the sender did, not just what they said
- **Composability**: Any client, bot, or agent can read and post messages using the same on-chain interface

---

## 10. Verification & Trust (SAID Protocol)

`torch.market` integrates the **SAID protocol** — an on-chain identity layer for agents and humans. SAID provides verifiable trust without requiring personal information.

### Trust Tiers

Each verified wallet receives a trust tier based on on-chain activity and verification depth:

| Tier | Color |
|------|-------|
| High | Emerald / Green |
| Medium | Blue |
| Low | Yellow |

### Where Badges Appear

- Token cards on the explore page
- Token detail pages (next to the creator wallet)
- Message boards (next to each message author)

### Reputation Scoring

Reputation is earned through on-chain activity on the platform:

- **+15 points**: Launch a token
- **+5 points**: Execute a trade
- **+10 points**: Cast a community vote

---

## 11. Built for Agents

`torch.market` is designed for both humans and AI agents. There is no API server between the agent and the protocol. Solana is the compute layer. The Torch SDK builds transactions locally from the on-chain program's Anchor IDL and reads all state directly from Solana RPC. No middleman, no API keys, no trust assumptions beyond the on-chain program itself.

### Direct On-Chain Access

Every protocol action — buy, sell, lend, govern, message — is an instruction on the Solana program. The SDK constructs these instructions locally using the Anchor IDL, serializes them into unsigned transactions, and submits them to any Solana RPC endpoint. The agent signs with its own keypair. No server processes the request. No intermediary touches the transaction. The path is:

```
Agent → SDK (local, Anchor IDL) → Solana RPC → On-chain program
```

This is a direct consequence of treating Solana as a computing substrate. The program is the API. The accounts are the database. The RPC is the network layer. There is nothing else to trust, nothing else to go down, nothing else to rate-limit.

### Discovery Chain

Agents discover `torch.market` through a standard discovery chain:

```
llms.txt          → Human/AI-readable overview
  └── agent.json  → Structured metadata, capabilities, actions
  └── skill.md    → Machine-readable SDK reference
  └── openapi.json → Full OpenAPI specification
```

### Agent Kit Plugin

For agents built on the Solana Agent Kit, a dedicated plugin is available:

```
npm install solana-agent-kit-torch-market
```

The plugin wraps the SDK with typed actions (buy, sell, create, vote, lend, message) and handles transaction signing automatically. Humans and agents use the same on-chain program — there is no separate "bot mode."

---

## 12. Protocol Architecture

The on-chain program is a directed graph of economic relationships. PDA seeds define the edges, handlers define the legal traversals. The topology enforces correctness — relationships between accounts are guaranteed by the Solana runtime, not by application logic.

```
┌─────────────────────────────────────────────────────────────────────────────────────┐
│                          TORCH MARKET PROTOCOL v3.7.8                                 │
├─────────────────────────────────────────────────────────────────────────────────────┤
│                                                                                      │
│  ┌─────────────────────────────────────────────────────────────────────────────┐    │
│  │                           PROTOCOL LAYER                                     │    │
│  │  ┌─────────────────┐  ┌──────────────────────────────────────┐              │    │
│  │  │  GlobalConfig   │  │        ProtocolTreasury               │              │    │
│  │  │  (authority,    │  │  (1% fees + reclaimed SOL,            │              │    │
│  │  │   settings)     │  │   no floor, epoch rewards)            │              │    │
│  │  └─────────────────┘  └──────────────────────────────────────┘              │    │
│  └─────────────────────────────────────────────────────────────────────────────┘    │
│                                       │                                              │
│                                       ▼                                              │
│  ┌─────────────────────────────────────────────────────────────────────────────┐    │
│  │                           PER-TOKEN LAYER                                    │    │
│  │                                                                              │    │
│  │  ┌──────────────┐    ┌──────────────┐    ┌──────────────┐                   │    │
│  │  │    Token     │    │   Bonding    │    │   Treasury   │                   │    │
│  │  │   (Mint)     │───▶│    Curve     │───▶│  (lending,   │                   │    │
│  │  │  Token-2022  │    │  (pricing,   │    │   stars,     │                   │    │
│  │  │ 0.04% xfer │    │   voting)    │    │   lending)   │                   │    │
│  │  └──────────────┘    └──────┬───────┘    └──────────────┘                   │    │
│  │                             │                                                │    │
│  │         ┌───────────────────┼───────────────────┐                           │    │
│  │         ▼                   ▼                   ▼                           │    │
│  │  ┌──────────────┐    ┌──────────────┐    ┌──────────────┐                   │    │
│  │  │  Token Vault │    │  Treasury's  │    │  Raydium     │                   │    │
│  │  │  (tradeable  │    │  Token Acct  │    │  CPMM Pool   │                   │    │
│  │  │   supply)    │    │  (vote vault)│    │  (post-grad) │                   │    │
│  │  └──────────────┘    └──────────────┘    └──────────────┘                   │    │
│  └─────────────────────────────────────────────────────────────────────────────┘    │
│                                       │                                              │
│                                       ▼                                              │
│  ┌─────────────────────────────────────────────────────────────────────────────┐    │
│  │                            USER LAYER                                        │    │
│  │  ┌─────────────────┐  ┌─────────────────┐  ┌─────────────────┐              │    │
│  │  │  UserPosition   │  │   UserStats     │  │   StarRecord    │              │    │
│  │  │  (per-token     │  │  (platform-wide │  │  (per-token     │              │    │
│  │  │   holdings,     │  │   volume,       │  │   appreciation) │              │    │
│  │  │   vote)         │  │   rewards)      │  │                 │              │    │
│  │  └─────────────────┘  └─────────────────┘  └─────────────────┘              │    │
│  └─────────────────────────────────────────────────────────────────────────────┘    │
│                                       │                                              │
│                                       ▼                                              │
│  ┌─────────────────────────────────────────────────────────────────────────────┐    │
│  │                    VAULT LAYER (V3.1.0 — Full Custody)                       │    │
│  │  ┌─────────────────┐  ┌─────────────────┐  ┌─────────────────┐              │    │
│  │  │   TorchVault    │  │VaultWalletLink  │  │  Vault ATAs     │              │    │
│  │  │  (per-creator   │◀─│  (per-wallet    │  │  (per-mint      │              │    │
│  │  │   SOL + token   │  │   reverse       │  │   token accts   │              │    │
│  │  │   full custody) │  │   pointer)      │  │   owned by PDA) │              │    │
│  │  └─────────────────┘  └─────────────────┘  └─────────────────┘              │    │
│  └─────────────────────────────────────────────────────────────────────────────┘    │
│                                                                                      │
├─────────────────────────────────────────────────────────────────────────────────────┤
│  INSTRUCTION HANDLERS (27 total)                                                     │
│  ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐      │
│  │ admin  │ │ token  │ │ market │ │treasury│ │ dex    │ │rewards │ │reclaim │      │
│  │        │ │        │ │        │ │/lending│ │migrate │ │        │ │/revival│      │
│  └────────┘ └────────┘ └────────┘ └────────┘ └────────┘ └────────┘ └────────┘      │
│  ┌────────┐ ┌────────┐                                                               │
│  │ vault  │ │  swap  │                                                               │
│  │        │ │(V3.1.1)│                                                               │
│  └────────┘ └────────┘                                                               │
└─────────────────────────────────────────────────────────────────────────────────────┘
```

### On-Chain Account Types

The protocol uses 12 on-chain account types, all deterministic PDAs:

| Account | PDA Seeds | Purpose |
|---------|-----------|---------|
| **GlobalConfig** | `["global_config"]` | Protocol-wide settings (authority, fees, pause flag) |
| **BondingCurve** | `["bonding_curve", mint]` | Per-token pricing state, reserves, votes, bonding target |
| **Treasury** | `["treasury", mint]` | Per-token treasury (SOL for lending, star balance, fee accumulation) |
| **TreasuryLock** | `["treasury_lock", mint]` | [V31] Holds 300M locked tokens (30% of supply) |
| **UserPosition** | `["user_position", bc, user]` | Per-user per-token holdings and vote |
| **UserStats** | `["user_stats", user]` | Platform-wide volume and reward tracking |
| **ProtocolTreasury** | `["protocol_treasury_v11"]` | Single treasury: fees + reclaims, no floor, epoch rewards (V32) |
| **StarRecord** | `["star_record", user, mint]` | Prevents double-starring |
| **LoanPosition** | `["loan", mint, user]` | Per-user per-token lending position |
| **TorchVault** | `["torch_vault", creator]` | Per-creator full-custody SOL + token escrow |
| **VaultWalletLink** | `["vault_wallet", wallet]` | Reverse pointer: wallet → vault (one link per wallet) |

### Instruction Set

The V3.7.8 program exposes 27 instructions across 9 handler domains:

| Domain | Instructions |
|--------|-------------|
| **Admin** | `initialize`, `initialize_protocol_treasury`, `update_dev_wallet` |
| **Token** | `create_token` |
| **Market** | `buy`, `sell` |
| **Treasury** | `harvest_fees`, `swap_fees_to_sol` |
| **Migration** | `fund_migration_wsol`, `migrate_to_dex` |
| **Rewards** | `advance_protocol_epoch`, `claim_protocol_rewards`, `star_token` |
| **Reclaim/Revival** | `reclaim_failed_token`, `contribute_revival` |
| **Vault** | `create_vault`, `deposit_vault`, `withdraw_vault`, `link_wallet`, `unlink_wallet`, `transfer_authority`, `withdraw_tokens` |
| **Swap** | `fund_vault_wsol`, `vault_swap` |
| **Lending** | `borrow`, `repay`, `liquidate` |

> **Note (V3.7.0):** `update_authority` was removed. Minimal admin surface: only `initialize` and `update_dev_wallet` require authority.
>
> **Note (V3.7.5):** V31 zero-burn migration. CURVE_SUPPLY 750M→700M, TREASURY_LOCK 250M→300M. Transfer fee 0.1%→0.03%. Vote return → treasury lock.
>
> **Note (V3.7.6):** V32 protocol treasury rebalance. Reserve floor removed (0 SOL). Volume eligibility 10→2 SOL. Min claim 0.1 SOL. Fee split 90/10 (was 75/25). 39 Kani proofs.
>
> **Note (V3.7.7):** V33 buyback removed, lending extended. `execute_auto_buyback` instruction removed (27 instructions). Lending utilization cap 50%→70%. Treasury simplified to fee harvest → sell → SOL → lending yield + epoch rewards.
>
> **Note (V3.7.8):** V34 creator revenue. Three creator income streams: bonding SOL share (0.2%→1% carved from treasury rate), 15% of post-migration fee swap proceeds, star payout (cost reduced 0.05→0.02 SOL). Transfer fee 3→4 bps (new tokens only). `creator` account added to `buy` and `swap_fees_to_sol`. 43 Kani proofs.

### Bonding Curve Formula

The protocol uses a constant product bonding curve:

```
Buy:   tokens_out = (virtual_token_reserves × sol_in) / (virtual_sol_reserves + sol_in)
Sell:  sol_out    = (virtual_sol_reserves × token_in) / (virtual_token_reserves + token_in)
Price: price      = virtual_sol_reserves / virtual_token_reserves
```

**Tiered Virtual Reserves (V27):** Each tier has per-tier initial virtual reserves tuned for a consistent ~13.44x multiplier:

| Tier | Target | IVS (3BT/8) | IVT | Curve Supply | Treasury Lock |
|------|--------|-------------|-----|-------------|---------------|
| **Spark** | 50 SOL | 18.75 SOL | 756.25M | 700M (70%) | 300M (30%) |
| **Flame** | 100 SOL | 37.5 SOL | 756.25M | 700M (70%) | 300M (30%) |
| **Torch** | 200 SOL | 75 SOL | 756.25M | 700M (70%) | 300M (30%) |

### Fee Flow

```
                              BUYER'S SOL
                                  │
                                  ▼
               ┌──────────────────────────────────────┐
               │           TOTAL SOL INPUT            │
               └──────────────────────────────────────┘
                                  │
           ┌──────────────────────┼──────────────────────┐
           │                      │                      │
           ▼                      ▼                      ▼
    ┌─────────────┐       ┌─────────────┐       ┌─────────────┐
    │  1% Protocol│       │ 1% Treasury │       │    98%      │
    │    Fee      │       │    Fee      │       │  Remaining  │
    └──────┬──────┘       └──────┬──────┘       └──────┬──────┘
           │                     │                     │
      ┌────┴────┐                │              ┌──────┴──────┐
      │         │                │              │             │
      ▼         ▼                ▼              ▼             ▼
┌─────────┐ ┌─────────┐   ┌──────────┐   ┌───────────┐ ┌──────────┐ ┌───────────┐
│Protocol │ │  Dev    │   │  Token   │   │  Token    │ │ Creator  │ │  Bonding  │
│Treasury │ │ Wallet  │   │ Treasury │   │ Treasury  │ │ Wallet   │ │   Curve   │
│  (90%)  │ │  (10%)  │   │  (100%)  │   │(19.8→4%)*│ │(0.2→1%)*│ │(80%→95%)* │
└─────────┘ └─────────┘   └────┬─────┘   └───────────┘ └──────────┘ └─────┬─────┘
                               │                                           │
                               │    *V34: Treasury split 3 ways:           ▼
                               │    Total 20%→5% (V25 flat decay)   ┌─────────────┐
                               │    Creator 0.2%→1% (carved out)    │   TOKENS    │
                               │    Treasury gets remainder         │    OUT      │
                               │                                    └──────┬──────┘
                               │                                           │
                               │                      ┌────────┴────────┐
                               │                      │                 │
                               ▼                      ▼                 ▼
                        ┌─────────────┐        ┌─────────────┐   ┌───────────┐
                        │  LENDING    │        │   BUYER     │   │ COMMUNITY │
                        │  (yield)    │        │   (90%)     │   │ TREASURY  │
                        └─────────────┘        └─────────────┘   │   (10%)   │
                                                                 └─────┬─────┘
                                                                       │
                                                           ┌───────────┴───────────┐
                                                           │      AT MIGRATION     │
                                                           │    (based on vote)    │
                                                           ├───────────────────────┤
                                                           │ BURN → destroy tokens │
                                                           │ RETURN → treasury lock│
                                                           └───────────────────────┘
```

### Security Model

**Access Control:**
- **Authority-only:** `initialize`, `update_dev_wallet`
- **Vault authority-only:** `withdraw_vault`, `link_wallet`, `unlink_wallet`, `transfer_authority`, `withdraw_tokens`
- **Permissionless cranks:** `advance_protocol_epoch`, `harvest_fees`, `swap_fees_to_sol`, `fund_migration_wsol`, `migrate_to_dex`, `reclaim_failed_token`, `liquidate`
- **Permissionless deposits:** Anyone can deposit into any vault

**Vault Security (V3.1.0 — Full Custody):**
- One vault per creator (PDA uniqueness)
- One link per wallet (PDA uniqueness)
- Authority separation: `creator` (immutable seed) vs `authority` (transferable admin)
- All value stays in vault — agent wallet never holds tokens or significant SOL
- CPI ordering enforced: token CPIs before lamport manipulation in all vault paths
- Compromised key safety: attacker gets dust, authority unlinks and re-links

**Raydium Pool Validation (V27 — PDA-Based):**
Pool accounts validated via PDA derivation constraints in Anchor contexts (`derive_pool_state`, `derive_pool_vault`, `derive_observation_state`). AMM config hardcoded to prevent fee-tier substitution. Oracle manipulation impossible — an attacker cannot derive a valid PDA pointing to a fake pool.

### Formal Verification

Core arithmetic is formally verified with [Kani](https://model-checking.github.io/kani/) — 43 proof harnesses, all passing, covering every possible input in constrained ranges. Proofs cover: fee calculations, bonding curve pricing, lending formulas (borrow/repay/liquidate lifecycle), reward distribution, sell-cycle ratio math, migration conservation, and V25/V27 token distribution. No SOL can be created from nothing, no tokens can be minted from thin air, and no fees can exceed their stated rates. See [VERIFICATION.md](https://torch.market/verification.md).

### Security Audit History

**V3.2.1 — `harvest_fees` Unconstrained Destination (CRITICAL, Fixed)**
The `harvest_fees` instruction did not validate that `treasury_token_account` matched the treasury PDA's ATA. An attacker could substitute their own Token-2022 ATA and steal all accumulated transfer fees. Fixed with Anchor `associated_token` constraints. Independent auditor verified.

**V3.2.1 — Oracle Manipulation via Unconstrained Raydium Pool (Non-Issue)**
Pool accounts were reported as unconstrained. Assessment: `validate_pool_accounts()` already validates pool ownership, vault addresses, and mint composition. V27 further hardens this with PDA-based derivation constraints.

---

## Token Lifecycle

```
CREATE → BONDING → COMPLETE → VOTE → MIGRATE → DEX
   │                                              │
   │                                              ▼
   │                                   [0.04% Transfer Fee]
   │                                              │
   │                                              ▼
   │                                    HARVEST → SWAP TO SOL → LENDING → YIELD
   │                                              │
   │                                     ┌────────┴────────┐
   │                                     │                  │
   │                              [TREASURY LENDING]  [MESSAGE BOARD]
   │                                     │
   │                              BORROW ↔ REPAY
   │                                     │
   │                                LIQUIDATION
   │
   ▼ (if 7 days inactive)
RECLAIM ──────────────────────────────────────────────────────┐
   │                                                           │
   ▼                                                           ▼
REVIVAL (IVS per tier) ──→ TRADING RESUMES          PROTOCOL TREASURY
                                                              │
                                                              ▼
                                                    EPOCH REWARDS TO TRADERS
```

Every path in this graph feeds value back into the system. There is no terminal node that extracts value — only cycles that compound it.

---

## Constants Reference

| Parameter | Value | Description |
|-----------|-------|-------------|
| Total Supply | 1,000,000,000 | Initial token supply (6 decimals) |
| Max Wallet | 2% (20,000,000) | Maximum tokens per wallet during bonding |
| Bonding Target | 50 / 100 / 200 SOL | Spark / Flame / Torch tier (creator chooses at launch) |
| Community Treasury | 10% | Portion of bought tokens to vote vault |
| Treasury SOL Share | 20%→5% | Dynamic: decays as bonding progresses |
| Token Treasury Fee | 1% | Fee on all buys (lifetime) |
| Protocol Fee | 1% | Fee during bonding (90% treasury, 10% dev) |
| Transfer Fee | 0.04% (4 bps) | [V34] Post-migration fee on all transfers (immutable per mint, pre-V34 tokens retain 3 bps) |
| Inactivity Period | 7 days | Time before failed token can be reclaimed |
| Revival Threshold (V27) | 3BT/8 per tier (18.75 / 37.5 / 75 SOL) | SOL needed to revive a reclaimed token |
| Voting Duration | ~24 hours | Time for community to vote on burn/return |
| Epoch Duration | 7 days | Protocol reward distribution cycle |
| Reward Eligibility | 2 SOL | Minimum epoch volume for protocol rewards |
| Min Claim | 0.1 SOL | Minimum payout per claim (rejects dust) |
| Protocol Reserve | 0 SOL | All fees distributed each epoch (no floor) |
| Max LTV | 50% | Maximum loan-to-value for treasury lending |
| Liquidation Threshold | 65% | Debt-to-collateral ratio triggering liquidation |
| Interest Rate | 2% / epoch | Lending interest per ~7-day epoch |
| Liquidation Bonus | 10% | Discount for liquidators on seized collateral |
| Utilization Cap | 70% | Max fraction of treasury SOL available for loans |
| Min Borrow | 0.1 SOL | Minimum borrow amount per loan |

---

## Version History

| Version | Features |
|---------|----------|
| V1 | Basic bonding curve, buy/sell |
| V2 | Treasury, fee accumulation, permanent burn split |
| V3 | Token-2022 with transfer fees |
| V4 | Failed token reclaim, platform rewards |
| V5 | Raydium DEX migration |
| V8 | Dev wallet split (10% of protocol fee, updated V32 from 25%) |
| V9 | Ratio-gated sell cycle (buyback removed in V33) |
| V10 | Simplified star system with auto-payout |
| V11 | Protocol treasury with epoch rewards (reserve floor removed in V32) |
| V12 | Token revival |
| V2.2 | 10% tokens to community treasury, 90% to buyer |
| V2.3 | Dynamic treasury SOL rate: 20%→5% decay |
| V2.4 | Treasury lending: borrow SOL against token collateral |
| V3.0.0 | **Torch Vault — Multi-Wallet Identity.** Per-creator SOL escrow with multi-wallet support. 6 new vault instructions. |
| V3.1.0 | **Vault Full Custody.** Buy, sell, star, borrow, repay all vault-routed. New `withdraw_tokens` (authority-only). |
| V3.1.1 | **Vault DEX Swap.** `fund_vault_wsol` + `vault_swap` for Raydium trading via vault. |
| V3.2.0 | **Platform treasury merged into protocol treasury.** Single reward system. |
| V3.2.1 | **Security: `harvest_fees` hardened.** Critical vulnerability fixed, auditor verified. |
| V3.3.0 | **Tiered Bonding Curves.** Spark (50 SOL), Flame (100 SOL), Torch (200 SOL). |
| V3.5.0 | **V25 Pump-Style Token Distribution.** IVS = BT/8, IVT = 900M tokens, ~81x multiplier. 35 Kani proofs. |
| V3.6.0 | **V26 Permissionless Migration + Authority Revocation.** Mint and freeze authority revoked permanently at migration. |
| V3.6.x | **V27 Treasury Lock + PDA Pool Validation.** 250M tokens locked at creation. IVS = 3BT/8, 13.44x multiplier. |
| V3.7.0 | **V28 `update_authority` Removed.** Authority transfer via multisig tooling. 27 instructions. Minimal admin surface. |
| V3.7.5 | **V31 Zero-Burn Migration.** CURVE_SUPPLY 750M→700M, TREASURY_LOCK 250M→300M. Transfer fee 0.1%→0.03%. Vote return → treasury lock. |
| V3.7.6 | **V32 Protocol Treasury Rebalance.** Reserve floor removed. Volume eligibility 10→2 SOL. Min claim 0.1 SOL. Fee split 90/10. 39 Kani proofs. |
| V3.7.7 | **V33 Buyback Removed, Lending Extended.** `execute_auto_buyback` removed (27 instructions). Lending utilization cap 50%→70%. Treasury simplified to: fee harvest → sell → SOL → lending yield + epoch rewards. |
| V3.7.8 | **V34 Creator Revenue + Transfer Fee Bump.** Three creator income streams: bonding SOL share (0.2%→1%), post-migration fee share (85/15 treasury/creator), star payout. Star cost 0.05→0.02 SOL. Transfer fee 3→4 bps (new tokens only). 43 Kani proofs. |

---

## Conclusion

`torch.market` is a programmable economic substrate. Every token launched on the protocol receives a complete, self-sustaining economy: a pricing engine, a treasury with lending yield, community governance, creator rewards, a failure-recovery system, and optional privacy — all composed from a small set of on-chain primitives that enforce correctness by topology.

The protocol is non-extractive by design. There is no configuration that makes it extractive because the graph doesn't have that edge. Fees become lending yield. Lending yield becomes community liquidity. Failed tokens become protocol rewards. Interest from borrowers compounds the treasury. Every outflow is an inflow somewhere else in the system.

The Torch Vault adds a custody-aware resolution layer that makes every economic flow in the protocol accessible through a single identity — without adding economic complexity. Agents and humans use the same on-chain program, the same API, the same graph.

The individual pieces — bonding curves, treasuries, lending, governance — are not new. The arrangement is. A closed, positive-sum economic graph where anyone can launch a token and receive a complete financial ecosystem, running on Solana as a distributed computing substrate.

**This is not a zero-sum game. This is not a launchpad. This is a substrate for programmable economies.**

---

*© 2026 Brightside Solutions. All rights reserved.*

[Terms](https://torch.market/terms) | [Privacy](https://torch.market/privacy) | [torch.market](https://torch.market)


path: lib/kit/types.js
content: "use strict";
Object.defineProperty(exports, "__esModule", { value: true });
//# sourceMappingURL=types.js.map

path: lib/kit/logger.js
content: "use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Logger = void 0;
const LEVELS = { debug: 0, info: 1, warn: 2, error: 3 };
class Logger {
    constructor(module, level = 'info') {
        this.child = (module) => {
            const child = new Logger(`${this.module}:${module}`);
            child.level = this.level;
            return child;
        };
        this.debug = (msg, data) => this.log('debug', msg, data);
        this.info = (msg, data) => this.log('info', msg, data);
        this.warn = (msg, data) => this.log('warn', msg, data);
        this.error = (msg, data) => this.log('error', msg, data);
        this.log = (level, msg, data) => {
            if (LEVELS[level] < this.level)
                return;
            const ts = new Date().toISOString().substr(11, 12);
            const prefix = `[${ts}] [${level.toUpperCase()}] [${this.module}]`;
            if (data !== undefined) {
                console.log(`${prefix} ${msg}`, data);
            }
            else {
                console.log(`${prefix} ${msg}`);
            }
        };
        this.module = module;
        this.level = LEVELS[level];
    }
}
exports.Logger = Logger;
//# sourceMappingURL=logger.js.map

path: lib/kit/scanner.js
content: "use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.scanForLendingMarkets = void 0;
const torchsdk_1 = require("torchsdk");
const utils_1 = require("./utils");
const MAX_PRICE_HISTORY = 50;
/**
 * Discover all active borrowers for a token using bulk loan scanner.
 * Returns all borrower addresses with open positions (not just top-20 holders).
 */
const discoverBorrowers = async (connection, mint, log) => {
    try {
        const { positions } = await (0, utils_1.withTimeout)((0, torchsdk_1.getAllLoanPositions)(connection, mint), 30000, 'getAllLoanPositions');
        return positions.map((p) => p.borrower);
    }
    catch (err) {
        log.debug(`borrower discovery failed for ${mint.slice(0, 8)}...: ${err}`);
        return [];
    }
};
/**
 * Scan for tokens with active lending markets.
 * Discovers migrated tokens, builds MonitoredToken entries, and probes for borrowers.
 */
const scanForLendingMarkets = async (connection, existing, depth, log) => {
    const tokens = new Map(existing);
    log.info(`scanning for lending markets (depth=${depth})`);
    try {
        const result = await (0, utils_1.withTimeout)((0, torchsdk_1.getTokens)(connection, {
            status: 'migrated',
            limit: depth,
            sort: 'newest',
        }), 30000, 'getTokens');
        log.info(`found ${result.tokens.length} migrated tokens`);
        for (const summary of result.tokens) {
            try {
                // skip if recently scanned
                const prev = tokens.get(summary.mint);
                if (prev && Date.now() - prev.lastScanned < 30000)
                    continue;
                const detail = await (0, utils_1.withTimeout)((0, torchsdk_1.getToken)(connection, summary.mint), 30000, 'getToken');
                const lending = await (0, utils_1.withTimeout)((0, torchsdk_1.getLendingInfo)(connection, summary.mint), 30000, 'getLendingInfo');
                const priceSol = detail.price_sol / torchsdk_1.LAMPORTS_PER_SOL;
                const prevHistory = prev?.priceHistory ?? [];
                const trimmedHistory = [...prevHistory, priceSol].slice(-MAX_PRICE_HISTORY);
                // discover borrowers when there are active loans
                let borrowers = prev?.activeBorrowers ?? [];
                if (lending.active_loans && lending.active_loans > 0) {
                    borrowers = await discoverBorrowers(connection, summary.mint, log);
                    log.info(`${detail.symbol}: ${lending.active_loans} active loans, ${borrowers.length} borrowers found, price=${priceSol.toFixed(8)} SOL`);
                }
                tokens.set(summary.mint, {
                    mint: summary.mint,
                    name: detail.name,
                    symbol: detail.symbol,
                    lendingInfo: lending,
                    priceSol,
                    priceHistory: trimmedHistory,
                    activeBorrowers: borrowers,
                    lastScanned: Date.now(),
                });
            }
            catch (err) {
                log.debug(`skipping ${summary.mint}: ${err}`);
            }
        }
    }
    catch (err) {
        log.error('scan failed', err);
    }
    return tokens;
};
exports.scanForLendingMarkets = scanForLendingMarkets;
//# sourceMappingURL=scanner.js.map

path: lib/kit/wallet-profiler.js
content: "use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.WalletProfiler = void 0;
const web3_js_1 = require("@solana/web3.js");
const torchsdk_1 = require("torchsdk");
const utils_1 = require("./utils");
const CACHE_TTL_MS = 30000;
class WalletProfiler {
    constructor(log) {
        this.cache = new Map();
        this.profile = async (connection, address, mint) => {
            // check cache
            const cached = this.cache.get(address);
            if (cached && Date.now() - cached.lastUpdated < CACHE_TTL_MS) {
                this.log.debug(`cache hit: ${address.slice(0, 8)}...`);
                return cached;
            }
            this.log.info(`profiling wallet: ${address.slice(0, 8)}...`);
            // SAID verification
            let saidVerified = false;
            let trustTier = null;
            try {
                const said = await (0, utils_1.withTimeout)((0, torchsdk_1.verifySaid)(address), 10000, 'verifySaid');
                saidVerified = said.verified;
                trustTier = said.trustTier;
            }
            catch {
                this.log.debug(`SAID lookup failed for ${address.slice(0, 8)}...`);
            }
            // analyze trade history via recent transactions
            const tradeStats = await this.analyzeTradeHistory(connection, address);
            // compute wallet risk score (0-100, higher = riskier)
            const riskScore = this.computeRiskScore(saidVerified, trustTier, tradeStats);
            const profile = {
                address,
                saidVerified,
                trustTier,
                tradeStats,
                riskScore,
                lastUpdated: Date.now(),
            };
            this.cache.set(address, profile);
            return profile;
        };
        this.analyzeTradeHistory = async (connection, address) => {
            try {
                const pubkey = new web3_js_1.PublicKey(address);
                const sigs = await connection.getSignaturesForAddress(pubkey, { limit: 50 });
                const totalTrades = sigs.length;
                // winRate = tx success rate (what % of submitted txs landed without error)
                const successfulTrades = sigs.filter((s) => s.err === null).length;
                const winRate = totalTrades > 0 ? successfulTrades / totalTrades : 0;
                // estimate avg hold time from transaction spacing
                let avgHoldTime = 0;
                if (sigs.length >= 2) {
                    const times = sigs
                        .filter((s) => s.blockTime)
                        .map((s) => s.blockTime * 1000)
                        .sort((a, b) => a - b);
                    if (times.length >= 2) {
                        const totalSpan = times[times.length - 1] - times[0];
                        avgHoldTime = totalSpan / times.length;
                    }
                }
                // estimate pnlSol from current balance vs expected airdrop/funding baseline
                let pnlSol = 0;
                try {
                    const balanceLamports = await connection.getBalance(pubkey);
                    // rough heuristic: assume wallet started with ~1 SOL in fees
                    // positive balance beyond that suggests net positive trading
                    pnlSol = balanceLamports / 1000000000 - 1;
                }
                catch {
                    // balance check failed, leave at 0
                }
                return { totalTrades, winRate, avgHoldTime, pnlSol };
            }
            catch {
                return { totalTrades: 0, winRate: 0, avgHoldTime: 0, pnlSol: 0 };
            }
        };
        this.computeRiskScore = (verified, tier, stats) => {
            let risk = 50; // baseline
            // SAID verification reduces risk
            if (verified)
                risk -= 15;
            if (tier === 'high')
                risk -= 10;
            else if (tier === 'medium')
                risk -= 5;
            // more trades = more history = lower risk
            if (stats.totalTrades > 20)
                risk -= 10;
            else if (stats.totalTrades > 5)
                risk -= 5;
            // high win rate = lower risk
            if (stats.winRate > 0.7)
                risk -= 5;
            else if (stats.winRate < 0.3)
                risk += 10;
            // clamp to 0-100
            return Math.min(Math.max(risk, 0), 100);
        };
        this.log = log;
    }
}
exports.WalletProfiler = WalletProfiler;
//# sourceMappingURL=wallet-profiler.js.map

path: lib/kit/index.js
content: #!/usr/bin/env node
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.generateScraperTicker = exports.evaluateDomain = exports.scanDomains = exports.generateTicker = exports.updateLeases = exports.checkTopHolder = exports.launchDomainToken = exports.Logger = exports.Liquidator = exports.scoreLoan = exports.WalletProfiler = exports.scanForLendingMarkets = void 0;
const web3_js_1 = require("@solana/web3.js");
const config_1 = require("./config");
const logger_1 = require("./logger");
const monitor_1 = require("./monitor");
const launcher_1 = require("./launcher");
const torchsdk_1 = require("torchsdk");
const utils_1 = require("./utils");
// bot exports
var scanner_1 = require("./scanner");
Object.defineProperty(exports, "scanForLendingMarkets", { enumerable: true, get: function () { return scanner_1.scanForLendingMarkets; } });
var wallet_profiler_1 = require("./wallet-profiler");
Object.defineProperty(exports, "WalletProfiler", { enumerable: true, get: function () { return wallet_profiler_1.WalletProfiler; } });
var risk_scorer_1 = require("./risk-scorer");
Object.defineProperty(exports, "scoreLoan", { enumerable: true, get: function () { return risk_scorer_1.scoreLoan; } });
var liquidator_1 = require("./liquidator");
Object.defineProperty(exports, "Liquidator", { enumerable: true, get: function () { return liquidator_1.Liquidator; } });
var logger_2 = require("./logger");
Object.defineProperty(exports, "Logger", { enumerable: true, get: function () { return logger_2.Logger; } });
var launcher_2 = require("./launcher");
Object.defineProperty(exports, "launchDomainToken", { enumerable: true, get: function () { return launcher_2.launchDomainToken; } });
var domain_manager_1 = require("./domain-manager");
Object.defineProperty(exports, "checkTopHolder", { enumerable: true, get: function () { return domain_manager_1.checkTopHolder; } });
Object.defineProperty(exports, "updateLeases", { enumerable: true, get: function () { return domain_manager_1.updateLeases; } });
var ticker_1 = require("./ticker");
Object.defineProperty(exports, "generateTicker", { enumerable: true, get: function () { return ticker_1.generateTicker; } });
// scraper exports
var scanner_2 = require("./scraper/scanner");
Object.defineProperty(exports, "scanDomains", { enumerable: true, get: function () { return scanner_2.scanDomains; } });
var evaluator_1 = require("./scraper/evaluator");
Object.defineProperty(exports, "evaluateDomain", { enumerable: true, get: function () { return evaluator_1.evaluateDomain; } });
var ticker_2 = require("./scraper/ticker");
Object.defineProperty(exports, "generateScraperTicker", { enumerable: true, get: function () { return ticker_2.generateTicker; } });
const printUsage = () => {
    console.log(`torch-domain-bot — domain auction bot for torch.market

Usage:
  torch-domain-bot monitor    Start the lending monitor and auto-liquidator
  torch-domain-bot launch     Launch a domain token (interactive)
  torch-domain-bot info       Show token info for a mint
  torch-domain-bot help       Show this help message
`);
};
const main = async () => {
    const args = process.argv.slice(2);
    const command = args[0] ?? 'help';
    if (command === 'help' || command === '--help') {
        printUsage();
        return;
    }
    const config = (0, config_1.loadConfig)();
    const log = new logger_1.Logger('bot', config.logLevel);
    const connection = new web3_js_1.Connection(config.rpcUrl, 'confirmed');
    // startup banner
    console.log('=== torch domain auction bot ===');
    console.log(`agent wallet: ${config.walletKeypair.publicKey.toBase58()}`);
    console.log(`vault creator: ${config.vaultCreator}`);
    console.log(`scan interval: ${config.scanIntervalMs}ms`);
    console.log();
    // verify vault exists
    const vault = await (0, utils_1.withTimeout)((0, torchsdk_1.getVault)(connection, config.vaultCreator), 30000, 'getVault');
    if (!vault) {
        throw new Error(`vault not found for creator ${config.vaultCreator}`);
    }
    log.info(`vault found — authority=${vault.authority}`);
    // verify agent wallet is linked to vault
    const link = await (0, utils_1.withTimeout)((0, torchsdk_1.getVaultForWallet)(connection, config.walletKeypair.publicKey.toBase58()), 30000, 'getVaultForWallet');
    if (!link) {
        console.log();
        console.log('--- ACTION REQUIRED ---');
        console.log('agent wallet is NOT linked to the vault.');
        console.log('link it by running (from your authority wallet):');
        console.log();
        console.log(`  buildLinkWalletTransaction(connection, {`);
        console.log(`    authority: "<your-authority-pubkey>",`);
        console.log(`    vault_creator: "${config.vaultCreator}",`);
        console.log(`    wallet_to_link: "${config.walletKeypair.publicKey.toBase58()}"`);
        console.log(`  })`);
        console.log();
        console.log('then restart the bot.');
        console.log('-----------------------');
        process.exit(1);
    }
    log.info('agent wallet linked to vault — starting');
    log.info(`treasury: ${(0, utils_1.sol)(vault.sol_balance ?? 0)} SOL`);
    if (command === 'monitor') {
        await (0, monitor_1.runMonitor)(config);
    }
    else if (command === 'launch') {
        const domain = args[1];
        if (!domain) {
            console.error('Usage: torch-domain-bot launch <domain>');
            process.exit(1);
        }
        const result = await (0, launcher_1.launchDomainToken)(connection, config.walletKeypair, domain, undefined, log);
        console.log(`launched: mint=${result.mint}, ticker=${result.ticker}`);
    }
    else if (command === 'info') {
        const mint = args[1];
        if (!mint) {
            console.error('Usage: torch-domain-bot info <mint>');
            process.exit(1);
        }
        const detail = await (0, torchsdk_1.getToken)(connection, mint);
        console.log(JSON.stringify(detail, null, 2));
    }
    else {
        console.error(`unknown command: ${command}`);
        printUsage();
        process.exit(1);
    }
};
if (require.main === module) {
    main().catch((err) => {
        console.error('FATAL:', err);
        process.exit(1);
    });
}
//# sourceMappingURL=index.js.map

path: lib/kit/config.js
content: "use strict";
/**
 * config.ts — loads environment variables into a typed BotConfig.
 *
 * env vars:
 *   SOLANA_RPC_URL    — solana RPC endpoint (required, fallback: BOT_RPC_URL)
 *   VAULT_CREATOR     — vault creator pubkey (required)
 *   SOLANA_PRIVATE_KEY — disposable controller keypair, base58 or JSON byte array (optional)
 *   BOT_SCAN_INTERVAL_MS  — ms between scan cycles (default 60000, min 5000)
 *   BOT_SCORE_INTERVAL_MS — ms between scoring cycles (default 15000)
 *   BOT_MIN_PROFIT_LAMPORTS — minimum profit threshold (default 10000000)
 *   BOT_RISK_THRESHOLD — minimum risk score for liquidation (default 60)
 *   BOT_PRICE_HISTORY_DEPTH — price history depth (default 20)
 *   BOT_LOG_LEVEL     — debug | info | warn | error (default info)
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.loadConfig = exports.loadKeypair = void 0;
const web3_js_1 = require("@solana/web3.js");
const utils_1 = require("./utils");
const LOG_LEVELS = ['debug', 'info', 'warn', 'error'];
const envOrDefault = (key, fallback) => process.env[key] ?? fallback;
/**
 * Load or generate an agent keypair.
 * If SOLANA_PRIVATE_KEY is set, decode it (JSON byte array or base58).
 * Otherwise, generate a fresh disposable keypair.
 */
const loadKeypair = () => {
    const privateKey = process.env.SOLANA_PRIVATE_KEY ?? null;
    if (privateKey) {
        try {
            const parsed = JSON.parse(privateKey);
            if (Array.isArray(parsed)) {
                return { keypair: web3_js_1.Keypair.fromSecretKey(Uint8Array.from(parsed)), generated: false };
            }
            throw new Error('SOLANA_PRIVATE_KEY JSON must be a byte array');
        }
        catch (e) {
            if (e.message?.includes('byte array'))
                throw e;
            // not JSON — try base58
            return { keypair: web3_js_1.Keypair.fromSecretKey((0, utils_1.decodeBase58)(privateKey)), generated: false };
        }
    }
    return { keypair: web3_js_1.Keypair.generate(), generated: true };
};
exports.loadKeypair = loadKeypair;
const loadConfig = (walletOverride) => {
    const rpcUrl = process.env.SOLANA_RPC_URL ?? process.env.BOT_RPC_URL;
    if (!rpcUrl)
        throw new Error('SOLANA_RPC_URL env var is required (fallback: BOT_RPC_URL)');
    const vaultCreator = process.env.VAULT_CREATOR;
    if (!vaultCreator)
        throw new Error('VAULT_CREATOR env var is required (vault creator pubkey)');
    const { keypair } = walletOverride ? { keypair: walletOverride } : (0, exports.loadKeypair)();
    const scanIntervalMs = Number(envOrDefault('BOT_SCAN_INTERVAL_MS', '60000'));
    if (isNaN(scanIntervalMs) || scanIntervalMs < 5000) {
        throw new Error('BOT_SCAN_INTERVAL_MS must be a number >= 5000');
    }
    const logLevel = envOrDefault('BOT_LOG_LEVEL', 'info');
    if (!LOG_LEVELS.includes(logLevel)) {
        throw new Error(`BOT_LOG_LEVEL must be one of: ${LOG_LEVELS.join(', ')}`);
    }
    return {
        rpcUrl,
        walletKeypair: keypair,
        vaultCreator,
        scanIntervalMs,
        scoreIntervalMs: Number(envOrDefault('BOT_SCORE_INTERVAL_MS', '15000')),
        minProfitLamports: Number(envOrDefault('BOT_MIN_PROFIT_LAMPORTS', '10000000')),
        riskThreshold: Number(envOrDefault('BOT_RISK_THRESHOLD', '60')),
        priceHistoryDepth: Number(envOrDefault('BOT_PRICE_HISTORY_DEPTH', '20')),
        logLevel,
    };
};
exports.loadConfig = loadConfig;
//# sourceMappingURL=config.js.map

path: lib/kit/monitor.js
content: "use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.runMonitor = void 0;
const web3_js_1 = require("@solana/web3.js");
const torchsdk_1 = require("torchsdk");
const scanner_1 = require("./scanner");
const wallet_profiler_1 = require("./wallet-profiler");
const risk_scorer_1 = require("./risk-scorer");
const liquidator_1 = require("./liquidator");
const domain_manager_1 = require("./domain-manager");
const logger_1 = require("./logger");
const utils_1 = require("./utils");
const runMonitor = async (config, domainTokens = []) => {
    const log = new logger_1.Logger('monitor', config.logLevel);
    const connection = new web3_js_1.Connection(config.rpcUrl, 'confirmed');
    const profiler = new wallet_profiler_1.WalletProfiler(log.child('profiler'));
    const liquidator = new liquidator_1.Liquidator(config, log.child('liquidator'));
    let tokens = new Map();
    let leases = [];
    log.info('domain auction bot monitor starting');
    while (true) {
        try {
            // scan for lending markets
            tokens = await (0, scanner_1.scanForLendingMarkets)(connection, tokens, config.priceHistoryDepth, log.child('scanner'));
            // update domain leases (check top holders, expire/rotate)
            if (domainTokens.length > 0) {
                leases = await (0, domain_manager_1.updateLeases)(connection, domainTokens, leases, log.child('leases'));
            }
            // score and attempt liquidation for each token with borrowers
            for (const [mint, token] of tokens) {
                if (!token.lendingInfo.active_loans || token.lendingInfo.active_loans === 0)
                    continue;
                // update price
                try {
                    const detail = await (0, utils_1.withTimeout)((0, torchsdk_1.getToken)(connection, mint), 30000, 'getToken');
                    const newPrice = detail.price_sol / torchsdk_1.LAMPORTS_PER_SOL;
                    token.priceHistory.push(newPrice);
                    if (token.priceHistory.length > config.priceHistoryDepth) {
                        token.priceHistory.shift();
                    }
                    token.priceSol = newPrice;
                }
                catch {
                    log.debug(`price update failed for ${token.symbol}`);
                }
                // score all active borrowers via bulk scan
                try {
                    const { positions } = await (0, utils_1.withTimeout)((0, torchsdk_1.getAllLoanPositions)(connection, mint), 30000, 'getAllLoanPositions');
                    for (const pos of positions) {
                        try {
                            const profile = await profiler.profile(connection, pos.borrower, mint);
                            const scored = (0, risk_scorer_1.scoreLoan)(token, pos.borrower, pos, profile);
                            log.info(`${token.symbol} — ${pos.borrower.slice(0, 8)}... risk=${scored.riskScore} health=${pos.health}`);
                            await liquidator.tryLiquidate(connection, scored);
                        }
                        catch (err) {
                            log.debug(`error scoring ${pos.borrower.slice(0, 8)}...`, err);
                        }
                    }
                }
                catch (err) {
                    log.debug(`bulk loan scan failed for ${token.symbol}`, err);
                }
            }
        }
        catch (err) {
            log.error('monitor tick failed', err);
        }
        await (0, utils_1.sleep)(config.scoreIntervalMs);
    }
};
exports.runMonitor = runMonitor;
//# sourceMappingURL=monitor.js.map

path: lib/kit/risk-scorer.js
content: "use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.scoreLoan = void 0;
const web3_js_1 = require("@solana/web3.js");
const WEIGHTS = {
    ltvProximity: 0.35,
    priceMomentum: 0.25,
    walletRisk: 0.2,
    interestBurden: 0.2,
};
const computeLtvProximity = (position, token) => {
    const currentLtv = position.current_ltv_bps ?? 0;
    const threshold = token.lendingInfo.liquidation_threshold_bps;
    if (threshold === 0)
        return 0;
    // how close is current LTV to liquidation threshold (0-100)
    const proximity = (currentLtv / threshold) * 100;
    return Math.min(proximity, 100);
};
const computePriceMomentum = (token) => {
    const history = token.priceHistory;
    if (history.length < 2)
        return 50; // neutral
    const recent = history[history.length - 1];
    const older = history[0];
    if (older === 0)
        return 50;
    // price drop = higher risk
    const change = (recent - older) / older;
    // map -50% to 100 risk, +50% to 0 risk
    const momentum = 50 - change * 100;
    return Math.min(Math.max(momentum, 0), 100);
};
const computeInterestBurden = (position) => {
    if (position.borrowed_amount === 0)
        return 0;
    const interestRatio = position.accrued_interest / position.borrowed_amount;
    // 10% interest = 50 risk, 20% = 100 risk
    return Math.min(interestRatio * 500, 100);
};
const estimateProfit = (position, token) => {
    const bonus = token.lendingInfo.liquidation_bonus_bps / 10000;
    // use SDK value if available, otherwise estimate from collateral amount * token price
    const collateralValue = position.collateral_value_sol ?? position.collateral_amount * token.priceSol;
    const profitSol = collateralValue * bonus;
    return Math.floor(profitSol * web3_js_1.LAMPORTS_PER_SOL);
};
/**
 * Score a loan position for liquidation risk and profitability.
 * Returns a ScoredLoan with risk score (0-100) and factor breakdown.
 */
const scoreLoan = (token, borrower, position, profile) => {
    const factors = {
        ltvProximity: Math.round(computeLtvProximity(position, token)),
        priceMomentum: Math.round(computePriceMomentum(token)),
        walletRisk: profile.riskScore,
        interestBurden: Math.round(computeInterestBurden(position)),
    };
    const riskScore = Math.round(factors.ltvProximity * WEIGHTS.ltvProximity +
        factors.priceMomentum * WEIGHTS.priceMomentum +
        factors.walletRisk * WEIGHTS.walletRisk +
        factors.interestBurden * WEIGHTS.interestBurden);
    return {
        mint: token.mint,
        tokenName: token.name,
        borrower,
        position,
        walletProfile: profile,
        riskScore: Math.min(Math.max(riskScore, 0), 100),
        factors,
        estimatedProfitLamports: estimateProfit(position, token),
        lastScored: Date.now(),
    };
};
exports.scoreLoan = scoreLoan;
//# sourceMappingURL=risk-scorer.js.map

path: lib/kit/ticker.js
content: "use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.generateTicker = void 0;
/**
 * Generate a sensible token ticker (3-6 chars, uppercase) from a domain name.
 */
const generateTicker = (domain) => {
    const base = domain.split('.')[0];
    const parts = base.split(/[-.]/).filter((p) => p.length > 0);
    let ticker;
    if (parts.length === 0) {
        ticker = 'TKN';
    }
    else if (parts.length === 1) {
        const word = parts[0];
        if (word.length <= 6) {
            ticker = word;
        }
        else {
            ticker = word.slice(0, 5);
        }
    }
    else {
        const first = parts[0];
        if (first.length <= 4) {
            ticker = first;
        }
        else {
            ticker = parts.map((p) => p[0]).join('');
        }
    }
    if (ticker.length < 3) {
        ticker = ticker.padEnd(3, 'X');
    }
    else if (ticker.length > 6) {
        ticker = ticker.slice(0, 6);
    }
    return ticker.toUpperCase();
};
exports.generateTicker = generateTicker;
//# sourceMappingURL=ticker.js.map

path: lib/kit/liquidator.js
content: "use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Liquidator = void 0;
const torchsdk_1 = require("torchsdk");
const utils_1 = require("./utils");
class Liquidator {
    constructor(config, log) {
        /**
         * Attempt to liquidate a scored loan position.
         * Returns null if the position should be skipped (healthy, not profitable, below threshold).
         */
        this.tryLiquidate = async (connection, scored) => {
            // skip healthy positions
            if (scored.position.health === 'healthy') {
                this.log.debug(`skipping ${scored.borrower.slice(0, 8)}... — position is healthy`);
                return null;
            }
            // skip if risk score below threshold
            if (scored.riskScore < this.config.riskThreshold) {
                this.log.debug(`skipping ${scored.borrower.slice(0, 8)}... — risk ${scored.riskScore} < threshold ${this.config.riskThreshold}`);
                return null;
            }
            // skip if not profitable enough
            if (scored.estimatedProfitLamports < this.config.minProfitLamports) {
                this.log.debug(`skipping ${scored.borrower.slice(0, 8)}... — profit ${scored.estimatedProfitLamports} < min ${this.config.minProfitLamports}`);
                return null;
            }
            // only liquidate positions that are actually liquidatable
            if (scored.position.health !== 'liquidatable') {
                this.log.info(`skipping ${scored.borrower.slice(0, 8)}... — health=${scored.position.health} (not liquidatable)`);
                return null;
            }
            this.log.info(`liquidating ${scored.borrower.slice(0, 8)}... on ${scored.tokenName} — risk=${scored.riskScore}, profit=${scored.estimatedProfitLamports}`);
            try {
                const result = await (0, utils_1.withTimeout)((0, torchsdk_1.buildLiquidateTransaction)(connection, {
                    mint: scored.mint,
                    liquidator: this.config.walletKeypair.publicKey.toBase58(),
                    borrower: scored.borrower,
                    vault: this.config.vaultCreator,
                }), 30000, 'buildLiquidateTransaction');
                result.transaction.partialSign(this.config.walletKeypair);
                const sig = await connection.sendRawTransaction(result.transaction.serialize(), {
                    skipPreflight: false,
                    preflightCommitment: 'confirmed',
                });
                await connection.confirmTransaction(sig, 'confirmed');
                this.log.info(`liquidation success: sig=${sig.slice(0, 8)}...`);
                return {
                    mint: scored.mint,
                    borrower: scored.borrower,
                    signature: sig,
                    profitLamports: scored.estimatedProfitLamports,
                    timestamp: Date.now(),
                    confirmed: true,
                };
            }
            catch (err) {
                this.log.error(`liquidation failed for ${scored.borrower.slice(0, 8)}...`, err);
                return null;
            }
        };
        this.config = config;
        this.log = log;
    }
}
exports.Liquidator = Liquidator;
//# sourceMappingURL=liquidator.js.map

path: lib/kit/utils.js
content: "use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.decodeBase58 = exports.withTimeout = exports.clamp = exports.bpsToPercent = exports.sol = exports.sleep = void 0;
const web3_js_1 = require("@solana/web3.js");
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
exports.sleep = sleep;
const sol = (lamports) => (lamports / web3_js_1.LAMPORTS_PER_SOL).toFixed(4);
exports.sol = sol;
const bpsToPercent = (bps) => (bps / 100).toFixed(2) + '%';
exports.bpsToPercent = bpsToPercent;
const clamp = (value, min, max) => Math.min(Math.max(value, min), max);
exports.clamp = clamp;
const withTimeout = (promise, ms, label) => {
    return new Promise((resolve, reject) => {
        const timer = setTimeout(() => reject(new Error(`timeout after ${ms}ms: ${label}`)), ms);
        promise.then((val) => { clearTimeout(timer); resolve(val); }, (err) => { clearTimeout(timer); reject(err); });
    });
};
exports.withTimeout = withTimeout;
// base58 decoder — avoids ESM-only bs58 dependency
const B58 = [REDACTED_SECRET];
const decodeBase58 = (s) => {
    const result = [];
    for (let i = 0; i < s.length; i++) {
        let carry = B58.indexOf(s[i]);
        if (carry < 0)
            throw new Error(`invalid base58 character: ${s[i]}`);
        for (let j = 0; j < result.length; j++) {
            carry += result[j] * 58;
            result[j] = carry & 0xff;
            carry >>= 8;
        }
        while (carry > 0) {
            result.push(carry & 0xff);
            carry >>= 8;
        }
    }
    for (let i = 0; i < s.length && s[i] === '1'; i++) {
        result.push(0);
    }
    return new Uint8Array(result.reverse());
};
exports.decodeBase58 = decodeBase58;
//# sourceMappingURL=utils.js.map

path: lib/kit/launcher.js
content: "use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.launchDomainToken = void 0;
const torchsdk_1 = require("torchsdk");
const ticker_1 = require("./ticker");
const utils_1 = require("./utils");
const launchDomainToken = async (connection, wallet, domain, ticker, log) => {
    const symbol = ticker ?? (0, ticker_1.generateTicker)(domain);
    const name = domain;
    const metadataUri = `https://${domain}`;
    log?.info(`launching token for ${domain} — symbol=${symbol}`);
    const result = await (0, utils_1.withTimeout)((0, torchsdk_1.buildCreateTokenTransaction)(connection, {
        creator: wallet.publicKey.toBase58(),
        name,
        symbol,
        metadata_uri: metadataUri,
    }), 30000, 'buildCreateTokenTransaction');
    // sign and send
    result.transaction.partialSign(wallet);
    const sig = await connection.sendRawTransaction(result.transaction.serialize(), {
        skipPreflight: false,
        preflightCommitment: 'confirmed',
    });
    await connection.confirmTransaction(sig, 'confirmed');
    const mint = result.mint.toBase58();
    log?.info(`launched: mint=${mint.slice(0, 8)}... sig=${sig.slice(0, 8)}...`);
    return { domain, mint, signature: sig, ticker: symbol };
};
exports.launchDomainToken = launchDomainToken;
//# sourceMappingURL=launcher.js.map

path: lib/kit/domain-manager.js
content: "use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.updateLeases = exports.checkTopHolder = void 0;
const torchsdk_1 = require("torchsdk");
const utils_1 = require("./utils");
const DEFAULT_LEASE_DURATION_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
const checkTopHolder = async (connection, mint) => {
    const result = await (0, utils_1.withTimeout)((0, torchsdk_1.getHolders)(connection, mint, 1), 30000, 'getHolders');
    if (result.holders.length === 0)
        return null;
    return result.holders[0].address;
};
exports.checkTopHolder = checkTopHolder;
const updateLeases = async (connection, domainTokens, leases, log) => {
    const now = Date.now();
    const updated = [...leases];
    // expire old leases
    for (const lease of updated) {
        if (lease.active && lease.expiresAt <= now) {
            lease.active = false;
            log?.info(`lease expired: ${lease.domain} — lessee=${lease.lessee.slice(0, 8)}...`);
        }
    }
    // check each domain token for new top holder
    for (const dt of domainTokens) {
        const topHolder = await (0, exports.checkTopHolder)(connection, dt.mint);
        if (!topHolder)
            continue;
        const activeLease = updated.find((l) => l.mint === dt.mint && l.active);
        if (activeLease) {
            // if top holder changed, expire current and create new
            if (activeLease.lessee !== topHolder) {
                activeLease.active = false;
                log?.info(`top holder changed for ${dt.domain}: ${activeLease.lessee.slice(0, 8)}... → ${topHolder.slice(0, 8)}...`);
                updated.push({
                    domain: dt.domain,
                    mint: dt.mint,
                    lessee: topHolder,
                    startedAt: now,
                    expiresAt: now + DEFAULT_LEASE_DURATION_MS,
                    active: true,
                });
            }
        }
        else {
            // no active lease — create one for top holder
            log?.info(`new lease: ${dt.domain} → ${topHolder.slice(0, 8)}...`);
            updated.push({
                domain: dt.domain,
                mint: dt.mint,
                lessee: topHolder,
                startedAt: now,
                expiresAt: now + DEFAULT_LEASE_DURATION_MS,
                active: true,
            });
        }
    }
    return updated;
};
exports.updateLeases = updateLeases;
//# sourceMappingURL=domain-manager.js.map

path: lib/kit/scraper/types.js
content: "use strict";
Object.defineProperty(exports, "__esModule", { value: true });
//# sourceMappingURL=types.js.map

path: lib/kit/scraper/scanner.js
content: "use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.scanDomains = void 0;
const evaluator_1 = require("./evaluator");
const logger_1 = require("../logger");
const scanDomains = async (config) => {
    const log = new logger_1.Logger('scanner', config.logLevel);
    const allListings = [];
    for (const provider of config.providers) {
        log.info(`scanning provider: ${provider.name}`);
        try {
            const listings = await provider.scan({
                maxPrice: config.maxPriceUsd,
                limit: 50,
            });
            log.info(`${provider.name} returned ${listings.length} listings`);
            allListings.push(...listings);
        }
        catch (err) {
            log.error(`provider ${provider.name} failed`, err);
        }
    }
    log.info(`total listings: ${allListings.length}`);
    // score all listings
    const scored = allListings.map(evaluator_1.evaluateDomain);
    // filter by minimum score and sort descending
    const filtered = scored
        .filter((d) => d.score >= config.minScore)
        .sort((a, b) => b.score - a.score);
    log.info(`${filtered.length} domains passed minimum score of ${config.minScore}`);
    return filtered;
};
exports.scanDomains = scanDomains;
//# sourceMappingURL=scanner.js.map

path: lib/kit/scraper/index.js
content: #!/usr/bin/env node
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Logger = exports.generateTicker = exports.evaluateDomain = exports.scanDomains = void 0;
const config_1 = require("./config");
const scanner_1 = require("./scanner");
const expired_domains_1 = require("./providers/expired-domains");
const availability_1 = require("./providers/availability");
const logger_1 = require("../logger");
var scanner_2 = require("./scanner");
Object.defineProperty(exports, "scanDomains", { enumerable: true, get: function () { return scanner_2.scanDomains; } });
var evaluator_1 = require("./evaluator");
Object.defineProperty(exports, "evaluateDomain", { enumerable: true, get: function () { return evaluator_1.evaluateDomain; } });
var ticker_1 = require("./ticker");
Object.defineProperty(exports, "generateTicker", { enumerable: true, get: function () { return ticker_1.generateTicker; } });
var logger_2 = require("../logger");
Object.defineProperty(exports, "Logger", { enumerable: true, get: function () { return logger_2.Logger; } });
const main = async () => {
    const config = (0, config_1.loadConfig)([expired_domains_1.expiredDomainsProvider, availability_1.availabilityProvider]);
    const log = new logger_1.Logger('scraper', config.logLevel);
    log.info('torch domain scraper starting');
    log.info(`config: maxPrice=$${config.maxPriceUsd}, minScore=${config.minScore}`);
    const results = await (0, scanner_1.scanDomains)(config);
    if (results.length === 0) {
        log.info('no domains found matching criteria');
        return;
    }
    log.info(`found ${results.length} candidate domains:\n`);
    for (const d of results) {
        console.log(`  [${d.score}] ${d.listing.name}.${d.listing.tld} — $${d.listing.price} — ticker: ${d.ticker} — ${d.reasoning}`);
    }
};
// only run CLI when executed directly
if (require.main === module) {
    main().catch((err) => {
        console.error('FATAL:', err);
        process.exit(1);
    });
}
//# sourceMappingURL=index.js.map

path: lib/kit/scraper/config.js
content: "use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.loadConfig = void 0;
const envOrDefault = (key, fallback) => process.env[key] ?? fallback;
const loadConfig = (providers = []) => ({
    maxPriceUsd: Number(envOrDefault('SCRAPER_MAX_PRICE_USD', '50')),
    minScore: Number(envOrDefault('SCRAPER_MIN_SCORE', '40')),
    scanIntervalMs: Number(envOrDefault('SCRAPER_SCAN_INTERVAL_MS', '300000')),
    providers,
    logLevel: envOrDefault('SCRAPER_LOG_LEVEL', 'info'),
});
exports.loadConfig = loadConfig;
//# sourceMappingURL=config.js.map

path: lib/kit/scraper/ticker.js
content: "use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.generateTicker = void 0;
/**
 * Generate a sensible token ticker (3-6 chars, uppercase) from a domain name.
 *
 * Algorithm:
 * 1. Strip TLD
 * 2. Split on hyphens/dots
 * 3. Single short word (<=6) -> uppercase it
 * 4. Single long word -> meaningful prefix (3-6 chars)
 * 5. Multi-word -> first word if short, else acronym
 */
const generateTicker = (domain) => {
    // strip TLD
    const base = domain.split('.')[0];
    // split on hyphens and dots
    const parts = base.split(/[-.]/).filter((p) => p.length > 0);
    let ticker;
    if (parts.length === 0) {
        ticker = 'TKN';
    }
    else if (parts.length === 1) {
        const word = parts[0];
        if (word.length <= 6) {
            ticker = word;
        }
        else {
            // take meaningful prefix: up to 5 chars, try to break at consonant cluster
            ticker = word.slice(0, 5);
        }
    }
    else {
        const first = parts[0];
        if (first.length <= 4) {
            ticker = first;
        }
        else {
            // acronym from first letters
            ticker = parts.map((p) => p[0]).join('');
        }
    }
    // enforce 3-6 chars
    if (ticker.length < 3) {
        ticker = ticker.padEnd(3, 'X');
    }
    else if (ticker.length > 6) {
        ticker = ticker.slice(0, 6);
    }
    return ticker.toUpperCase();
};
exports.generateTicker = generateTicker;
//# sourceMappingURL=ticker.js.map

path: lib/kit/scraper/evaluator.js
content: "use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.evaluateDomain = void 0;
const ticker_1 = require("./ticker");
const TLD_SCORES = {
    com: 30,
    io: 22,
    ai: 25,
    dev: 18,
    net: 15,
    org: 12,
    xyz: 10,
    co: 16,
    app: 14,
};
const VALUABLE_KEYWORDS = ['ai', 'crypto', 'defi', 'nft', 'sol', 'web3', 'chain', 'swap', 'pay'];
const scoreDomainLength = (name) => {
    if (name.length <= 3)
        return 25;
    if (name.length <= 5)
        return 20;
    if (name.length <= 8)
        return 15;
    if (name.length <= 12)
        return 8;
    return 3;
};
const scoreTld = (tld) => TLD_SCORES[tld.toLowerCase()] ?? 5;
const scoreKeywords = (name) => {
    const lower = name.toLowerCase();
    const matches = VALUABLE_KEYWORDS.filter((kw) => lower.includes(kw));
    return Math.min(matches.length * 8, 20);
};
const scoreCleanName = (name) => {
    const hasHyphens = name.includes('-');
    const hasNumbers = /\d/.test(name);
    if (!hasHyphens && !hasNumbers)
        return 15;
    if (!hasHyphens)
        return 8;
    return 3;
};
const scorePriceValue = (price) => {
    if (price <= 5)
        return 10;
    if (price <= 15)
        return 7;
    if (price <= 30)
        return 4;
    return 1;
};
const evaluateDomain = (listing) => {
    const lengthScore = scoreDomainLength(listing.name);
    const tldScore = scoreTld(listing.tld);
    const kwScore = scoreKeywords(listing.name);
    const cleanScore = scoreCleanName(listing.name);
    const priceScore = scorePriceValue(listing.price);
    const score = Math.min(lengthScore + tldScore + kwScore + cleanScore + priceScore, 100);
    const reasons = [];
    if (lengthScore >= 20)
        reasons.push('short name');
    if (tldScore >= 20)
        reasons.push(`premium TLD (.${listing.tld})`);
    if (kwScore > 0)
        reasons.push('valuable keywords');
    if (cleanScore >= 15)
        reasons.push('clean (no hyphens/numbers)');
    if (priceScore >= 7)
        reasons.push('low price');
    return {
        listing,
        score,
        ticker: (0, ticker_1.generateTicker)(`${listing.name}.${listing.tld}`),
        reasoning: reasons.length > 0 ? reasons.join(', ') : 'average domain',
    };
};
exports.evaluateDomain = evaluateDomain;
//# sourceMappingURL=evaluator.js.map

path: lib/kit/scraper/utils.js
content: "use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.fqdn = exports.formatUsd = exports.sleep = void 0;
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
exports.sleep = sleep;
const formatUsd = (cents) => `$${(cents / 100).toFixed(2)}`;
exports.formatUsd = formatUsd;
const fqdn = (name, tld) => `${name}.${tld}`;
exports.fqdn = fqdn;
//# sourceMappingURL=utils.js.map

path: lib/kit/scraper/providers/expired-domains.js
content: "use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.expiredDomainsProvider = void 0;
/**
 * Expired domains provider — fetches publicly available expired/expiring domain feeds.
 * v1: returns simulated listings from known expired domain patterns.
 * Future: integrate with real expired domain feeds (ExpiredDomains.net, NameJet, etc.)
 */
exports.expiredDomainsProvider = {
    name: 'expired-domains',
    scan: async (opts) => {
        const listings = [];
        try {
            // v1: fetch from a public expired domain list endpoint
            // this is a placeholder — real implementation would hit an actual API
            // for now, we simulate the interface so the pipeline works end-to-end
            const response = await fetch('https://www.expireddomains.net/domain-name-search/?fwhois=22&ftlds[]=1', {
                headers: { 'User-Agent': 'TorchDomainBot/1.0' },
                signal: AbortSignal.timeout(10000),
            });
            if (!response.ok) {
                return listings;
            }
            const html = await response.text();
            // parse domain names from the response (basic extraction)
            const domainRegex = /class="field_name"[^>]*><a[^>]*>([a-z0-9-]+)\.([a-z]{2,6})<\/a>/gi;
            let match;
            while ((match = domainRegex.exec(html)) !== null && listings.length < opts.limit) {
                const name = match[1].toLowerCase();
                const tld = match[2].toLowerCase();
                listings.push({
                    name,
                    tld,
                    price: 10, // expired domains typically cost registration fee
                    currency: 'USD',
                    provider: 'expired-domains',
                });
            }
        }
        catch {
            // network errors are expected — provider is best-effort
        }
        return listings.filter((l) => l.price <= opts.maxPrice).slice(0, opts.limit);
    },
};
//# sourceMappingURL=expired-domains.js.map

path: lib/kit/scraper/providers/availability.js
content: "use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.availabilityProvider = void 0;
/**
 * Availability provider — checks domain availability via RDAP protocol.
 * RDAP is the replacement for WHOIS and doesn't require API keys.
 * v1: checks a predefined list of interesting short domains.
 */
const CANDIDATE_NAMES = [
    'aibot',
    'solpay',
    'defiai',
    'web3go',
    'chainx',
    'nftlab',
    'cryptox',
    'swapai',
    'payfi',
    'tokenv',
];
const CANDIDATE_TLDS = ['com', 'io', 'ai', 'xyz', 'dev'];
const checkRdap = async (name, tld) => {
    try {
        const rdapUrl = `https://rdap.org/domain/${name}.${tld}`;
        const response = await fetch(rdapUrl, {
            method: 'HEAD',
            signal: AbortSignal.timeout(5000),
        });
        // 404 means domain is available, 200 means registered
        return response.status === 404;
    }
    catch {
        return false;
    }
};
exports.availabilityProvider = {
    name: 'availability',
    scan: async (opts) => {
        const listings = [];
        for (const name of CANDIDATE_NAMES) {
            if (listings.length >= opts.limit)
                break;
            for (const tld of CANDIDATE_TLDS) {
                if (listings.length >= opts.limit)
                    break;
                const available = await checkRdap(name, tld);
                if (available) {
                    // estimate registration cost by TLD
                    const price = tld === 'com' ? 12 : tld === 'ai' ? 25 : tld === 'io' ? 15 : 8;
                    if (price <= opts.maxPrice) {
                        listings.push({
                            name,
                            tld,
                            price,
                            currency: 'USD',
                            provider: 'availability',
                        });
                    }
                }
            }
        }
        return listings;
    },
};
//# sourceMappingURL=availability.js.map

path: lib/torchsdk/constants.js
content: "use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.TOKEN_MULTIPLIER = exports.LAMPORTS_PER_SOL = exports.BLACKLISTED_MINTS = exports.LEGACY_MINTS = exports.TOKEN_DECIMALS = exports.TOTAL_SUPPLY = exports.TREASURY_LOCK_SEED = exports.VAULT_WALLET_LINK_SEED = exports.TORCH_VAULT_SEED = exports.COLLATERAL_VAULT_SEED = exports.LOAN_SEED = exports.STAR_RECORD_SEED = exports.USER_STATS_SEED = exports.PROTOCOL_TREASURY_SEED = exports.VOTE_SEED = exports.USER_POSITION_SEED = exports.TREASURY_SEED = exports.BONDING_CURVE_SEED = exports.GLOBAL_CONFIG_SEED = exports.TOKEN_2022_PROGRAM_ID = exports.MEMO_PROGRAM_ID = exports.RAYDIUM_FEE_RECEIVER = exports.getRaydiumFeeReceiver = exports.RAYDIUM_AMM_CONFIG = exports.getRaydiumAmmConfig = exports.WSOL_MINT = exports.RAYDIUM_CPMM_PROGRAM = exports.getRaydiumCpmmProgram = exports.PROGRAM_ID = void 0;
const web3_js_1 = require("@solana/web3.js");
// Program ID - Mainnet/Devnet (deployed program)
exports.PROGRAM_ID = new web3_js_1.PublicKey([REDACTED_SECRET]);
// Network detection: evaluated at call time so env can be set dynamically.
// Checks globalThis.__TORCH_NETWORK__ first (for browser runtime switching),
// then falls back to process.env.TORCH_NETWORK (for Node.js / build-time).
const isDevnet = () => globalThis.__TORCH_NETWORK__ === 'devnet' ||
    (typeof process !== 'undefined' && process.env?.TORCH_NETWORK === 'devnet');
// Raydium CPMM Program (different on mainnet vs devnet)
const getRaydiumCpmmProgram = () => new web3_js_1.PublicKey(isDevnet()
    ? [REDACTED_SECRET]
    : [REDACTED_SECRET]);
exports.getRaydiumCpmmProgram = getRaydiumCpmmProgram;
/** @deprecated Use getRaydiumCpmmProgram() for dynamic network support */
exports.RAYDIUM_CPMM_PROGRAM = (0, exports.getRaydiumCpmmProgram)();
// WSOL Mint (same on all networks)
exports.WSOL_MINT = new web3_js_1.PublicKey([REDACTED_SECRET]);
// Raydium AMM Config (different on mainnet vs devnet)
const getRaydiumAmmConfig = () => new web3_js_1.PublicKey(isDevnet()
    ? [REDACTED_SECRET]
    : [REDACTED_SECRET]);
exports.getRaydiumAmmConfig = getRaydiumAmmConfig;
/** @deprecated Use getRaydiumAmmConfig() for dynamic network support */
exports.RAYDIUM_AMM_CONFIG = (0, exports.getRaydiumAmmConfig)();
// Raydium Fee Receiver (different on mainnet vs devnet)
const getRaydiumFeeReceiver = () => new web3_js_1.PublicKey(isDevnet()
    ? [REDACTED_SECRET]
    : [REDACTED_SECRET]);
exports.getRaydiumFeeReceiver = getRaydiumFeeReceiver;
/** @deprecated Use getRaydiumFeeReceiver() for dynamic network support */
exports.RAYDIUM_FEE_RECEIVER = (0, exports.getRaydiumFeeReceiver)();
// SPL Memo Program
exports.MEMO_PROGRAM_ID = new web3_js_1.PublicKey([REDACTED_SECRET]);
// Token-2022 Program (for Token Extensions)
exports.TOKEN_2022_PROGRAM_ID = new web3_js_1.PublicKey([REDACTED_SECRET]);
// PDA Seeds (must match the Rust program)
exports.GLOBAL_CONFIG_SEED = 'global_config';
exports.BONDING_CURVE_SEED = 'bonding_curve';
// [V13] BURN_VAULT_SEED removed - treasury's ATA now holds vote vault tokens
exports.TREASURY_SEED = 'treasury';
exports.USER_POSITION_SEED = 'user_position';
exports.VOTE_SEED = 'vote';
exports.PROTOCOL_TREASURY_SEED = 'protocol_treasury_v11'; // V11: Protocol fee treasury
exports.USER_STATS_SEED = 'user_stats';
exports.STAR_RECORD_SEED = 'star_record';
exports.LOAN_SEED = 'loan';
exports.COLLATERAL_VAULT_SEED = 'collateral_vault';
exports.TORCH_VAULT_SEED = 'torch_vault'; // V2.0: Vault PDA
exports.VAULT_WALLET_LINK_SEED = 'vault_wallet'; // V2.0: Wallet link PDA
exports.TREASURY_LOCK_SEED = 'treasury_lock'; // V27: Treasury lock PDA
// Token constants (must match the Rust program)
exports.TOTAL_SUPPLY = BigInt('1000000000000000'); // 1B with 6 decimals
exports.TOKEN_DECIMALS = 6;
// Legacy tokens (old test tokens, failed launches, etc.) — shown greyed out in UI
exports.LEGACY_MINTS = [
    [REDACTED_SECRET], // Legacy SPL test token (pre-prod-beta)
    [REDACTED_SECRET], // Legacy SPL devnet test token
    [REDACTED_SECRET], // Legacy SPL devnet test token
    [REDACTED_SECRET], // Legacy SPL devnet test token
    [REDACTED_SECRET], // Pre-V13 devnet test token
    [REDACTED_SECRET], // Pre-V13 devnet test token
    [REDACTED_SECRET], // Pre-V13 mainnet test token
    [REDACTED_SECRET], // Pre-V13 mainnet test token
    [REDACTED_SECRET], // Pre-V13 mainnet test token
    [REDACTED_SECRET], // Duplicate test token (keep Second Torch only)
    [REDACTED_SECRET], // Failed mainnet token (relaunched)
    [REDACTED_SECRET], // Relaunched token
    [REDACTED_SECRET],
    [REDACTED_SECRET],
    [REDACTED_SECRET],
    [REDACTED_SECRET],
    [REDACTED_SECRET],
    [REDACTED_SECRET],
    [REDACTED_SECRET],
    [REDACTED_SECRET],
    [REDACTED_SECRET],
    [REDACTED_SECRET],
    [REDACTED_SECRET],
    [REDACTED_SECRET],
    [REDACTED_SECRET],
    [REDACTED_SECRET],
    [REDACTED_SECRET],
    [REDACTED_SECRET],
    [REDACTED_SECRET],
    [REDACTED_SECRET],
    [REDACTED_SECRET],
    [REDACTED_SECRET],
    [REDACTED_SECRET]
];
// Blacklisted tokens — completely hidden, never returned by the SDK
exports.BLACKLISTED_MINTS = [
    [REDACTED_SECRET],
    [REDACTED_SECRET],
];
// Formatting helpers
exports.LAMPORTS_PER_SOL = 1000000000;
exports.TOKEN_MULTIPLIER = Math.pow(10, exports.TOKEN_DECIMALS);
//# sourceMappingURL=constants.js.map

path: lib/torchsdk/tokens.js
content: "use strict";
/**
 * Token data fetching
 *
 * Read-only functions for querying token state from Solana.
 */
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.fetchTokenRaw = exports.getVaultWalletLink = exports.getVaultForWallet = exports.getVault = exports.getAllLoanPositions = exports.getLoanPosition = exports.getLendingInfo = exports.getMessages = exports.getHolders = exports.getToken = exports.getTokenMetadata = exports.getTokens = void 0;
const web3_js_1 = require("@solana/web3.js");
const anchor_1 = require("@coral-xyz/anchor");
const spl_token_1 = require("@solana/spl-token");
const program_1 = require("./program");
const constants_1 = require("./constants");
const gateway_1 = require("./gateway");
const torch_market_json_1 = __importDefault(require("./torch_market.json"));
const getTokenStatus = (bc) => {
    if (bc.migrated)
        return 'migrated';
    if (bc.bonding_complete)
        return 'complete';
    return 'bonding';
};
const fetchAllRawTokens = async (connection) => {
    const coder = new anchor_1.BorshCoder(torch_market_json_1.default);
    const accounts = await connection.getProgramAccounts(constants_1.PROGRAM_ID, {
        filters: [{ memcmp: { offset: 0, bytes: '4y6pru6YvC7' } }],
    });
    const tokens = [];
    for (const acc of accounts) {
        try {
            const decoded = coder.accounts.decode('BondingCurve', acc.account.data);
            const mintStr = decoded.mint.toString();
            if (constants_1.BLACKLISTED_MINTS.includes(mintStr))
                continue;
            if (decoded.reclaimed)
                continue;
            tokens.push({
                mint: mintStr,
                bondingCurve: decoded,
            });
        }
        catch {
            // Not a bonding curve account
        }
    }
    return tokens;
};
const toTokenSummary = (raw) => {
    const bc = raw.bondingCurve;
    const virtualSol = BigInt(bc.virtual_sol_reserves.toString());
    const virtualTokens = BigInt(bc.virtual_token_reserves.toString());
    const realSol = BigInt(bc.real_sol_reserves.toString());
    const realTokens = BigInt(bc.real_token_reserves.toString());
    const voteVault = BigInt(bc.vote_vault_balance.toString());
    const price = (0, program_1.calculatePrice)(virtualSol, virtualTokens);
    const priceInSol = (price * constants_1.TOKEN_MULTIPLIER) / constants_1.LAMPORTS_PER_SOL;
    // Market cap = fully diluted (total supply × price), matching pump.fun convention
    const marketCapSol = (priceInSol * Number(constants_1.TOTAL_SUPPLY)) / constants_1.TOKEN_MULTIPLIER;
    return {
        mint: raw.mint,
        name: (0, program_1.decodeString)(bc.name),
        symbol: (0, program_1.decodeString)(bc.symbol),
        status: getTokenStatus(bc),
        price_sol: priceInSol,
        market_cap_sol: marketCapSol,
        progress_percent: (0, program_1.calculateBondingProgress)(realSol),
        holders: null,
        created_at: 0,
    };
};
const filterAndSort = (tokens, params) => {
    let filtered = [...tokens];
    if (params.status && params.status !== 'all') {
        filtered = filtered.filter((t) => getTokenStatus(t.bondingCurve) === params.status);
    }
    switch (params.sort) {
        case 'marketcap':
        case 'volume':
            filtered.sort((a, b) => {
                const aR = BigInt(a.bondingCurve.real_sol_reserves.toString());
                const bR = BigInt(b.bondingCurve.real_sol_reserves.toString());
                return bR > aR ? 1 : bR < aR ? -1 : 0;
            });
            break;
        case 'newest':
        default:
            filtered.sort((a, b) => {
                const aA = BigInt(a.bondingCurve.last_activity_slot.toString());
                const bA = BigInt(b.bondingCurve.last_activity_slot.toString());
                return bA > aA ? 1 : bA < aA ? -1 : 0;
            });
            break;
    }
    const offset = params.offset || 0;
    const limit = Math.min(params.limit || 50, 100);
    return filtered.slice(offset, offset + limit);
};
const buildTokenDetail = (mint, bc, treasury, metadata, holdersCount, solPriceUsd, saidVerification, warnings, poolPrice) => {
    const virtualSol = BigInt(bc.virtual_sol_reserves.toString());
    const virtualTokens = BigInt(bc.virtual_token_reserves.toString());
    const realSol = BigInt(bc.real_sol_reserves.toString());
    const realTokens = BigInt(bc.real_token_reserves.toString());
    const voteVault = BigInt(bc.vote_vault_balance.toString());
    const burned = BigInt(bc.permanently_burned_tokens?.toString() || '0');
    let priceInSol;
    let marketCapSol;
    if (bc.migrated && poolPrice && poolPrice.tokenReserves > 0) {
        // Use live Raydium pool price for migrated tokens
        priceInSol = poolPrice.solReserves / poolPrice.tokenReserves;
    }
    else {
        // Use bonding curve virtual reserves for pre-migration tokens
        const price = (0, program_1.calculatePrice)(virtualSol, virtualTokens);
        priceInSol = (price * constants_1.TOKEN_MULTIPLIER) / constants_1.LAMPORTS_PER_SOL;
    }
    // Market cap = fully diluted (total supply × price), matching pump.fun convention
    marketCapSol = (priceInSol * Number(constants_1.TOTAL_SUPPLY)) / constants_1.TOKEN_MULTIPLIER;
    const circulating = constants_1.TOTAL_SUPPLY - realTokens - voteVault;
    const treasurySol = treasury ? Number(treasury.sol_balance.toString()) / constants_1.LAMPORTS_PER_SOL : 0;
    const treasuryTokens = treasury ? Number(treasury.tokens_held.toString()) / constants_1.TOKEN_MULTIPLIER : 0;
    // V33: buyback removed — these fields are deprecated (always 0 for new tokens)
    const boughtBack = treasury ? Number(treasury.total_bought_back.toString()) / constants_1.TOKEN_MULTIPLIER : 0;
    const buybackCount = treasury ? Number(treasury.buyback_count.toString()) : 0;
    const stars = treasury ? Number(treasury.total_stars.toString()) : 0;
    return {
        mint,
        name: (0, program_1.decodeString)(bc.name),
        symbol: (0, program_1.decodeString)(bc.symbol),
        description: metadata?.description,
        image: metadata?.image,
        status: getTokenStatus(bc),
        price_sol: priceInSol,
        price_usd: solPriceUsd ? priceInSol * solPriceUsd : undefined,
        market_cap_sol: marketCapSol,
        market_cap_usd: solPriceUsd ? marketCapSol * solPriceUsd : undefined,
        progress_percent: (0, program_1.calculateBondingProgress)(realSol),
        sol_raised: Number(realSol) / constants_1.LAMPORTS_PER_SOL,
        sol_target: 200,
        total_supply: Number(constants_1.TOTAL_SUPPLY) / constants_1.TOKEN_MULTIPLIER,
        circulating_supply: Number(circulating) / constants_1.TOKEN_MULTIPLIER,
        tokens_in_curve: Number(realTokens) / constants_1.TOKEN_MULTIPLIER,
        tokens_in_vote_vault: Number(voteVault) / constants_1.TOKEN_MULTIPLIER,
        tokens_burned: Number(burned) / constants_1.TOKEN_MULTIPLIER,
        treasury_sol_balance: treasurySol,
        treasury_token_balance: treasuryTokens,
        total_bought_back: boughtBack,
        buyback_count: buybackCount,
        votes_return: Number(bc.votes_return.toString()),
        votes_burn: Number(bc.votes_burn.toString()),
        creator: bc.creator.toString(),
        holders: holdersCount ?? null,
        stars,
        created_at: 0,
        last_activity_at: Number(bc.last_activity_slot.toString()),
        twitter: metadata?.twitter,
        telegram: metadata?.telegram,
        website: metadata?.website,
        creator_verified: saidVerification?.verified,
        creator_trust_tier: saidVerification?.trustTier,
        creator_said_name: saidVerification?.name,
        creator_badge_url: saidVerification?.verified
            ? `https://api.saidprotocol.com/api/badge/${bc.creator.toString()}.svg`
            : undefined,
        ...(warnings && warnings.length > 0 ? { warnings } : {}),
    };
};
// Internal: fetch single token on-chain data
const fetchTokenRaw = async (connection, mint) => {
    const coder = new anchor_1.BorshCoder(torch_market_json_1.default);
    const [bondingCurvePda] = (0, program_1.getBondingCurvePda)(mint);
    const [treasuryPda] = (0, program_1.getTokenTreasuryPda)(mint);
    const [bcAccount, treasuryAccount] = await Promise.all([
        connection.getAccountInfo(bondingCurvePda),
        connection.getAccountInfo(treasuryPda),
    ]);
    if (!bcAccount)
        return null;
    const bondingCurve = coder.accounts.decode('BondingCurve', bcAccount.data);
    let treasury = null;
    if (treasuryAccount) {
        treasury = coder.accounts.decode('Treasury', treasuryAccount.data);
    }
    return { bondingCurve, treasury };
};
exports.fetchTokenRaw = fetchTokenRaw;
// ============================================================================
// Public API
// ============================================================================
/**
 * List tokens with optional filtering and sorting.
 */
const getTokens = async (connection, params = {}) => {
    const allTokens = await fetchAllRawTokens(connection);
    const filtered = filterAndSort(allTokens, params);
    const summaries = filtered.map(toTokenSummary);
    return {
        tokens: summaries,
        total: allTokens.length,
        limit: params.limit || 50,
        offset: params.offset || 0,
    };
};
exports.getTokens = getTokens;
/**
 * Get on-chain Token-2022 metadata for a token.
 *
 * Reads name, symbol, and uri directly from the mint's TokenMetadata extension.
 * Returns null if the mint has no metadata (legacy pre-V29 tokens).
 */
const getTokenMetadata = async (connection, mintStr) => {
    const mint = new web3_js_1.PublicKey(mintStr);
    const metadata = await (0, spl_token_1.getTokenMetadata)(connection, mint, 'confirmed', constants_1.TOKEN_2022_PROGRAM_ID);
    if (!metadata)
        return null;
    return {
        name: metadata.name,
        symbol: metadata.symbol,
        uri: metadata.uri,
        mint: mintStr,
    };
};
exports.getTokenMetadata = getTokenMetadata;
/**
 * Get detailed info for a single token.
 */
const getToken = async (connection, mintStr) => {
    const mint = new web3_js_1.PublicKey(mintStr);
    const tokenData = await fetchTokenRaw(connection, mint);
    if (!tokenData) {
        throw new Error(`Token not found: ${mintStr}`);
    }
    const { bondingCurve, treasury } = tokenData;
    const warnings = [];
    // Fetch metadata from URI
    let metadata;
    const uri = (0, program_1.decodeString)(bondingCurve.uri);
    if (uri) {
        try {
            const res = await (0, gateway_1.fetchWithFallback)(uri);
            const data = (await res.json());
            metadata = {
                description: data.description,
                image: data.image && (0, gateway_1.isIrysUrl)(data.image) ? (0, gateway_1.irysToUploader)(data.image) : data.image,
                twitter: data.twitter,
                telegram: data.telegram,
                website: data.website,
            };
        }
        catch (e) {
            warnings.push(`Metadata fetch failed: ${e instanceof Error ? e.message : String(e)}`);
        }
    }
    // Fetch holders count
    let holdersCount = null;
    try {
        const holders = await connection.getTokenLargestAccounts(mint, 'confirmed');
        holdersCount = holders.value.filter((a) => a.uiAmount && a.uiAmount > 0).length;
    }
    catch (e) {
        warnings.push(`Holders fetch failed: ${e instanceof Error ? e.message : String(e)}`);
    }
    // Fetch SOL price
    let solPriceUsd;
    try {
        const res = await fetch('https://api.coingecko.com/api/v3/simple/price?ids=solana&vs_currencies=usd');
        const data = (await res.json());
        solPriceUsd = data?.solana?.usd;
    }
    catch (e) {
        warnings.push(`SOL price fetch failed: ${e instanceof Error ? e.message : String(e)}`);
    }
    // Fetch live pool price for migrated tokens
    let poolPrice;
    if (bondingCurve.migrated) {
        try {
            const raydium = (0, program_1.getRaydiumMigrationAccounts)(mint);
            const [vault0Info, vault1Info] = await Promise.all([
                connection.getTokenAccountBalance(raydium.token0Vault),
                connection.getTokenAccountBalance(raydium.token1Vault),
            ]);
            const vault0Amount = Number(vault0Info.value.amount);
            const vault1Amount = Number(vault1Info.value.amount);
            if (raydium.isWsolToken0) {
                poolPrice = { solReserves: vault0Amount, tokenReserves: vault1Amount };
            }
            else {
                poolPrice = { solReserves: vault1Amount, tokenReserves: vault0Amount };
            }
        }
        catch (e) {
            warnings.push(`Pool price fetch failed: ${e instanceof Error ? e.message : String(e)}`);
        }
    }
    return buildTokenDetail(mintStr, bondingCurve, treasury, metadata, holdersCount, solPriceUsd, undefined, warnings, poolPrice);
};
exports.getToken = getToken;
/**
 * Get top holders for a token.
 */
const getHolders = async (connection, mintStr, limit = 20) => {
    const mint = new web3_js_1.PublicKey(mintStr);
    const safeLimit = Math.min(limit, 100);
    // Build excluded addresses (pools/vaults)
    const excluded = new Set();
    const [bondingCurvePda] = (0, program_1.getBondingCurvePda)(mint);
    const bondingCurveVault = (0, spl_token_1.getAssociatedTokenAddressSync)(mint, bondingCurvePda, true, constants_1.TOKEN_2022_PROGRAM_ID);
    excluded.add(bondingCurveVault.toString());
    const [treasuryPda] = (0, program_1.getTokenTreasuryPda)(mint);
    const treasuryVault = (0, spl_token_1.getAssociatedTokenAddressSync)(mint, treasuryPda, true, constants_1.TOKEN_2022_PROGRAM_ID);
    excluded.add(treasuryVault.toString());
    try {
        const raydiumAccounts = (0, program_1.getRaydiumMigrationAccounts)(mint);
        excluded.add(raydiumAccounts.token0Vault.toString());
        excluded.add(raydiumAccounts.token1Vault.toString());
    }
    catch {
        // Ignore
    }
    const response = await connection.getTokenLargestAccounts(mint, 'confirmed');
    const totalSupply = BigInt(1000000000) * BigInt(10 ** constants_1.TOKEN_DECIMALS);
    const filteredAccounts = response.value
        .filter((account) => account.uiAmount && account.uiAmount > 0)
        .filter((account) => !excluded.has(account.address.toString()))
        .slice(0, safeLimit);
    const accountInfos = await connection.getMultipleParsedAccounts(filteredAccounts.map((a) => a.address));
    const holders = filteredAccounts.map((account, i) => {
        const parsed = accountInfos.value[i]?.data;
        const owner = parsed && 'parsed' in parsed ? parsed.parsed?.info?.owner : null;
        return {
            address: owner || account.address.toString(),
            balance: Number(account.amount) / 10 ** constants_1.TOKEN_DECIMALS,
            percentage: (Number(account.amount) / Number(totalSupply)) * 100,
        };
    });
    return {
        holders,
        total_holders: response.value.filter((a) => a.uiAmount && a.uiAmount > 0 && !excluded.has(a.address.toString())).length,
    };
};
exports.getHolders = getHolders;
/**
 * Get messages (memos) for a token.
 */
const getMessages = async (connection, mintStr, limit = 50) => {
    const mint = new web3_js_1.PublicKey(mintStr);
    const safeLimit = Math.min(limit, 100);
    const [bondingCurvePda] = (0, program_1.getBondingCurvePda)(mint);
    // Fetch only as many signatures as needed (keep small to avoid 429s)
    const sigLimit = Math.min(safeLimit, 50);
    const signatures = await connection.getSignaturesForAddress(bondingCurvePda, { limit: sigLimit }, 'confirmed');
    if (signatures.length === 0)
        return { messages: [], total: 0 };
    const messages = [];
    // Batch fetch transactions (1 RPC call per batch instead of 1 per tx)
    const BATCH_SIZE = 100;
    for (let i = 0; i < signatures.length && messages.length < safeLimit; i += BATCH_SIZE) {
        const batch = signatures.slice(i, i + BATCH_SIZE);
        const sigStrings = batch.map((s) => s.signature);
        let txs;
        try {
            txs = await connection.getParsedTransactions(sigStrings, {
                maxSupportedTransactionVersion: 0,
            });
        }
        catch {
            continue;
        }
        for (let j = 0; j < txs.length && messages.length < safeLimit; j++) {
            const tx = txs[j];
            if (!tx?.meta || tx.meta.err)
                continue;
            for (const ix of tx.transaction.message.instructions) {
                const programId = 'programId' in ix ? ix.programId.toString() : '';
                const programName = 'program' in ix ? ix.program : '';
                const isMemo = programId === constants_1.MEMO_PROGRAM_ID.toString() ||
                    programId === [REDACTED_SECRET] ||
                    programName === 'spl-memo';
                if (isMemo) {
                    let memoText = '';
                    if ('parsed' in ix) {
                        memoText = typeof ix.parsed === 'string' ? ix.parsed : JSON.stringify(ix.parsed);
                    }
                    else if ('data' in ix && typeof ix.data === 'string') {
                        try {
                            const bs58 = await Promise.resolve().then(() => __importStar(require('bs58')));
                            const decoded = bs58.default.decode(ix.data);
                            memoText = new TextDecoder().decode(decoded);
                        }
                        catch {
                            memoText = ix.data;
                        }
                    }
                    if (memoText && memoText.trim()) {
                        const sender = tx.transaction.message.accountKeys[0]?.pubkey?.toString() || 'Unknown';
                        messages.push({
                            signature: batch[j].signature,
                            memo: memoText.trim(),
                            sender,
                            timestamp: batch[j].blockTime || 0,
                        });
                        break;
                    }
                }
            }
        }
    }
    return { messages, total: messages.length };
};
exports.getMessages = getMessages;
// ============================================================================
// Lending (V2.4)
// ============================================================================
// Lending constants (matching the Rust program)
const INTEREST_RATE_BPS = 200; // 2% per epoch
const MAX_LTV_BPS = 5000; // 50%
const LIQUIDATION_THRESHOLD_BPS = 6500; // 65%
const LIQUIDATION_BONUS_BPS = 1000; // 10%
const LENDING_UTILIZATION_CAP_BPS = 5000; // 50%
/**
 * Get lending info for a migrated token.
 *
 * Returns interest rates, LTV limits, and active loan statistics.
 * Lending is available on all migrated tokens with treasury SOL.
 */
const getLendingInfo = async (connection, mintStr) => {
    const mint = new web3_js_1.PublicKey(mintStr);
    const tokenData = await fetchTokenRaw(connection, mint);
    if (!tokenData)
        throw new Error(`Token not found: ${mintStr}`);
    const { bondingCurve, treasury } = tokenData;
    if (!bondingCurve.migrated)
        throw new Error('Token not yet migrated, lending not available');
    const treasurySol = treasury ? Number(treasury.sol_balance.toString()) : 0;
    // Scan for active loan positions via collateral vault balance
    const [collateralVaultPda] = (0, program_1.getCollateralVaultPda)(mint);
    const vaultInfo = await connection.getAccountInfo(collateralVaultPda);
    // Count active loans by scanning LoanPosition accounts
    let activeLoans = 0;
    let totalSolLent = 0;
    const warnings = [];
    try {
        // Derive discriminator from IDL rather than hardcoding
        const coder = new anchor_1.BorshCoder(torch_market_json_1.default);
        const loanDiscriminator = coder.accounts.accountDiscriminator('LoanPosition');
        const bs58 = await Promise.resolve().then(() => __importStar(require('bs58')));
        const accounts = await connection.getProgramAccounts(constants_1.PROGRAM_ID, {
            filters: [
                { memcmp: { offset: 0, bytes: bs58.default.encode(loanDiscriminator) } },
                { memcmp: { offset: 8 + 32, bytes: mint.toBase58() } }, // mint at offset 40
            ],
            dataSlice: { offset: 8 + 32 + 32, length: 16 }, // collateral_amount + borrowed_amount
        });
        for (const acc of accounts) {
            try {
                // Read borrowed_amount (u64 at offset 8 within the slice)
                const borrowed = acc.account.data.readBigUInt64LE(8);
                if (borrowed > BigInt(0)) {
                    activeLoans = (activeLoans ?? 0) + 1;
                    totalSolLent = (totalSolLent ?? 0) + Number(borrowed);
                }
            }
            catch {
                // Skip malformed accounts
            }
        }
    }
    catch (e) {
        activeLoans = null;
        totalSolLent = null;
        warnings.push(`Loan enumeration failed: ${e instanceof Error ? e.message : String(e)}`);
    }
    return {
        interest_rate_bps: INTEREST_RATE_BPS,
        max_ltv_bps: MAX_LTV_BPS,
        liquidation_threshold_bps: LIQUIDATION_THRESHOLD_BPS,
        liquidation_bonus_bps: LIQUIDATION_BONUS_BPS,
        total_sol_lent: totalSolLent,
        active_loans: activeLoans,
        treasury_sol_available: Math.max(0, Math.floor(treasurySol * LENDING_UTILIZATION_CAP_BPS / 10000) - (totalSolLent ?? 0)),
        ...(warnings.length > 0 ? { warnings } : {}),
    };
};
exports.getLendingInfo = getLendingInfo;
/**
 * Get loan position for a wallet on a specific token.
 *
 * Returns collateral locked, SOL owed, health status, etc.
 * Returns health="none" if no active loan exists.
 */
const getLoanPosition = async (connection, mintStr, walletStr) => {
    const mint = new web3_js_1.PublicKey(mintStr);
    const wallet = new web3_js_1.PublicKey(walletStr);
    const coder = new anchor_1.BorshCoder(torch_market_json_1.default);
    const [loanPositionPda] = (0, program_1.getLoanPositionPda)(mint, wallet);
    const accountInfo = await connection.getAccountInfo(loanPositionPda);
    if (!accountInfo) {
        return {
            collateral_amount: 0,
            borrowed_amount: 0,
            accrued_interest: 0,
            total_owed: 0,
            collateral_value_sol: 0,
            current_ltv_bps: 0,
            health: 'none',
        };
    }
    const loan = coder.accounts.decode('LoanPosition', accountInfo.data);
    const collateral = Number(loan.collateral_amount.toString());
    const borrowed = Number(loan.borrowed_amount.toString());
    const interest = Number(loan.accrued_interest.toString());
    const totalOwed = borrowed + interest;
    // Get collateral value from Raydium pool price
    let collateralValueSol = 0;
    const warnings = [];
    try {
        const raydium = (0, program_1.getRaydiumMigrationAccounts)(mint);
        const [vault0Info, vault1Info] = await Promise.all([
            connection.getTokenAccountBalance(raydium.token0Vault),
            connection.getTokenAccountBalance(raydium.token1Vault),
        ]);
        const vault0Amount = Number(vault0Info.value.amount);
        const vault1Amount = Number(vault1Info.value.amount);
        // Determine which vault is SOL and which is token
        if (raydium.isWsolToken0) {
            // token0 = WSOL, token1 = token
            const solReserves = vault0Amount;
            const tokenReserves = vault1Amount;
            if (tokenReserves > 0) {
                collateralValueSol = (collateral * solReserves) / tokenReserves;
            }
        }
        else {
            // token0 = token, token1 = WSOL
            const solReserves = vault1Amount;
            const tokenReserves = vault0Amount;
            if (tokenReserves > 0) {
                collateralValueSol = (collateral * solReserves) / tokenReserves;
            }
        }
    }
    catch (e) {
        collateralValueSol = null;
        warnings.push(`Collateral valuation failed: ${e instanceof Error ? e.message : String(e)}`);
    }
    let currentLtvBps;
    if (collateralValueSol === null) {
        currentLtvBps = null;
    }
    else if (collateralValueSol > 0) {
        currentLtvBps = Math.floor((totalOwed / collateralValueSol) * 10000);
    }
    else {
        currentLtvBps = totalOwed > 0 ? 10000 : 0;
    }
    let health;
    if (borrowed === 0 && interest === 0) {
        health = 'none';
    }
    else if (currentLtvBps === null) {
        health = 'healthy';
    }
    else if (currentLtvBps >= LIQUIDATION_THRESHOLD_BPS) {
        health = 'liquidatable';
    }
    else if (currentLtvBps >= MAX_LTV_BPS) {
        health = 'at_risk';
    }
    else {
        health = 'healthy';
    }
    return {
        collateral_amount: collateral,
        borrowed_amount: borrowed,
        accrued_interest: interest,
        total_owed: totalOwed,
        collateral_value_sol: collateralValueSol,
        current_ltv_bps: currentLtvBps,
        health,
        ...(warnings.length > 0 ? { warnings } : {}),
    };
};
exports.getLoanPosition = getLoanPosition;
/**
 * Get all active loan positions for a given token mint.
 *
 * Scans on-chain LoanPosition accounts, computes health for each,
 * and returns them sorted: liquidatable first, then at_risk, then healthy.
 */
const getAllLoanPositions = async (connection, mintStr) => {
    const mint = new web3_js_1.PublicKey(mintStr);
    const coder = new anchor_1.BorshCoder(torch_market_json_1.default);
    // 1. Fetch all LoanPosition accounts for this mint
    const loanDiscriminator = coder.accounts.accountDiscriminator('LoanPosition');
    const bs58 = await Promise.resolve().then(() => __importStar(require('bs58')));
    const accounts = await connection.getProgramAccounts(constants_1.PROGRAM_ID, {
        filters: [
            { memcmp: { offset: 0, bytes: bs58.default.encode(loanDiscriminator) } },
            { memcmp: { offset: 8 + 32, bytes: mint.toBase58() } }, // mint at offset 40
        ],
    });
    // 2. Decode and filter to active loans (borrowed_amount > 0)
    const activeLoans = [];
    for (const acc of accounts) {
        try {
            const loan = coder.accounts.decode('LoanPosition', acc.account.data);
            const borrowed = Number(loan.borrowed_amount.toString());
            if (borrowed > 0) {
                activeLoans.push({
                    borrower: loan.user.toString(),
                    loan,
                });
            }
        }
        catch {
            // Skip malformed accounts
        }
    }
    // 3. Fetch Raydium pool price ONCE
    let poolPriceSol = null;
    let solReserves = 0;
    let tokenReserves = 0;
    try {
        const raydium = (0, program_1.getRaydiumMigrationAccounts)(mint);
        const [vault0Info, vault1Info] = await Promise.all([
            connection.getTokenAccountBalance(raydium.token0Vault),
            connection.getTokenAccountBalance(raydium.token1Vault),
        ]);
        const vault0Amount = Number(vault0Info.value.amount);
        const vault1Amount = Number(vault1Info.value.amount);
        if (raydium.isWsolToken0) {
            solReserves = vault0Amount;
            tokenReserves = vault1Amount;
        }
        else {
            solReserves = vault1Amount;
            tokenReserves = vault0Amount;
        }
        if (tokenReserves > 0) {
            poolPriceSol = solReserves / tokenReserves;
        }
    }
    catch {
        // Pool price unavailable
    }
    // 4. Compute health for each position
    const positions = activeLoans.map(({ borrower, loan }) => {
        const collateral = Number(loan.collateral_amount.toString());
        const borrowed = Number(loan.borrowed_amount.toString());
        const interest = Number(loan.accrued_interest.toString());
        const totalOwed = borrowed + interest;
        let collateralValueSol = null;
        if (poolPriceSol !== null && tokenReserves > 0) {
            collateralValueSol = (collateral * solReserves) / tokenReserves;
        }
        let currentLtvBps;
        if (collateralValueSol === null) {
            currentLtvBps = null;
        }
        else if (collateralValueSol > 0) {
            currentLtvBps = Math.floor((totalOwed / collateralValueSol) * 10000);
        }
        else {
            currentLtvBps = totalOwed > 0 ? 10000 : 0;
        }
        let health;
        if (borrowed === 0 && interest === 0) {
            health = 'none';
        }
        else if (currentLtvBps === null) {
            health = 'healthy';
        }
        else if (currentLtvBps >= LIQUIDATION_THRESHOLD_BPS) {
            health = 'liquidatable';
        }
        else if (currentLtvBps >= MAX_LTV_BPS) {
            health = 'at_risk';
        }
        else {
            health = 'healthy';
        }
        return {
            borrower,
            collateral_amount: collateral,
            borrowed_amount: borrowed,
            accrued_interest: interest,
            total_owed: totalOwed,
            collateral_value_sol: collateralValueSol,
            current_ltv_bps: currentLtvBps,
            health,
        };
    });
    // 5. Sort: liquidatable first, then at_risk, then healthy
    const healthOrder = { liquidatable: 0, at_risk: 1, healthy: 2, none: 3 };
    positions.sort((a, b) => (healthOrder[a.health] ?? 3) - (healthOrder[b.health] ?? 3));
    return { positions, pool_price_sol: poolPriceSol };
};
exports.getAllLoanPositions = getAllLoanPositions;
// ============================================================================
// Vault Queries (V2.0)
// ============================================================================
/**
 * Get vault state by the vault creator's public key.
 *
 * Returns vault balance, authority, linked wallet count, etc.
 * Returns null if no vault exists for this creator.
 */
const getVault = async (connection, creatorStr) => {
    const creator = new web3_js_1.PublicKey(creatorStr);
    const coder = new anchor_1.BorshCoder(torch_market_json_1.default);
    const [vaultPda] = (0, program_1.getTorchVaultPda)(creator);
    const accountInfo = await connection.getAccountInfo(vaultPda);
    if (!accountInfo)
        return null;
    const vault = coder.accounts.decode('TorchVault', accountInfo.data);
    return {
        address: vaultPda.toString(),
        creator: vault.creator.toString(),
        authority: vault.authority.toString(),
        sol_balance: Number(vault.sol_balance.toString()) / constants_1.LAMPORTS_PER_SOL,
        total_deposited: Number(vault.total_deposited.toString()) / constants_1.LAMPORTS_PER_SOL,
        total_withdrawn: Number(vault.total_withdrawn.toString()) / constants_1.LAMPORTS_PER_SOL,
        total_spent: Number(vault.total_spent.toString()) / constants_1.LAMPORTS_PER_SOL,
        total_received: Number(vault.total_received.toString()) / constants_1.LAMPORTS_PER_SOL,
        linked_wallets: vault.linked_wallets,
        created_at: Number(vault.created_at.toString()),
    };
};
exports.getVault = getVault;
/**
 * Get vault state by looking up a linked wallet's VaultWalletLink.
 *
 * Useful when you have an agent wallet and need to find its vault.
 * Returns null if the wallet is not linked to any vault.
 */
const getVaultForWallet = async (connection, walletStr) => {
    const wallet = new web3_js_1.PublicKey(walletStr);
    const coder = new anchor_1.BorshCoder(torch_market_json_1.default);
    const [walletLinkPda] = (0, program_1.getVaultWalletLinkPda)(wallet);
    const linkInfo = await connection.getAccountInfo(walletLinkPda);
    if (!linkInfo)
        return null;
    const link = coder.accounts.decode('VaultWalletLink', linkInfo.data);
    // Now fetch the vault using the vault PDA stored in the link
    const vaultInfo = await connection.getAccountInfo(link.vault);
    if (!vaultInfo)
        return null;
    const vault = coder.accounts.decode('TorchVault', vaultInfo.data);
    return {
        address: link.vault.toString(),
        creator: vault.creator.toString(),
        authority: vault.authority.toString(),
        sol_balance: Number(vault.sol_balance.toString()) / constants_1.LAMPORTS_PER_SOL,
        total_deposited: Number(vault.total_deposited.toString()) / constants_1.LAMPORTS_PER_SOL,
        total_withdrawn: Number(vault.total_withdrawn.toString()) / constants_1.LAMPORTS_PER_SOL,
        total_spent: Number(vault.total_spent.toString()) / constants_1.LAMPORTS_PER_SOL,
        total_received: Number(vault.total_received.toString()) / constants_1.LAMPORTS_PER_SOL,
        linked_wallets: vault.linked_wallets,
        created_at: Number(vault.created_at.toString()),
    };
};
exports.getVaultForWallet = getVaultForWallet;
/**
 * Get wallet link state for a specific wallet.
 *
 * Returns the link info (which vault it's linked to, when) or null if not linked.
 */
const getVaultWalletLink = async (connection, walletStr) => {
    const wallet = new web3_js_1.PublicKey(walletStr);
    const coder = new anchor_1.BorshCoder(torch_market_json_1.default);
    const [walletLinkPda] = (0, program_1.getVaultWalletLinkPda)(wallet);
    const accountInfo = await connection.getAccountInfo(walletLinkPda);
    if (!accountInfo)
        return null;
    const link = coder.accounts.decode('VaultWalletLink', accountInfo.data);
    return {
        address: walletLinkPda.toString(),
        vault: link.vault.toString(),
        wallet: link.wallet.toString(),
        linked_at: Number(link.linked_at.toString()),
    };
};
exports.getVaultWalletLink = getVaultWalletLink;
//# sourceMappingURL=tokens.js.map

path: lib/torchsdk/types.js
content: "use strict";
/**
 * Torch Market SDK Types
 */
Object.defineProperty(exports, "__esModule", { value: true });
//# sourceMappingURL=types.js.map

path: lib/torchsdk/gateway.js
content: "use strict";
/**
 * Gateway URL utilities for Irys content
 * Use uploader.irys.xyz as fallback when gateway.irys.xyz has issues
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.fetchWithFallback = exports.isIrysUrl = exports.irysToUploader = void 0;
const IRYS_GATEWAY = 'gateway.irys.xyz';
const IRYS_UPLOADER = 'uploader.irys.xyz';
/**
 * Convert an Irys gateway URL to uploader URL (fallback)
 */
const irysToUploader = (url) => url.replace(IRYS_GATEWAY, IRYS_UPLOADER);
exports.irysToUploader = irysToUploader;
/**
 * Check if URL is from Irys gateway
 */
const isIrysUrl = (url) => {
    try {
        return new URL(url).hostname === IRYS_GATEWAY;
    }
    catch {
        return false;
    }
};
exports.isIrysUrl = isIrysUrl;
/**
 * Fetch with automatic fallback from gateway.irys.xyz to uploader.irys.xyz
 */
const fetchWithFallback = async (url, options, timeoutMs = 10000) => {
    const controller = new AbortController();
    const timer = setTimeout(() => controller.abort(), timeoutMs);
    const opts = { ...options, signal: controller.signal };
    try {
        // If it's an Irys gateway URL, use uploader directly (gateway has SSL issues)
        if ((0, exports.isIrysUrl)(url)) {
            const uploaderUrl = (0, exports.irysToUploader)(url);
            return await fetch(uploaderUrl, opts);
        }
        // For non-Irys URLs, fetch normally
        return await fetch(url, opts);
    }
    finally {
        clearTimeout(timer);
    }
};
exports.fetchWithFallback = fetchWithFallback;
//# sourceMappingURL=gateway.js.map

path: lib/torchsdk/index.js
content: "use strict";
/**
 * @torch-market/sdk
 *
 * AI agent toolkit for Solana fair-launch tokens.
 *
 * Usage:
 *   import { getTokens, buildBuyTransaction } from "@torch-market/sdk";
 *   const connection = new Connection("https://api.mainnet-beta.solana.com");
 *   const tokens = await getTokens(connection);
 *   const tx = await buildBuyTransaction(connection, { mint, buyer, amount_sol: 100_000_000 });
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.LEGACY_MINTS = exports.TOTAL_SUPPLY = exports.TOKEN_MULTIPLIER = exports.LAMPORTS_PER_SOL = exports.PROGRAM_ID = exports.confirmTransaction = exports.verifySaid = exports.createEphemeralAgent = exports.buildSwapFeesToSolTransaction = exports.buildHarvestFeesTransaction = exports.buildVaultSwapTransaction = exports.buildWithdrawTokensTransaction = exports.buildTransferAuthorityTransaction = exports.buildUnlinkWalletTransaction = exports.buildLinkWalletTransaction = exports.buildWithdrawVaultTransaction = exports.buildDepositVaultTransaction = exports.buildCreateVaultTransaction = exports.buildClaimProtocolRewardsTransaction = exports.buildLiquidateTransaction = exports.buildRepayTransaction = exports.buildBorrowTransaction = exports.buildMigrateTransaction = exports.buildStarTransaction = exports.buildCreateTokenTransaction = exports.buildSellTransaction = exports.buildDirectBuyTransaction = exports.buildBuyTransaction = exports.getSellQuote = exports.getBuyQuote = exports.getVaultWalletLink = exports.getVaultForWallet = exports.getVault = exports.getAllLoanPositions = exports.getLoanPosition = exports.getLendingInfo = exports.getMessages = exports.getHolders = exports.getTokenMetadata = exports.getToken = exports.getTokens = void 0;
// Token data
var tokens_1 = require("./tokens");
Object.defineProperty(exports, "getTokens", { enumerable: true, get: function () { return tokens_1.getTokens; } });
Object.defineProperty(exports, "getToken", { enumerable: true, get: function () { return tokens_1.getToken; } });
Object.defineProperty(exports, "getTokenMetadata", { enumerable: true, get: function () { return tokens_1.getTokenMetadata; } });
Object.defineProperty(exports, "getHolders", { enumerable: true, get: function () { return tokens_1.getHolders; } });
Object.defineProperty(exports, "getMessages", { enumerable: true, get: function () { return tokens_1.getMessages; } });
Object.defineProperty(exports, "getLendingInfo", { enumerable: true, get: function () { return tokens_1.getLendingInfo; } });
Object.defineProperty(exports, "getLoanPosition", { enumerable: true, get: function () { return tokens_1.getLoanPosition; } });
Object.defineProperty(exports, "getAllLoanPositions", { enumerable: true, get: function () { return tokens_1.getAllLoanPositions; } });
Object.defineProperty(exports, "getVault", { enumerable: true, get: function () { return tokens_1.getVault; } });
Object.defineProperty(exports, "getVaultForWallet", { enumerable: true, get: function () { return tokens_1.getVaultForWallet; } });
Object.defineProperty(exports, "getVaultWalletLink", { enumerable: true, get: function () { return tokens_1.getVaultWalletLink; } });
// Quotes
var quotes_1 = require("./quotes");
Object.defineProperty(exports, "getBuyQuote", { enumerable: true, get: function () { return quotes_1.getBuyQuote; } });
Object.defineProperty(exports, "getSellQuote", { enumerable: true, get: function () { return quotes_1.getSellQuote; } });
// Transaction builders
var transactions_1 = require("./transactions");
Object.defineProperty(exports, "buildBuyTransaction", { enumerable: true, get: function () { return transactions_1.buildBuyTransaction; } });
Object.defineProperty(exports, "buildDirectBuyTransaction", { enumerable: true, get: function () { return transactions_1.buildDirectBuyTransaction; } });
Object.defineProperty(exports, "buildSellTransaction", { enumerable: true, get: function () { return transactions_1.buildSellTransaction; } });
Object.defineProperty(exports, "buildCreateTokenTransaction", { enumerable: true, get: function () { return transactions_1.buildCreateTokenTransaction; } });
Object.defineProperty(exports, "buildStarTransaction", { enumerable: true, get: function () { return transactions_1.buildStarTransaction; } });
Object.defineProperty(exports, "buildMigrateTransaction", { enumerable: true, get: function () { return transactions_1.buildMigrateTransaction; } });
Object.defineProperty(exports, "buildBorrowTransaction", { enumerable: true, get: function () { return transactions_1.buildBorrowTransaction; } });
Object.defineProperty(exports, "buildRepayTransaction", { enumerable: true, get: function () { return transactions_1.buildRepayTransaction; } });
Object.defineProperty(exports, "buildLiquidateTransaction", { enumerable: true, get: function () { return transactions_1.buildLiquidateTransaction; } });
Object.defineProperty(exports, [REDACTED_SECRET], { enumerable: true, get: function () { return transactions_1.buildClaimProtocolRewardsTransaction; } });
Object.defineProperty(exports, "buildCreateVaultTransaction", { enumerable: true, get: function () { return transactions_1.buildCreateVaultTransaction; } });
Object.defineProperty(exports, "buildDepositVaultTransaction", { enumerable: true, get: function () { return transactions_1.buildDepositVaultTransaction; } });
Object.defineProperty(exports, "buildWithdrawVaultTransaction", { enumerable: true, get: function () { return transactions_1.buildWithdrawVaultTransaction; } });
Object.defineProperty(exports, "buildLinkWalletTransaction", { enumerable: true, get: function () { return transactions_1.buildLinkWalletTransaction; } });
Object.defineProperty(exports, "buildUnlinkWalletTransaction", { enumerable: true, get: function () { return transactions_1.buildUnlinkWalletTransaction; } });
Object.defineProperty(exports, [REDACTED_SECRET], { enumerable: true, get: function () { return transactions_1.buildTransferAuthorityTransaction; } });
Object.defineProperty(exports, "buildWithdrawTokensTransaction", { enumerable: true, get: function () { return transactions_1.buildWithdrawTokensTransaction; } });
Object.defineProperty(exports, "buildVaultSwapTransaction", { enumerable: true, get: function () { return transactions_1.buildVaultSwapTransaction; } });
Object.defineProperty(exports, "buildHarvestFeesTransaction", { enumerable: true, get: function () { return transactions_1.buildHarvestFeesTransaction; } });
Object.defineProperty(exports, "buildSwapFeesToSolTransaction", { enumerable: true, get: function () { return transactions_1.buildSwapFeesToSolTransaction; } });
// Ephemeral Agent
var ephemeral_1 = require("./ephemeral");
Object.defineProperty(exports, "createEphemeralAgent", { enumerable: true, get: function () { return ephemeral_1.createEphemeralAgent; } });
// SAID Protocol
var said_1 = require("./said");
Object.defineProperty(exports, "verifySaid", { enumerable: true, get: function () { return said_1.verifySaid; } });
Object.defineProperty(exports, "confirmTransaction", { enumerable: true, get: function () { return said_1.confirmTransaction; } });
// Constants (for advanced usage)
var constants_1 = require("./constants");
Object.defineProperty(exports, "PROGRAM_ID", { enumerable: true, get: function () { return constants_1.PROGRAM_ID; } });
Object.defineProperty(exports, "LAMPORTS_PER_SOL", { enumerable: true, get: function () { return constants_1.LAMPORTS_PER_SOL; } });
Object.defineProperty(exports, "TOKEN_MULTIPLIER", { enumerable: true, get: function () { return constants_1.TOKEN_MULTIPLIER; } });
Object.defineProperty(exports, "TOTAL_SUPPLY", { enumerable: true, get: function () { return constants_1.TOTAL_SUPPLY; } });
Object.defineProperty(exports, "LEGACY_MINTS", { enumerable: true, get: function () { return constants_1.LEGACY_MINTS; } });
//# sourceMappingURL=index.js.map

path: lib/torchsdk/ephemeral.js
content: "use strict";
/**
 * Ephemeral Agent Keypair
 *
 * Generates an in-process keypair that lives only in memory.
 * The authority links this wallet to their vault, and the SDK
 * uses it to sign transactions. When the process stops, the
 * private key is lost — zero key management, zero risk.
 *
 * Flow:
 *   1. const agent = createEphemeralAgent()
 *   2. Authority calls buildLinkWalletTransaction({ wallet_to_link: agent.publicKey })
 *   3. SDK uses agent.sign(tx) for all vault operations
 *   4. On shutdown, keys are GC'd. Authority unlinks the wallet.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.createEphemeralAgent = void 0;
const web3_js_1 = require("@solana/web3.js");
/**
 * Create an ephemeral agent keypair.
 *
 * The keypair exists only in memory. No file is written to disk.
 * When the process exits, the private key is permanently lost.
 *
 * @returns EphemeralAgent with publicKey, sign function, and raw keypair
 */
const createEphemeralAgent = () => {
    const keypair = web3_js_1.Keypair.generate();
    return {
        publicKey: keypair.publicKey.toBase58(),
        keypair,
        sign: (tx) => {
            tx.partialSign(keypair);
            return tx;
        },
    };
};
exports.createEphemeralAgent = createEphemeralAgent;
//# sourceMappingURL=ephemeral.js.map

path: lib/torchsdk/quotes.js
content: "use strict";
/**
 * Quote calculations
 *
 * Get expected output for buy/sell operations.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.getSellQuote = exports.getBuyQuote = void 0;
const web3_js_1 = require("@solana/web3.js");
const program_1 = require("./program");
const constants_1 = require("./constants");
const tokens_1 = require("./tokens");
/**
 * Get a buy quote: how many tokens for a given SOL amount.
 */
const getBuyQuote = async (connection, mintStr, amountSolLamports) => {
    const mint = new web3_js_1.PublicKey(mintStr);
    const tokenData = await (0, tokens_1.fetchTokenRaw)(connection, mint);
    if (!tokenData) {
        throw new Error(`Token not found: ${mintStr}`);
    }
    const { bondingCurve } = tokenData;
    if (bondingCurve.bonding_complete) {
        throw new Error('Bonding curve complete, trade on DEX');
    }
    const virtualSol = BigInt(bondingCurve.virtual_sol_reserves.toString());
    const virtualTokens = BigInt(bondingCurve.virtual_token_reserves.toString());
    const realSol = BigInt(bondingCurve.real_sol_reserves.toString());
    const bondingTarget = BigInt(bondingCurve.bonding_target.toString());
    const amountSol = BigInt(amountSolLamports);
    const result = (0, program_1.calculateTokensOut)(amountSol, virtualSol, virtualTokens, realSol, 100, 100, bondingTarget);
    const priceBefore = (0, program_1.calculatePrice)(virtualSol, virtualTokens);
    const priceAfter = (0, program_1.calculatePrice)(virtualSol + result.solToCurve, virtualTokens - result.tokensOut);
    const priceImpact = ((priceAfter - priceBefore) / priceBefore) * 100;
    const minOutput = (result.tokensToUser * BigInt(99)) / BigInt(100);
    return {
        input_sol: Number(amountSol),
        output_tokens: Number(result.tokensOut),
        tokens_to_user: Number(result.tokensToUser),
        tokens_to_treasury: Number(result.tokensToCommunity),
        protocol_fee_sol: Number(result.protocolFee),
        price_per_token_sol: (priceBefore * constants_1.TOKEN_MULTIPLIER) / constants_1.LAMPORTS_PER_SOL,
        price_impact_percent: priceImpact,
        min_output_tokens: Number(minOutput),
    };
};
exports.getBuyQuote = getBuyQuote;
/**
 * Get a sell quote: how much SOL for a given token amount.
 */
const getSellQuote = async (connection, mintStr, amountTokens) => {
    const mint = new web3_js_1.PublicKey(mintStr);
    const tokenData = await (0, tokens_1.fetchTokenRaw)(connection, mint);
    if (!tokenData) {
        throw new Error(`Token not found: ${mintStr}`);
    }
    const { bondingCurve } = tokenData;
    if (bondingCurve.bonding_complete) {
        throw new Error('Bonding curve complete, trade on DEX');
    }
    const virtualSol = BigInt(bondingCurve.virtual_sol_reserves.toString());
    const virtualTokens = BigInt(bondingCurve.virtual_token_reserves.toString());
    const tokenAmount = BigInt(amountTokens);
    const result = (0, program_1.calculateSolOut)(tokenAmount, virtualSol, virtualTokens);
    const priceBefore = (0, program_1.calculatePrice)(virtualSol, virtualTokens);
    const priceAfter = (0, program_1.calculatePrice)(virtualSol - result.solOut, virtualTokens + tokenAmount);
    const priceImpact = ((priceBefore - priceAfter) / priceBefore) * 100;
    const minOutput = (result.solToUser * BigInt(99)) / BigInt(100);
    return {
        input_tokens: Number(tokenAmount),
        output_sol: Number(result.solToUser),
        protocol_fee_sol: 0,
        price_per_token_sol: (priceBefore * constants_1.TOKEN_MULTIPLIER) / constants_1.LAMPORTS_PER_SOL,
        price_impact_percent: priceImpact,
        min_output_sol: Number(minOutput),
    };
};
exports.getSellQuote = getSellQuote;
//# sourceMappingURL=quotes.js.map

path: lib/torchsdk/said.js
content: "use strict";
/**
 * SAID Protocol integration
 *
 * Verify wallet reputation and confirm transactions for SAID feedback.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.confirmTransaction = exports.verifySaid = void 0;
const constants_1 = require("./constants");
const SAID_API_URL = 'https://api.saidprotocol.com/api';
// ============================================================================
// Verify
// ============================================================================
/**
 * Check SAID verification status for a wallet.
 *
 * @param wallet - Wallet address to verify
 * @returns Verification status including trust tier
 */
const verifySaid = async (wallet) => {
    try {
        const res = await fetch(`${SAID_API_URL}/verify/${wallet}`);
        const data = (await res.json());
        return {
            verified: data.verified ?? false,
            trustTier: data.trustTier ?? null,
            name: data.name,
        };
    }
    catch {
        return { verified: false, trustTier: null };
    }
};
exports.verifySaid = verifySaid;
// ============================================================================
// Confirm
// ============================================================================
/**
 * Confirm a transaction on-chain and determine event type.
 *
 * After an agent submits a transaction, call this to verify it succeeded
 * and determine the event type for reputation tracking.
 *
 * @param connection - Solana RPC connection
 * @param signature - Transaction signature to confirm
 * @param wallet - Wallet address that signed the transaction
 * @returns Confirmation result with event type
 */
const confirmTransaction = async (connection, signature, wallet) => {
    const tx = await connection.getParsedTransaction(signature, {
        maxSupportedTransactionVersion: 0,
        commitment: 'confirmed',
    });
    if (!tx)
        throw new Error('Transaction not found or not confirmed');
    if (tx.meta?.err)
        throw new Error('Transaction failed on-chain');
    // Verify wallet was a signer
    const signers = tx.transaction.message.accountKeys
        .filter((key) => key.signer)
        .map((key) => key.pubkey.toString());
    if (!signers.includes(wallet)) {
        throw new Error('Wallet was not a signer on this transaction');
    }
    // Find Torch Market instructions
    const torchInstructions = tx.transaction.message.instructions.filter((ix) => {
        const programId = 'programId' in ix ? ix.programId.toString() : '';
        return programId === constants_1.PROGRAM_ID.toString();
    });
    if (torchInstructions.length === 0) {
        throw new Error('Transaction does not involve Torch Market');
    }
    // Determine event type from logs
    const logs = tx.meta?.logMessages || [];
    const isCreateToken = logs.some((log) => log.includes('Instruction: CreateToken') || log.includes('create_token'));
    const isBuy = logs.some((log) => log.includes('Instruction: Buy') || log.includes('Program log: Buy'));
    const isSell = logs.some((log) => log.includes('Instruction: Sell') || log.includes('Program log: Sell'));
    const isVote = logs.some((log) => log.includes('Instruction: Vote') || log.includes('Program log: Vote'));
    let event_type = 'unknown';
    if (isCreateToken)
        event_type = 'token_launch';
    else if (isBuy || isSell)
        event_type = 'trade_complete';
    else if (isVote)
        event_type = 'governance_vote';
    return { confirmed: true, event_type };
};
exports.confirmTransaction = confirmTransaction;
//# sourceMappingURL=said.js.map

Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)