@wzrd_sol/sdk 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -221,8 +221,10 @@ export async function createSettleMarketIx(connection, user, marketId, programId
221
221
  const protocolState = getProtocolStatePDA(programId);
222
222
  const marketVault = getMarketVaultPDA(protocolState, marketId, programId);
223
223
  const userPosition = getUserPositionPDA(marketVault, user, programId);
224
- const accountsMode = options.accountsMode
225
- ?? 'current';
224
+ // 'auto' resolves based on program ID: mainnet uses legacy_ccm layout
225
+ const accountsMode = options.accountsMode === 'auto' || !options.accountsMode
226
+ ? (programId.equals(PROGRAM_ID) ? 'legacy_ccm' : 'current')
227
+ : options.accountsMode;
226
228
  // Fetch vault + protocol state to discover mints/accounts.
227
229
  const vaultInfo = await connection.getAccountInfo(marketVault);
228
230
  if (!vaultInfo)
@@ -523,6 +525,31 @@ function getMarketStatePDA(ccmMint, marketId, programId = PROGRAM_ID) {
523
525
  idBuf.writeBigUInt64LE(BigInt(marketId));
524
526
  return PublicKey.findProgramAddressSync([MARKET_STATE_SEED, ccmMint.toBuffer(), idBuf], programId)[0];
525
527
  }
528
+ // ── Prediction market PDA helpers ────────────────────────────────────
529
+ const PM_VAULT_SEED = Buffer.from('market_vault');
530
+ const MARKET_YES_MINT_SEED = Buffer.from('market_yes');
531
+ const MARKET_NO_MINT_SEED = Buffer.from('market_no');
532
+ const MARKET_MINT_AUTH_SEED = Buffer.from('market_auth');
533
+ function getPredictionVaultPDA(ccmMint, marketId, programId = PROGRAM_ID) {
534
+ const idBuf = Buffer.alloc(8);
535
+ idBuf.writeBigUInt64LE(BigInt(marketId));
536
+ return PublicKey.findProgramAddressSync([PM_VAULT_SEED, ccmMint.toBuffer(), idBuf], programId)[0];
537
+ }
538
+ function getPredictionYesMintPDA(ccmMint, marketId, programId = PROGRAM_ID) {
539
+ const idBuf = Buffer.alloc(8);
540
+ idBuf.writeBigUInt64LE(BigInt(marketId));
541
+ return PublicKey.findProgramAddressSync([MARKET_YES_MINT_SEED, ccmMint.toBuffer(), idBuf], programId)[0];
542
+ }
543
+ function getPredictionNoMintPDA(ccmMint, marketId, programId = PROGRAM_ID) {
544
+ const idBuf = Buffer.alloc(8);
545
+ idBuf.writeBigUInt64LE(BigInt(marketId));
546
+ return PublicKey.findProgramAddressSync([MARKET_NO_MINT_SEED, ccmMint.toBuffer(), idBuf], programId)[0];
547
+ }
548
+ function getPredictionMintAuthorityPDA(ccmMint, marketId, programId = PROGRAM_ID) {
549
+ const idBuf = Buffer.alloc(8);
550
+ idBuf.writeBigUInt64LE(BigInt(marketId));
551
+ return PublicKey.findProgramAddressSync([MARKET_MINT_AUTH_SEED, ccmMint.toBuffer(), idBuf], programId)[0];
552
+ }
526
553
  function getLegacyProtocolStatePDA(ccmMint, programId = PROGRAM_ID) {
527
554
  return PublicKey.findProgramAddressSync([PROTOCOL_SEED, ccmMint.toBuffer()], programId)[0];
528
555
  }
@@ -1574,3 +1601,174 @@ label, newUpdater, programId = PROGRAM_ID) {
1574
1601
  data,
1575
1602
  });
1576
1603
  }
1604
+ // ══════════════════════════════════════════════════════════════════════
1605
+ // PREDICTION MARKET TRADING INSTRUCTIONS
1606
+ // ══════════════════════════════════════════════════════════════════════
1607
+ /** Standard SPL Token program (used for YES/NO outcome tokens). */
1608
+ const SPL_TOKEN_PROGRAM_ID = new PublicKey('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA');
1609
+ /**
1610
+ * Build a `mint_shares` TransactionInstruction (prediction markets).
1611
+ *
1612
+ * Deposits CCM into the prediction market vault and receives 1:1 YES + NO tokens.
1613
+ *
1614
+ * Accounts (13 fixed, order matches markets.rs mint_shares):
1615
+ * 0. depositor (signer, writable)
1616
+ * 1. protocol_state (readonly)
1617
+ * 2. market_state (writable)
1618
+ * 3. ccm_mint (readonly)
1619
+ * 4. depositor_ccm_ata (writable)
1620
+ * 5. vault (writable)
1621
+ * 6. yes_mint (writable)
1622
+ * 7. no_mint (writable)
1623
+ * 8. depositor_yes_ata (writable)
1624
+ * 9. depositor_no_ata (writable)
1625
+ * 10. mint_authority (readonly)
1626
+ * 11. token_program (Token-2022, for CCM)
1627
+ * 12. outcome_token_prog (SPL Token, for YES/NO)
1628
+ *
1629
+ * Args: amount (u64) — CCM native units to deposit
1630
+ */
1631
+ export async function createMintSharesIx(depositor, ccmMint, marketId, amount, programId = PROGRAM_ID) {
1632
+ const protocolState = getProtocolStatePDA(programId);
1633
+ const marketState = getMarketStatePDA(ccmMint, marketId, programId);
1634
+ const vault = getPredictionVaultPDA(ccmMint, marketId, programId);
1635
+ const yesMint = getPredictionYesMintPDA(ccmMint, marketId, programId);
1636
+ const noMint = getPredictionNoMintPDA(ccmMint, marketId, programId);
1637
+ const mintAuthority = getPredictionMintAuthorityPDA(ccmMint, marketId, programId);
1638
+ const depositorCcm = getAta(depositor, ccmMint, TOKEN_2022_PROGRAM_ID);
1639
+ const depositorYes = getAta(depositor, yesMint, SPL_TOKEN_PROGRAM_ID);
1640
+ const depositorNo = getAta(depositor, noMint, SPL_TOKEN_PROGRAM_ID);
1641
+ const disc = await anchorDisc('mint_shares');
1642
+ const data = Buffer.alloc(16);
1643
+ disc.copy(data, 0);
1644
+ data.writeBigUInt64LE(BigInt(amount), 8);
1645
+ return new TransactionInstruction({
1646
+ programId,
1647
+ keys: [
1648
+ { pubkey: depositor, isSigner: true, isWritable: true },
1649
+ { pubkey: protocolState, isSigner: false, isWritable: false },
1650
+ { pubkey: marketState, isSigner: false, isWritable: true },
1651
+ { pubkey: ccmMint, isSigner: false, isWritable: false },
1652
+ { pubkey: depositorCcm, isSigner: false, isWritable: true },
1653
+ { pubkey: vault, isSigner: false, isWritable: true },
1654
+ { pubkey: yesMint, isSigner: false, isWritable: true },
1655
+ { pubkey: noMint, isSigner: false, isWritable: true },
1656
+ { pubkey: depositorYes, isSigner: false, isWritable: true },
1657
+ { pubkey: depositorNo, isSigner: false, isWritable: true },
1658
+ { pubkey: mintAuthority, isSigner: false, isWritable: false },
1659
+ { pubkey: TOKEN_2022_PROGRAM_ID, isSigner: false, isWritable: false },
1660
+ { pubkey: SPL_TOKEN_PROGRAM_ID, isSigner: false, isWritable: false },
1661
+ ],
1662
+ data,
1663
+ });
1664
+ }
1665
+ /**
1666
+ * Build a `redeem_shares` TransactionInstruction (prediction markets).
1667
+ *
1668
+ * Burns equal YES + NO shares and returns CCM. Pre-resolution only.
1669
+ *
1670
+ * Accounts (13 fixed, order matches markets.rs redeem_shares):
1671
+ * 0. redeemer (signer, writable)
1672
+ * 1. protocol_state (readonly)
1673
+ * 2. market_state (writable)
1674
+ * 3. ccm_mint (readonly)
1675
+ * 4. vault (writable)
1676
+ * 5. yes_mint (writable)
1677
+ * 6. no_mint (writable)
1678
+ * 7. redeemer_yes_ata (writable)
1679
+ * 8. redeemer_no_ata (writable)
1680
+ * 9. redeemer_ccm_ata (writable)
1681
+ * 10. mint_authority (readonly)
1682
+ * 11. token_program (Token-2022, for CCM)
1683
+ * 12. outcome_token_prog (SPL Token, for YES/NO)
1684
+ *
1685
+ * Args: shares (u64) — number of YES+NO pairs to redeem
1686
+ */
1687
+ export async function createRedeemSharesIx(redeemer, ccmMint, marketId, shares, programId = PROGRAM_ID) {
1688
+ const protocolState = getProtocolStatePDA(programId);
1689
+ const marketState = getMarketStatePDA(ccmMint, marketId, programId);
1690
+ const vault = getPredictionVaultPDA(ccmMint, marketId, programId);
1691
+ const yesMint = getPredictionYesMintPDA(ccmMint, marketId, programId);
1692
+ const noMint = getPredictionNoMintPDA(ccmMint, marketId, programId);
1693
+ const mintAuthority = getPredictionMintAuthorityPDA(ccmMint, marketId, programId);
1694
+ const redeemerYes = getAta(redeemer, yesMint, SPL_TOKEN_PROGRAM_ID);
1695
+ const redeemerNo = getAta(redeemer, noMint, SPL_TOKEN_PROGRAM_ID);
1696
+ const redeemerCcm = getAta(redeemer, ccmMint, TOKEN_2022_PROGRAM_ID);
1697
+ const disc = await anchorDisc('redeem_shares');
1698
+ const data = Buffer.alloc(16);
1699
+ disc.copy(data, 0);
1700
+ data.writeBigUInt64LE(BigInt(shares), 8);
1701
+ return new TransactionInstruction({
1702
+ programId,
1703
+ keys: [
1704
+ { pubkey: redeemer, isSigner: true, isWritable: true },
1705
+ { pubkey: protocolState, isSigner: false, isWritable: false },
1706
+ { pubkey: marketState, isSigner: false, isWritable: true },
1707
+ { pubkey: ccmMint, isSigner: false, isWritable: false },
1708
+ { pubkey: vault, isSigner: false, isWritable: true },
1709
+ { pubkey: yesMint, isSigner: false, isWritable: true },
1710
+ { pubkey: noMint, isSigner: false, isWritable: true },
1711
+ { pubkey: redeemerYes, isSigner: false, isWritable: true },
1712
+ { pubkey: redeemerNo, isSigner: false, isWritable: true },
1713
+ { pubkey: redeemerCcm, isSigner: false, isWritable: true },
1714
+ { pubkey: mintAuthority, isSigner: false, isWritable: false },
1715
+ { pubkey: TOKEN_2022_PROGRAM_ID, isSigner: false, isWritable: false },
1716
+ { pubkey: SPL_TOKEN_PROGRAM_ID, isSigner: false, isWritable: false },
1717
+ ],
1718
+ data,
1719
+ });
1720
+ }
1721
+ /**
1722
+ * Build a `settle` TransactionInstruction (prediction markets).
1723
+ *
1724
+ * Burns winning-side tokens and returns CCM 1:1. Post-resolution only.
1725
+ *
1726
+ * Accounts (11 fixed, order matches markets.rs settle):
1727
+ * 0. settler (signer, writable)
1728
+ * 1. protocol_state (readonly)
1729
+ * 2. market_state (readonly)
1730
+ * 3. ccm_mint (readonly)
1731
+ * 4. vault (writable)
1732
+ * 5. winning_mint (writable) — yes_mint if YES won, no_mint if NO
1733
+ * 6. settler_winning (writable) — settler's ATA for the winning mint
1734
+ * 7. settler_ccm (writable) — settler's CCM ATA
1735
+ * 8. mint_authority (readonly)
1736
+ * 9. token_program (Token-2022, for CCM)
1737
+ * 10. outcome_token_prog (SPL Token, for YES/NO)
1738
+ *
1739
+ * Args: shares (u64) — winning tokens to settle
1740
+ */
1741
+ export async function createSettlePredictionIx(settler, ccmMint, marketId, shares,
1742
+ /** true = YES won, false = NO won */
1743
+ yesOutcome, programId = PROGRAM_ID) {
1744
+ const protocolState = getProtocolStatePDA(programId);
1745
+ const marketState = getMarketStatePDA(ccmMint, marketId, programId);
1746
+ const vault = getPredictionVaultPDA(ccmMint, marketId, programId);
1747
+ const yesMint = getPredictionYesMintPDA(ccmMint, marketId, programId);
1748
+ const noMint = getPredictionNoMintPDA(ccmMint, marketId, programId);
1749
+ const mintAuthority = getPredictionMintAuthorityPDA(ccmMint, marketId, programId);
1750
+ const winningMint = yesOutcome ? yesMint : noMint;
1751
+ const settlerWinning = getAta(settler, winningMint, SPL_TOKEN_PROGRAM_ID);
1752
+ const settlerCcm = getAta(settler, ccmMint, TOKEN_2022_PROGRAM_ID);
1753
+ const disc = await anchorDisc('settle');
1754
+ const data = Buffer.alloc(16);
1755
+ disc.copy(data, 0);
1756
+ data.writeBigUInt64LE(BigInt(shares), 8);
1757
+ return new TransactionInstruction({
1758
+ programId,
1759
+ keys: [
1760
+ { pubkey: settler, isSigner: true, isWritable: true },
1761
+ { pubkey: protocolState, isSigner: false, isWritable: false },
1762
+ { pubkey: marketState, isSigner: false, isWritable: false },
1763
+ { pubkey: ccmMint, isSigner: false, isWritable: false },
1764
+ { pubkey: vault, isSigner: false, isWritable: true },
1765
+ { pubkey: winningMint, isSigner: false, isWritable: true },
1766
+ { pubkey: settlerWinning, isSigner: false, isWritable: true },
1767
+ { pubkey: settlerCcm, isSigner: false, isWritable: true },
1768
+ { pubkey: mintAuthority, isSigner: false, isWritable: false },
1769
+ { pubkey: TOKEN_2022_PROGRAM_ID, isSigner: false, isWritable: false },
1770
+ { pubkey: SPL_TOKEN_PROGRAM_ID, isSigner: false, isWritable: false },
1771
+ ],
1772
+ data,
1773
+ });
1774
+ }
@@ -302,14 +302,14 @@ describe('parseUserMarketPosition', () => {
302
302
  expect(parseUserMarketPosition(buf)).toBeNull();
303
303
  });
304
304
  it('reads settled flag correctly', () => {
305
- const buf = Buffer.alloc(8 + 100);
305
+ const buf = Buffer.alloc(114);
306
306
  buf[8 + 89] = 1; // settled
307
307
  const pos = parseUserMarketPosition(buf);
308
308
  expect(pos).not.toBeNull();
309
309
  expect(pos.settled).toBe(true);
310
310
  });
311
311
  it('reads depositedAmount as u64 LE', () => {
312
- const buf = Buffer.alloc(8 + 100);
312
+ const buf = Buffer.alloc(114);
313
313
  buf.writeBigUInt64LE(1000000n, 8 + 1 + 32 + 32); // depositedAmount at offset 73 from disc
314
314
  const pos = parseUserMarketPosition(buf);
315
315
  expect(pos).not.toBeNull();
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Model Selector — pick the best open-source model using live WZRD velocity data.
3
+ *
4
+ * Combines on-chain attention signals from the WZRD protocol with
5
+ * OpenRouter pricing to compute a value score for every tracked model,
6
+ * then returns a ranked list filtered by budget, task, and trend.
7
+ *
8
+ * Usage:
9
+ * import { bestModel } from '@wzrd_sol/sdk';
10
+ * const picks = await bestModel({ task: 'code', budget: 'micro' });
11
+ * console.log(picks[0].model_id);
12
+ *
13
+ * @module model-selector
14
+ */
15
+ /** Budget tiers controlling the maximum blended price per million tokens. */
16
+ export type BudgetTier = 'micro' | 'budget' | 'mid' | 'premium';
17
+ /** Supported task types. The selector applies a task-specific boost. */
18
+ export type TaskType = 'chat' | 'code' | 'reasoning';
19
+ /** Velocity trend labels returned by the WZRD momentum API. */
20
+ export type VelocityTrend = 'surging' | 'accelerating' | 'stable' | 'insufficient_history' | 'decelerating' | 'cooling';
21
+ /** Confidence level from the WZRD momentum API. */
22
+ export type Confidence = 'high' | 'medium' | 'low' | 'insufficient';
23
+ /** A single model recommendation returned by the selector. */
24
+ export interface ModelRecommendation {
25
+ /** Full model identifier (e.g. "google/gemma-3-27b-it"). */
26
+ model_id: string;
27
+ /** Routing provider — currently always "openrouter". */
28
+ provider: string;
29
+ /** Blended price per million tokens in USD (3:1 prompt:completion weighting). */
30
+ price_per_m_tokens: number;
31
+ /** Exponential moving average of the model's velocity from WZRD. */
32
+ velocity_ema: number;
33
+ /** Composite value score: velocity_ema / (blended_price + epsilon). Higher is better. */
34
+ value_score: number;
35
+ /** Current velocity trend from the WZRD momentum signal. */
36
+ trend: VelocityTrend;
37
+ /** Confidence level of the momentum signal. */
38
+ confidence: Confidence;
39
+ }
40
+ /** Options for filtering and ranking models. */
41
+ export interface ModelSelectorOptions {
42
+ /** Maximum price tier. Default: "mid". */
43
+ budget?: BudgetTier;
44
+ /** Task type — applies a relevance boost to known-good models. Default: "chat". */
45
+ task?: TaskType;
46
+ /** Minimum confidence level from the momentum signal. Default: "low". */
47
+ min_confidence?: Confidence;
48
+ /** Maximum number of results to return. Default: 5. */
49
+ limit?: number;
50
+ /** Model ID substrings to exclude (case-insensitive). */
51
+ exclude?: string[];
52
+ }
53
+ /** Configuration for the ModelSelector class. */
54
+ export interface ModelSelectorConfig {
55
+ /** WZRD API base URL. Default: "https://api.twzrd.xyz". */
56
+ wzrd_base_url?: string;
57
+ /** OpenRouter catalog URL. Default: "https://openrouter.ai/api/v1/models". */
58
+ openrouter_url?: string;
59
+ /** Cache TTL in milliseconds. Default: 300_000 (5 minutes). */
60
+ cache_ttl_ms?: number;
61
+ }
62
+ /**
63
+ * Fetches WZRD velocity data and OpenRouter pricing, caches results,
64
+ * and scores models by value (velocity / cost).
65
+ *
66
+ * Create one instance and reuse it — the internal cache avoids redundant
67
+ * HTTP requests within the TTL window.
68
+ *
69
+ * @example
70
+ * ```ts
71
+ * const selector = new ModelSelector({ wzrd_base_url: 'https://api.twzrd.xyz' });
72
+ * const picks = await selector.select({ task: 'code', budget: 'budget', limit: 3 });
73
+ * console.log(picks[0].model_id);
74
+ * ```
75
+ */
76
+ export declare class ModelSelector {
77
+ private readonly wzrdBaseUrl;
78
+ private readonly openrouterUrl;
79
+ private readonly cacheTtlMs;
80
+ private cache;
81
+ constructor(config?: ModelSelectorConfig);
82
+ /**
83
+ * Select the best models for a given task and budget.
84
+ *
85
+ * Fetches live data from the WZRD leaderboard, momentum signal, and
86
+ * OpenRouter catalog (all cached for 5 minutes by default), then ranks
87
+ * every OpenRouter model tracked by WZRD using:
88
+ *
89
+ * value_score = velocity_ema / (blended_price_per_m_tokens + epsilon)
90
+ *
91
+ * Results are filtered by budget tier, task relevance, minimum confidence,
92
+ * and exclusion list, then sorted by value_score descending.
93
+ *
94
+ * @param options - Filtering and ranking options.
95
+ * @returns Ranked array of model recommendations (may be empty).
96
+ */
97
+ select(options?: ModelSelectorOptions): Promise<ModelRecommendation[]>;
98
+ /**
99
+ * Invalidate the in-memory cache.
100
+ *
101
+ * Useful when you know upstream data has changed and want
102
+ * fresh results on the next `select()` call.
103
+ */
104
+ clearCache(): void;
105
+ private fetchLeaderboard;
106
+ private fetchMomentum;
107
+ private fetchOpenRouterCatalog;
108
+ /**
109
+ * Fetch JSON from a URL with in-memory TTL caching.
110
+ * Returns null on any error (network, HTTP status, parse failure).
111
+ */
112
+ private cachedFetch;
113
+ }
114
+ /**
115
+ * Pick the best open-source model for a task using live WZRD velocity data.
116
+ *
117
+ * This is a convenience wrapper around {@link ModelSelector.select} that
118
+ * uses a module-level singleton with default configuration. For custom
119
+ * base URLs or cache settings, instantiate {@link ModelSelector} directly.
120
+ *
121
+ * @param options - Filtering and ranking options.
122
+ * @returns Ranked array of model recommendations (may be empty if the API is unreachable).
123
+ *
124
+ * @example
125
+ * ```ts
126
+ * import { bestModel } from '@wzrd_sol/sdk';
127
+ *
128
+ * // Cheapest model good for code
129
+ * const picks = await bestModel({ task: 'code', budget: 'micro' });
130
+ * console.log(picks[0].model_id);
131
+ *
132
+ * // Premium reasoning model, exclude specific providers
133
+ * const reasoning = await bestModel({
134
+ * task: 'reasoning',
135
+ * budget: 'premium',
136
+ * exclude: ['gpt'],
137
+ * limit: 3,
138
+ * });
139
+ * ```
140
+ */
141
+ export declare function bestModel(options?: ModelSelectorOptions): Promise<ModelRecommendation[]>;
@@ -0,0 +1,247 @@
1
+ /**
2
+ * Model Selector — pick the best open-source model using live WZRD velocity data.
3
+ *
4
+ * Combines on-chain attention signals from the WZRD protocol with
5
+ * OpenRouter pricing to compute a value score for every tracked model,
6
+ * then returns a ranked list filtered by budget, task, and trend.
7
+ *
8
+ * Usage:
9
+ * import { bestModel } from '@wzrd_sol/sdk';
10
+ * const picks = await bestModel({ task: 'code', budget: 'micro' });
11
+ * console.log(picks[0].model_id);
12
+ *
13
+ * @module model-selector
14
+ */
15
+ // ── Constants ─────────────────────────────────────────────────────
16
+ const DEFAULT_WZRD_URL = 'https://api.twzrd.xyz';
17
+ const DEFAULT_OPENROUTER_URL = 'https://openrouter.ai/api/v1/models';
18
+ const DEFAULT_CACHE_TTL_MS = 300000; // 5 minutes
19
+ const EPSILON = 0.001;
20
+ /** Maximum blended price per million tokens for each budget tier. */
21
+ const BUDGET_LIMITS = {
22
+ micro: 0.20,
23
+ budget: 1.00,
24
+ mid: 5.00,
25
+ premium: Infinity,
26
+ };
27
+ /** Confidence levels ranked from lowest to highest. */
28
+ const CONFIDENCE_RANK = {
29
+ insufficient: 0,
30
+ low: 1,
31
+ medium: 2,
32
+ high: 3,
33
+ };
34
+ /** Task-specific model name patterns and their score boost multiplier. */
35
+ const TASK_BOOSTS = {
36
+ code: {
37
+ patterns: ['deepseek', 'qwen', 'codestral', 'starcoder', 'coder'],
38
+ boost: 1.3,
39
+ },
40
+ reasoning: {
41
+ patterns: ['deepseek-r', 'o1', 'o3', 'reasoning'],
42
+ boost: 1.5,
43
+ },
44
+ chat: {
45
+ patterns: ['claude', 'gpt', 'gemini', 'chat', 'llama'],
46
+ boost: 1.1,
47
+ },
48
+ };
49
+ // ── ModelSelector Class ───────────────────────────────────────────
50
+ /**
51
+ * Fetches WZRD velocity data and OpenRouter pricing, caches results,
52
+ * and scores models by value (velocity / cost).
53
+ *
54
+ * Create one instance and reuse it — the internal cache avoids redundant
55
+ * HTTP requests within the TTL window.
56
+ *
57
+ * @example
58
+ * ```ts
59
+ * const selector = new ModelSelector({ wzrd_base_url: 'https://api.twzrd.xyz' });
60
+ * const picks = await selector.select({ task: 'code', budget: 'budget', limit: 3 });
61
+ * console.log(picks[0].model_id);
62
+ * ```
63
+ */
64
+ export class ModelSelector {
65
+ constructor(config = {}) {
66
+ this.cache = new Map();
67
+ this.wzrdBaseUrl = (config.wzrd_base_url ?? DEFAULT_WZRD_URL).replace(/\/+$/, '');
68
+ this.openrouterUrl = config.openrouter_url ?? DEFAULT_OPENROUTER_URL;
69
+ this.cacheTtlMs = config.cache_ttl_ms ?? DEFAULT_CACHE_TTL_MS;
70
+ }
71
+ /**
72
+ * Select the best models for a given task and budget.
73
+ *
74
+ * Fetches live data from the WZRD leaderboard, momentum signal, and
75
+ * OpenRouter catalog (all cached for 5 minutes by default), then ranks
76
+ * every OpenRouter model tracked by WZRD using:
77
+ *
78
+ * value_score = velocity_ema / (blended_price_per_m_tokens + epsilon)
79
+ *
80
+ * Results are filtered by budget tier, task relevance, minimum confidence,
81
+ * and exclusion list, then sorted by value_score descending.
82
+ *
83
+ * @param options - Filtering and ranking options.
84
+ * @returns Ranked array of model recommendations (may be empty).
85
+ */
86
+ async select(options = {}) {
87
+ const { budget = 'mid', task = 'chat', min_confidence = 'low', limit = 5, exclude = [], } = options;
88
+ // Fetch all three data sources in parallel (cached).
89
+ const [leaderboard, momentum, catalog] = await Promise.all([
90
+ this.fetchLeaderboard(),
91
+ this.fetchMomentum(),
92
+ this.fetchOpenRouterCatalog(),
93
+ ]);
94
+ // If any critical source failed, return empty (graceful degradation).
95
+ if (leaderboard === null || catalog === null) {
96
+ return [];
97
+ }
98
+ // Build lookup indices.
99
+ const orIndex = new Map();
100
+ for (const m of catalog.data) {
101
+ orIndex.set(m.id, m);
102
+ }
103
+ const momentumIndex = new Map();
104
+ if (momentum !== null) {
105
+ for (const m of momentum.models) {
106
+ momentumIndex.set(m.market_id, m);
107
+ }
108
+ }
109
+ // Filtering thresholds.
110
+ const maxPrice = BUDGET_LIMITS[budget];
111
+ const minConfidenceRank = CONFIDENCE_RANK[min_confidence];
112
+ const taskBoost = TASK_BOOSTS[task];
113
+ const excludeLower = exclude.map((e) => e.toLowerCase());
114
+ const candidates = [];
115
+ for (const market of leaderboard.markets) {
116
+ if (market.platform !== 'openrouter')
117
+ continue;
118
+ if (market.status !== 'open')
119
+ continue;
120
+ const orModel = orIndex.get(market.channel_id);
121
+ if (!orModel)
122
+ continue;
123
+ // Exclusion filter.
124
+ if (excludeLower.some((e) => market.channel_id.toLowerCase().includes(e)))
125
+ continue;
126
+ // Compute blended price per million tokens (3:1 prompt:completion weighting).
127
+ const promptPrice = parseFloat(orModel.pricing?.prompt ?? '0');
128
+ const completionPrice = parseFloat(orModel.pricing?.completion ?? '0');
129
+ const blendedPerM = ((promptPrice * 3 + completionPrice) / 4) * 1000000;
130
+ // Budget filter.
131
+ if (blendedPerM > maxPrice)
132
+ continue;
133
+ // Momentum data.
134
+ const mom = momentumIndex.get(market.market_id);
135
+ const trend = mom?.velocity_trend ?? 'insufficient_history';
136
+ const confidence = mom?.history_confidence ?? 'insufficient';
137
+ // Confidence filter.
138
+ const confidenceRank = CONFIDENCE_RANK[confidence];
139
+ if (confidenceRank < minConfidenceRank)
140
+ continue;
141
+ // Compute value score: velocity / (price + epsilon).
142
+ const velocityEma = market.velocity_ema;
143
+ const rawValueScore = velocityEma / (blendedPerM + EPSILON);
144
+ // Apply task-specific boost.
145
+ let taskMultiplier = 1.0;
146
+ if (taskBoost) {
147
+ for (const pattern of taskBoost.patterns) {
148
+ if (market.channel_id.toLowerCase().includes(pattern)) {
149
+ taskMultiplier = taskBoost.boost;
150
+ break;
151
+ }
152
+ }
153
+ }
154
+ const valueScore = rawValueScore * taskMultiplier;
155
+ candidates.push({
156
+ model_id: market.channel_id,
157
+ provider: 'openrouter',
158
+ price_per_m_tokens: Math.round(blendedPerM * 1000) / 1000,
159
+ velocity_ema: velocityEma,
160
+ value_score: Math.round(valueScore * 1000) / 1000,
161
+ trend,
162
+ confidence,
163
+ });
164
+ }
165
+ // Sort by value_score descending.
166
+ candidates.sort((a, b) => b.value_score - a.value_score);
167
+ return candidates.slice(0, limit);
168
+ }
169
+ /**
170
+ * Invalidate the in-memory cache.
171
+ *
172
+ * Useful when you know upstream data has changed and want
173
+ * fresh results on the next `select()` call.
174
+ */
175
+ clearCache() {
176
+ this.cache.clear();
177
+ }
178
+ // ── Private Helpers ───────────────────────────────────────────
179
+ async fetchLeaderboard() {
180
+ return this.cachedFetch('wzrd:leaderboard', `${this.wzrdBaseUrl}/v1/leaderboard?limit=100`);
181
+ }
182
+ async fetchMomentum() {
183
+ return this.cachedFetch('wzrd:momentum', `${this.wzrdBaseUrl}/v1/signals/momentum`);
184
+ }
185
+ async fetchOpenRouterCatalog() {
186
+ return this.cachedFetch('openrouter:catalog', this.openrouterUrl);
187
+ }
188
+ /**
189
+ * Fetch JSON from a URL with in-memory TTL caching.
190
+ * Returns null on any error (network, HTTP status, parse failure).
191
+ */
192
+ async cachedFetch(key, url) {
193
+ const now = Date.now();
194
+ const cached = this.cache.get(key);
195
+ if (cached && cached.expires_at > now) {
196
+ return cached.data;
197
+ }
198
+ try {
199
+ const resp = await fetch(url);
200
+ if (!resp.ok) {
201
+ return null;
202
+ }
203
+ const data = (await resp.json());
204
+ this.cache.set(key, { data, expires_at: now + this.cacheTtlMs });
205
+ return data;
206
+ }
207
+ catch {
208
+ return null;
209
+ }
210
+ }
211
+ }
212
+ // ── Convenience Function ──────────────────────────────────────────
213
+ /** Shared singleton used by the convenience `bestModel()` function. */
214
+ let defaultSelector = null;
215
+ /**
216
+ * Pick the best open-source model for a task using live WZRD velocity data.
217
+ *
218
+ * This is a convenience wrapper around {@link ModelSelector.select} that
219
+ * uses a module-level singleton with default configuration. For custom
220
+ * base URLs or cache settings, instantiate {@link ModelSelector} directly.
221
+ *
222
+ * @param options - Filtering and ranking options.
223
+ * @returns Ranked array of model recommendations (may be empty if the API is unreachable).
224
+ *
225
+ * @example
226
+ * ```ts
227
+ * import { bestModel } from '@wzrd_sol/sdk';
228
+ *
229
+ * // Cheapest model good for code
230
+ * const picks = await bestModel({ task: 'code', budget: 'micro' });
231
+ * console.log(picks[0].model_id);
232
+ *
233
+ * // Premium reasoning model, exclude specific providers
234
+ * const reasoning = await bestModel({
235
+ * task: 'reasoning',
236
+ * budget: 'premium',
237
+ * exclude: ['gpt'],
238
+ * limit: 3,
239
+ * });
240
+ * ```
241
+ */
242
+ export async function bestModel(options = {}) {
243
+ if (!defaultSelector) {
244
+ defaultSelector = new ModelSelector();
245
+ }
246
+ return defaultSelector.select(options);
247
+ }
package/dist/pda.d.ts CHANGED
@@ -16,5 +16,9 @@ export declare function getGlobalRootConfigPDA(ccmMint: PublicKey, programId?: P
16
16
  export declare function getClaimStatePDA(ccmMint: PublicKey, claimer: PublicKey, programId?: PublicKey): PublicKey;
17
17
  /** Derive a ChannelConfigV2 PDA for a given mint and subject. */
18
18
  export declare function getChannelConfigV2PDA(mint: PublicKey, subject: PublicKey, programId?: PublicKey): PublicKey;
19
+ /** Derive the StreamRootConfig PDA for a given vLOFI mint. */
20
+ export declare function getStreamRootConfigPDA(vlofiMint: PublicKey, programId?: PublicKey): PublicKey;
21
+ /** Derive the per-user ClaimStateStream PDA. */
22
+ export declare function getClaimStateStreamPDA(vlofiMint: PublicKey, claimer: PublicKey, programId?: PublicKey): PublicKey;
19
23
  /** Derive an Associated Token Account address (works for both SPL and Token-2022). */
20
24
  export declare function getAta(owner: PublicKey, mint: PublicKey, tokenProgramId: PublicKey): PublicKey;
package/dist/pda.js CHANGED
@@ -4,7 +4,7 @@
4
4
  * Seeds must match programs/attention-oracle/src/constants.rs.
5
5
  */
6
6
  import { PublicKey } from '@solana/web3.js';
7
- import { PROGRAM_ID, ASSOCIATED_TOKEN_PROGRAM_ID, PROTOCOL_STATE_SEED, MARKET_VAULT_SEED, MARKET_POSITION_SEED, GLOBAL_ROOT_SEED, CLAIM_STATE_GLOBAL_SEED, CHANNEL_CONFIG_V2_SEED, } from './constants.js';
7
+ import { PROGRAM_ID, ASSOCIATED_TOKEN_PROGRAM_ID, PROTOCOL_STATE_SEED, MARKET_VAULT_SEED, MARKET_POSITION_SEED, GLOBAL_ROOT_SEED, CLAIM_STATE_GLOBAL_SEED, CHANNEL_CONFIG_V2_SEED, STREAM_ROOT_SEED, CLAIM_STATE_STREAM_SEED, } from './constants.js';
8
8
  /** Derive the singleton ProtocolState PDA. */
9
9
  export function getProtocolStatePDA(programId = PROGRAM_ID) {
10
10
  return PublicKey.findProgramAddressSync([Buffer.from(PROTOCOL_STATE_SEED)], programId)[0];
@@ -31,6 +31,15 @@ export function getClaimStatePDA(ccmMint, claimer, programId = PROGRAM_ID) {
31
31
  export function getChannelConfigV2PDA(mint, subject, programId = PROGRAM_ID) {
32
32
  return PublicKey.findProgramAddressSync([Buffer.from(CHANNEL_CONFIG_V2_SEED), mint.toBuffer(), subject.toBuffer()], programId)[0];
33
33
  }
34
+ // ── Stream (vLOFI distribution) PDA Derivation ──────────
35
+ /** Derive the StreamRootConfig PDA for a given vLOFI mint. */
36
+ export function getStreamRootConfigPDA(vlofiMint, programId = PROGRAM_ID) {
37
+ return PublicKey.findProgramAddressSync([Buffer.from(STREAM_ROOT_SEED), vlofiMint.toBuffer()], programId)[0];
38
+ }
39
+ /** Derive the per-user ClaimStateStream PDA. */
40
+ export function getClaimStateStreamPDA(vlofiMint, claimer, programId = PROGRAM_ID) {
41
+ return PublicKey.findProgramAddressSync([Buffer.from(CLAIM_STATE_STREAM_SEED), vlofiMint.toBuffer(), claimer.toBuffer()], programId)[0];
42
+ }
34
43
  /** Derive an Associated Token Account address (works for both SPL and Token-2022). */
35
44
  export function getAta(owner, mint, tokenProgramId) {
36
45
  return PublicKey.findProgramAddressSync([owner.toBuffer(), tokenProgramId.toBuffer(), mint.toBuffer()], ASSOCIATED_TOKEN_PROGRAM_ID)[0];