openbroker 1.1.1 → 1.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.
package/README.md CHANGED
@@ -106,36 +106,38 @@ openbroker markets --sort change --top 10 # Top movers
106
106
 
107
107
  #### `all-markets` — All Markets
108
108
 
109
- Browse all available markets across main perps, HIP-3 perps, and spot — grouped by type.
109
+ Browse all available markets across main perps, HIP-3 perps, spot, and HIP-4 outcomes — grouped by type.
110
110
 
111
111
  ```bash
112
112
  openbroker all-markets # Everything
113
113
  openbroker all-markets --type perp # Main perps only
114
114
  openbroker all-markets --type hip3 # HIP-3 perps only
115
115
  openbroker all-markets --type spot # Spot only
116
+ openbroker all-markets --type outcome # HIP-4 outcomes only
116
117
  openbroker all-markets --top 20 # Top 20 by volume
117
118
  ```
118
119
 
119
120
  | Flag | Description | Default |
120
121
  |------|-------------|---------|
121
- | `--type` | Filter: `perp`, `spot`, `hip3`, or `all` | `all` |
122
+ | `--type` | Filter: `perp`, `spot`, `hip3`, `outcome`, or `all` | `all` |
122
123
  | `--top` | Limit to top N by volume | — |
123
124
  | `--verbose` | Show detailed output | — |
124
125
 
125
126
  #### `search` — Search Markets
126
127
 
127
- Search for assets by name across all providers (perps, HIP-3, spot). Shows funding comparison when an asset is listed on multiple venues.
128
+ Search for assets by name across all providers (perps, HIP-3, spot, HIP-4 outcomes). Shows funding comparison when an asset is listed on multiple venues.
128
129
 
129
130
  ```bash
130
131
  openbroker search --query GOLD # Find all GOLD markets
131
132
  openbroker search --query ETH --type perp # ETH perps only
132
133
  openbroker search --query PURR --type spot # PURR spot only
134
+ openbroker search --query BTC --type outcome # HIP-4 outcomes only
133
135
  ```
134
136
 
135
137
  | Flag | Description | Default |
136
138
  |------|-------------|---------|
137
139
  | `--query` | Search term (matches coin name) | **required** |
138
- | `--type` | Filter: `perp`, `spot`, `hip3`, or `all` | `all` |
140
+ | `--type` | Filter: `perp`, `spot`, `hip3`, `outcome`, or `all` | `all` |
139
141
  | `--verbose` | Show detailed output | — |
140
142
 
141
143
  #### `spot` — Spot Markets & Balances
@@ -154,6 +156,26 @@ openbroker spot --top 20 # Top 20 by volume
154
156
  | `--top` | Limit to top N by volume | — |
155
157
  | `--verbose` | Show token metadata | — |
156
158
 
159
+ #### `outcomes` — HIP-4 Outcome Markets
160
+
161
+ Search and inspect prediction/outcome markets. Outcome sides use Hyperliquid's encoded spot-like coin form: `#<encoding>`, where `encoding = 10 * outcomeId + side`; side `0` is usually YES and side `1` is usually NO.
162
+
163
+ ```bash
164
+ openbroker outcomes --query BTC
165
+ openbroker outcomes --outcome 123
166
+ openbroker outcomes --outcome 123 --side yes --json
167
+ openbroker outcomes --balances
168
+ ```
169
+
170
+ | Flag | Description | Default |
171
+ |------|-------------|---------|
172
+ | `--query` | Search market name, description, underlying, expiry, or target price | — |
173
+ | `--outcome` | Outcome id, `#<encoding>`, or `+<encoding>` | — |
174
+ | `--side` | Outcome side for plain ids: `yes`, `no`, `0`, or `1` | `yes` |
175
+ | `--balances` | Show outcome token balances | — |
176
+ | `--top` | Limit to top N matches | — |
177
+ | `--json` | Machine-readable output | — |
178
+
157
179
  #### `fills` — Trade Fill History
158
180
 
159
181
  View your trade executions with prices, fees, and realized PnL.
@@ -379,6 +401,30 @@ openbroker cancel --all --dry # Preview what would be cancelled
379
401
  | `--all` | Cancel all open orders | — |
380
402
  | `--dry` | Show orders without cancelling | — |
381
403
 
404
+ #### `outcome-buy` / `outcome-sell` / `outcome-order` — HIP-4 Outcome Orders
405
+
406
+ Buy or sell a YES/NO outcome token. Buying opens exposure to that side; selling reduces or closes that token balance. Market orders are IOC limits with slippage protection.
407
+
408
+ ```bash
409
+ openbroker outcomes --query BTC
410
+ openbroker outcome-buy --outcome 123 --outcome-side yes --size 10 --dry
411
+ openbroker outcome-buy --outcome 123 --outcome-side no --size 5 --price 0.42
412
+ openbroker outcome-sell --outcome #1230 --size 10
413
+ openbroker outcome-order --outcome 123 --outcome-side yes --side buy --size 10
414
+ ```
415
+
416
+ | Flag | Description | Default |
417
+ |------|-------------|---------|
418
+ | `--outcome` | Outcome id, `#<encoding>`, or `+<encoding>` | **required** |
419
+ | `--outcome-side` | `yes`, `no`, `0`, or `1` when using a plain outcome id | `yes` |
420
+ | `--side` | `buy` or `sell` (auto-set by shortcuts) | **required** |
421
+ | `--size` | Size in outcome token units | **required** |
422
+ | `--price` | Limit price between 0 and 1 (omit for market IOC) | market |
423
+ | `--tif` | Time in force for limit orders: `Gtc`, `Ioc`, `Alo` | `Gtc` |
424
+ | `--slippage` | Slippage tolerance in bps for market orders | `50` |
425
+ | `--sz-decimals` | Override size decimals if metadata omits token decimals | metadata / `0` |
426
+ | `--dry` | Preview without executing | — |
427
+
382
428
  ---
383
429
 
384
430
  ### Advanced Execution
@@ -742,8 +788,9 @@ When loaded, the plugin registers these agent tools:
742
788
  | Info | `ob_candles` | OHLCV candle data for an asset |
743
789
  | Info | `ob_trades` | Recent trades (tape) for an asset |
744
790
  | Info | `ob_markets` | Market data (price, volume, OI) |
745
- | Info | `ob_search` | Search assets across perps, HIP-3, and spot |
791
+ | Info | `ob_search` | Search assets across perps, HIP-3, spot, and HIP-4 outcomes |
746
792
  | Info | `ob_spot` | Spot markets and token balances |
793
+ | Info | `ob_outcomes` | HIP-4 outcome markets and outcome token balances |
747
794
  | Info | `ob_rate_limit` | API rate limit usage and capacity |
748
795
  | Trading | `ob_buy` | Market buy |
749
796
  | Trading | `ob_sell` | Market sell |
@@ -751,6 +798,8 @@ When loaded, the plugin registers these agent tools:
751
798
  | Trading | `ob_trigger` | Trigger order (TP/SL) |
752
799
  | Trading | `ob_tpsl` | Set TP/SL on existing position |
753
800
  | Trading | `ob_cancel` | Cancel orders |
801
+ | Trading | `ob_outcome_buy` | Buy a HIP-4 YES/NO outcome token |
802
+ | Trading | `ob_outcome_sell` | Sell or close a HIP-4 YES/NO outcome token |
754
803
  | Advanced | `ob_twap` | Native TWAP order (exchange-managed) |
755
804
  | Advanced | `ob_twap_cancel` | Cancel a running TWAP order |
756
805
  | Advanced | `ob_twap_status` | View TWAP order history/status |
package/bin/cli.ts CHANGED
@@ -20,7 +20,7 @@ const commands: Record<string, { script: string; description: string }> = {
20
20
  'positions': { script: 'info/positions.ts', description: 'View open positions' },
21
21
  'funding': { script: 'info/funding.ts', description: 'View funding rates' },
22
22
  'markets': { script: 'info/markets.ts', description: 'View market data' },
23
- 'all-markets': { script: 'info/all-markets.ts', description: 'View all markets (perps, HIP-3, spot)' },
23
+ 'all-markets': { script: 'info/all-markets.ts', description: 'View all markets (perps, HIP-3, spot, HIP-4)' },
24
24
  'search': { script: 'info/search-markets.ts', description: 'Search for assets across providers' },
25
25
  'spot': { script: 'info/spot.ts', description: 'View spot markets and balances' },
26
26
  'fills': { script: 'info/fills.ts', description: 'View trade fill history' },
@@ -32,6 +32,7 @@ const commands: Record<string, { script: string; description: string }> = {
32
32
  'trades': { script: 'info/trades.ts', description: 'View recent trades for an asset' },
33
33
  'rate-limit': { script: 'info/rate-limit.ts', description: 'View API rate limit status' },
34
34
  'funding-scan': { script: 'info/funding-scan.ts', description: 'Scan funding rates across all dexes' },
35
+ 'outcomes': { script: 'info/outcomes.ts', description: 'Search and inspect HIP-4 outcome markets' },
35
36
 
36
37
  // Operations
37
38
  'buy': { script: 'operations/market-order.ts', description: 'Market buy order' },
@@ -50,6 +51,11 @@ const commands: Record<string, { script: string; description: string }> = {
50
51
  'spot-buy': { script: 'operations/spot-order.ts', description: 'Spot buy order' },
51
52
  'spot-sell': { script: 'operations/spot-order.ts', description: 'Spot sell order' },
52
53
  'spot-order': { script: 'operations/spot-order.ts', description: 'Spot order (market or limit)' },
54
+ 'outcome-buy': { script: 'operations/outcome-order.ts', description: 'Buy a HIP-4 outcome token' },
55
+ 'outcome-sell': { script: 'operations/outcome-order.ts', description: 'Sell a HIP-4 outcome token' },
56
+ 'outcome-open': { script: 'operations/outcome-order.ts', description: 'Open a HIP-4 outcome position' },
57
+ 'outcome-close': { script: 'operations/outcome-order.ts', description: 'Close a HIP-4 outcome position' },
58
+ 'outcome-order': { script: 'operations/outcome-order.ts', description: 'HIP-4 outcome order (market or limit)' },
53
59
 
54
60
  // Automations
55
61
  'auto': { script: 'auto/cli.ts', description: 'Run/manage trading automations' },
@@ -76,11 +82,12 @@ Info Commands:
76
82
  candles View OHLCV candle data
77
83
  trades View recent trades (tape) for an asset
78
84
  markets View market data for main perps
79
- all-markets View all markets (perps, HIP-3, spot)
85
+ all-markets View all markets (perps, HIP-3, spot, HIP-4)
80
86
  search Search for assets across all providers
81
87
  spot View spot markets and balances
82
88
  rate-limit View API rate limit status
83
89
  funding-scan Scan funding rates across all dexes (main + HIP-3)
90
+ outcomes Search and inspect HIP-4 outcome markets
84
91
 
85
92
  Trading Commands:
86
93
  buy Market buy order
@@ -96,6 +103,13 @@ Spot Trading:
96
103
  spot-sell Spot sell order
97
104
  spot-order Spot order (market or limit, specify --side)
98
105
 
106
+ HIP-4 Outcome Trading:
107
+ outcome-buy Buy a YES/NO outcome token
108
+ outcome-sell Sell a YES/NO outcome token
109
+ outcome-open Alias for outcome-buy
110
+ outcome-close Alias for outcome-sell
111
+ outcome-order Outcome order (market or limit, specify --side)
112
+
99
113
  Advanced Execution:
100
114
  twap Native TWAP order (exchange-managed)
101
115
  twap-cancel Cancel a running TWAP order
@@ -127,6 +141,8 @@ Examples:
127
141
  openbroker buy --coin ETH --size 0.1 # Market buy 0.1 ETH
128
142
  openbroker limit --coin BTC --side buy --size 0.01 --price 60000
129
143
  openbroker search --query GOLD # Find GOLD across providers
144
+ openbroker outcomes --query BTC # Find HIP-4 outcome markets
145
+ openbroker outcome-buy --outcome 123 --outcome-side yes --size 10 --dry
130
146
  openbroker tpsl --coin HYPE --tp 40 --sl 30 # Set TP/SL on position
131
147
 
132
148
  Documentation: https://github.com/aurracloud/open-broker
@@ -204,6 +220,14 @@ function main() {
204
220
  runScript(commands['spot-order'].script, ['--side', 'sell', ...commandArgs]);
205
221
  return;
206
222
  }
223
+ if (command === 'outcome-buy' || command === 'outcome-open') {
224
+ runScript(commands['outcome-order'].script, ['--side', 'buy', ...commandArgs]);
225
+ return;
226
+ }
227
+ if (command === 'outcome-sell' || command === 'outcome-close') {
228
+ runScript(commands['outcome-order'].script, ['--side', 'sell', ...commandArgs]);
229
+ return;
230
+ }
207
231
 
208
232
  // Handle version
209
233
  if (command === '--version' || command === '-v') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openbroker",
3
- "version": "1.1.1",
3
+ "version": "1.2.0",
4
4
  "description": "Hyperliquid trading CLI - execute orders, manage positions, and run trading strategies",
5
5
  "type": "module",
6
6
  "bin": {
@@ -30,6 +30,7 @@
30
30
  "search-markets": "tsx scripts/info/search-markets.ts",
31
31
  "spot": "tsx scripts/info/spot.ts",
32
32
  "market-order": "tsx scripts/operations/market-order.ts",
33
+ "outcome-order": "tsx scripts/operations/outcome-order.ts",
33
34
  "limit-order": "tsx scripts/operations/limit-order.ts",
34
35
  "trigger-order": "tsx scripts/operations/trigger-order.ts",
35
36
  "set-tpsl": "tsx scripts/operations/set-tpsl.ts",
@@ -41,6 +42,7 @@
41
42
  "bracket": "tsx scripts/operations/bracket.ts",
42
43
  "chase": "tsx scripts/operations/chase.ts",
43
44
  "funding-scan": "tsx scripts/info/funding-scan.ts",
45
+ "outcomes": "tsx scripts/info/outcomes.ts",
44
46
  "prepublishOnly": "npm run test:cli",
45
47
  "test:cli": "node --import tsx bin/cli.ts --help"
46
48
  },
@@ -53,12 +53,21 @@ async function loadConventionObservers(log: AutomationLogger): Promise<Automatio
53
53
  const observer = typeof exported === 'function' ? exported() : exported;
54
54
  if (observer && typeof observer === 'object') {
55
55
  observers.push(observer as AutomationAuditObserver);
56
- log.debug(`Loaded audit observer: ${name}`);
56
+ log.info(`[audit-observer] loaded: ${name}`);
57
+ } else {
58
+ // Package resolved but its factory returned null/undefined — typically
59
+ // because required env (e.g. OB_DASHBOARD_URL) is missing. Surface this
60
+ // so operators don't silently lose telemetry.
61
+ log.warn(
62
+ `[audit-observer] ${name} resolved but factory returned no observer — check the package's required env vars`
63
+ );
57
64
  }
58
65
  } catch (err) {
59
66
  const code = (err as NodeJS.ErrnoException | undefined)?.code;
60
- if (code !== 'ERR_MODULE_NOT_FOUND' && code !== 'MODULE_NOT_FOUND') {
61
- log.warn(`Failed to load audit observer "${name}": ${err instanceof Error ? err.message : String(err)}`);
67
+ if (code === 'ERR_MODULE_NOT_FOUND' || code === 'MODULE_NOT_FOUND') {
68
+ log.info(`[audit-observer] ${name} not installed skipping (install it alongside openbroker to forward telemetry)`);
69
+ } else {
70
+ log.warn(`[audit-observer] failed to load "${name}": ${err instanceof Error ? err.message : String(err)}`);
62
71
  }
63
72
  }
64
73
  }
@@ -13,6 +13,9 @@ import type {
13
13
  AssetCtx,
14
14
  ClearinghouseState,
15
15
  OpenOrder,
16
+ OutcomeMetaResponse,
17
+ OutcomeMarket,
18
+ OutcomeQuestion,
16
19
  } from './types.js';
17
20
  import { loadConfig, isMainnet } from './config.js';
18
21
  import { roundPrice, roundSize } from './utils.js';
@@ -51,6 +54,8 @@ export class HyperliquidClient {
51
54
  private spotSzDecimalsMap: Map<string, number> = new Map();
52
55
  /** Whether spot metadata has been loaded */
53
56
  private spotMetaLoaded: boolean = false;
57
+ /** HIP-4 outcome metadata cache */
58
+ private outcomeMeta: OutcomeMetaResponse | null = null;
54
59
  public verbose: boolean = false;
55
60
 
56
61
  constructor(config?: OpenBrokerConfig) {
@@ -658,6 +663,247 @@ export class HyperliquidClient {
658
663
  };
659
664
  }
660
665
 
666
+ // ============ HIP-4 Outcomes ============
667
+
668
+ private parseOutcomeDescription(description: string): Record<string, string> {
669
+ const parsed: Record<string, string> = {};
670
+ for (const part of description.split('|')) {
671
+ const idx = part.indexOf(':');
672
+ if (idx <= 0) continue;
673
+ parsed[part.slice(0, idx)] = part.slice(idx + 1);
674
+ }
675
+ return parsed;
676
+ }
677
+
678
+ private normalizeOutcomeSide(side: string | number): 0 | 1 {
679
+ if (typeof side === 'number') {
680
+ if (side === 0 || side === 1) return side;
681
+ } else {
682
+ const normalized = side.trim().toLowerCase();
683
+ if (normalized === '0' || normalized === 'yes' || normalized === 'y') return 0;
684
+ if (normalized === '1' || normalized === 'no' || normalized === 'n') return 1;
685
+ }
686
+ throw new Error(`Invalid outcome side "${side}". Use yes/no or 0/1.`);
687
+ }
688
+
689
+ getOutcomeEncoding(outcome: number, side: 0 | 1): number {
690
+ return 10 * outcome + side;
691
+ }
692
+
693
+ getOutcomeCoin(outcome: number, side: 0 | 1): string {
694
+ return `#${this.getOutcomeEncoding(outcome, side)}`;
695
+ }
696
+
697
+ getOutcomeAssetId(outcome: number, side: 0 | 1): number {
698
+ return 100_000_000 + this.getOutcomeEncoding(outcome, side);
699
+ }
700
+
701
+ resolveOutcomeRef(ref: string | number, side?: string | number): {
702
+ outcome: number;
703
+ side: 0 | 1;
704
+ encoding: number;
705
+ coin: string;
706
+ tokenName: string;
707
+ assetId: number;
708
+ } {
709
+ if (typeof ref === 'number') {
710
+ const resolvedSide = this.normalizeOutcomeSide(side ?? 0);
711
+ const encoding = this.getOutcomeEncoding(ref, resolvedSide);
712
+ return {
713
+ outcome: ref,
714
+ side: resolvedSide,
715
+ encoding,
716
+ coin: `#${encoding}`,
717
+ tokenName: `+${encoding}`,
718
+ assetId: 100_000_000 + encoding,
719
+ };
720
+ }
721
+
722
+ const trimmed = ref.trim();
723
+ const encoded = trimmed.startsWith('#') || trimmed.startsWith('+')
724
+ ? parseInt(trimmed.slice(1), 10)
725
+ : NaN;
726
+
727
+ if (!Number.isNaN(encoded)) {
728
+ const resolvedSide = encoded % 10;
729
+ if (resolvedSide !== 0 && resolvedSide !== 1) {
730
+ throw new Error(`Invalid outcome encoding "${ref}". Outcome side must encode to 0 or 1.`);
731
+ }
732
+ const outcome = Math.floor(encoded / 10);
733
+ return {
734
+ outcome,
735
+ side: resolvedSide as 0 | 1,
736
+ encoding: encoded,
737
+ coin: `#${encoded}`,
738
+ tokenName: `+${encoded}`,
739
+ assetId: 100_000_000 + encoded,
740
+ };
741
+ }
742
+
743
+ const outcome = parseInt(trimmed, 10);
744
+ if (!Number.isFinite(outcome) || outcome < 0) {
745
+ throw new Error(`Invalid outcome reference "${ref}". Use an outcome id, #<encoding>, or +<encoding>.`);
746
+ }
747
+
748
+ const resolvedSide = this.normalizeOutcomeSide(side ?? 0);
749
+ const encoding = this.getOutcomeEncoding(outcome, resolvedSide);
750
+ return {
751
+ outcome,
752
+ side: resolvedSide,
753
+ encoding,
754
+ coin: `#${encoding}`,
755
+ tokenName: `+${encoding}`,
756
+ assetId: 100_000_000 + encoding,
757
+ };
758
+ }
759
+
760
+ async getOutcomeMeta(): Promise<OutcomeMetaResponse> {
761
+ if (this.outcomeMeta) return this.outcomeMeta;
762
+
763
+ this.log('Fetching outcomeMeta...');
764
+ const data = await this.postInfo<OutcomeMetaResponse>({ type: 'outcomeMeta' }, 'outcomeMeta');
765
+ if (!data || !Array.isArray(data.outcomes)) {
766
+ throw new Error('outcomeMeta returned empty/malformed payload.');
767
+ }
768
+ this.outcomeMeta = {
769
+ outcomes: data.outcomes,
770
+ questions: data.questions ?? [],
771
+ };
772
+ this.log(`Loaded ${this.outcomeMeta.outcomes.length} outcome markets`);
773
+ return this.outcomeMeta;
774
+ }
775
+
776
+ private async getOutcomeCtxMap(): Promise<Map<string, {
777
+ coin?: string;
778
+ dayNtlVlm?: string;
779
+ markPx?: string;
780
+ midPx?: string | null;
781
+ prevDayPx?: string;
782
+ }>> {
783
+ const ctxMap = new Map<string, {
784
+ coin?: string;
785
+ dayNtlVlm?: string;
786
+ markPx?: string;
787
+ midPx?: string | null;
788
+ prevDayPx?: string;
789
+ }>();
790
+
791
+ try {
792
+ const spotData = await this.getSpotMetaAndAssetCtxs();
793
+ for (const ctx of spotData.assetCtxs) {
794
+ if (ctx.coin) ctxMap.set(ctx.coin, ctx);
795
+ }
796
+ } catch (e) {
797
+ this.log('Unable to load spot/outcome contexts:', e);
798
+ }
799
+
800
+ try {
801
+ const mids = await this.getAllMids();
802
+ for (const [coin, midPx] of Object.entries(mids)) {
803
+ if (!coin.startsWith('#')) continue;
804
+ const existing = ctxMap.get(coin) ?? { coin };
805
+ existing.midPx = existing.midPx ?? midPx;
806
+ existing.markPx = existing.markPx ?? midPx;
807
+ ctxMap.set(coin, existing);
808
+ }
809
+ } catch (e) {
810
+ this.log('Unable to load allMids for outcomes:', e);
811
+ }
812
+
813
+ return ctxMap;
814
+ }
815
+
816
+ async getOutcomeMarkets(): Promise<OutcomeMarket[]> {
817
+ const [meta, spotMeta, ctxMap] = await Promise.all([
818
+ this.getOutcomeMeta(),
819
+ this.getSpotMeta().catch(() => null),
820
+ this.getOutcomeCtxMap(),
821
+ ]);
822
+
823
+ const tokenDecimals = new Map<number, number>();
824
+ if (spotMeta) {
825
+ for (const token of spotMeta.tokens) {
826
+ tokenDecimals.set(token.index, token.szDecimals);
827
+ }
828
+ }
829
+
830
+ const questions = new Map<number, OutcomeQuestion>();
831
+ for (const question of meta.questions ?? []) {
832
+ for (const outcome of question.namedOutcomes) {
833
+ questions.set(outcome, question);
834
+ }
835
+ questions.set(question.fallbackOutcome, question);
836
+ }
837
+
838
+ return meta.outcomes.map((outcome) => {
839
+ const sides = outcome.sideSpecs.map((sideSpec, idx) => {
840
+ const side = idx as 0 | 1;
841
+ const encoding = this.getOutcomeEncoding(outcome.outcome, side);
842
+ const coin = `#${encoding}`;
843
+ const ctx = ctxMap.get(coin);
844
+ return {
845
+ side,
846
+ name: sideSpec.name,
847
+ encoding,
848
+ coin,
849
+ tokenName: `+${encoding}`,
850
+ assetId: 100_000_000 + encoding,
851
+ token: sideSpec.token,
852
+ szDecimals: sideSpec.token !== undefined ? tokenDecimals.get(sideSpec.token) : undefined,
853
+ midPx: ctx?.midPx ?? undefined,
854
+ markPx: ctx?.markPx,
855
+ prevDayPx: ctx?.prevDayPx,
856
+ dayNtlVlm: ctx?.dayNtlVlm,
857
+ };
858
+ });
859
+
860
+ return {
861
+ outcome: outcome.outcome,
862
+ name: outcome.name,
863
+ description: outcome.description,
864
+ parsedDescription: this.parseOutcomeDescription(outcome.description),
865
+ sides,
866
+ question: questions.get(outcome.outcome),
867
+ };
868
+ });
869
+ }
870
+
871
+ async getOutcomeMarket(outcomeId: number): Promise<OutcomeMarket | null> {
872
+ const markets = await this.getOutcomeMarkets();
873
+ return markets.find((market) => market.outcome === outcomeId) ?? null;
874
+ }
875
+
876
+ async getOutcomeSzDecimals(outcome: number, side: 0 | 1): Promise<number> {
877
+ const market = await this.getOutcomeMarket(outcome);
878
+ return market?.sides.find((s) => s.side === side)?.szDecimals ?? 0;
879
+ }
880
+
881
+ async getOutcomeMidPrice(outcome: number, side: 0 | 1): Promise<number> {
882
+ const coin = this.getOutcomeCoin(outcome, side);
883
+ const markets = await this.getOutcomeMarkets();
884
+ const marketSide = markets
885
+ .find((market) => market.outcome === outcome)
886
+ ?.sides.find((s) => s.side === side);
887
+ const fromMeta = marketSide?.midPx ?? marketSide?.markPx;
888
+ if (fromMeta) {
889
+ const mid = parseFloat(fromMeta);
890
+ if (mid > 0) return mid;
891
+ }
892
+
893
+ const mids = await this.getAllMids();
894
+ const mid = parseFloat(mids[coin] || '0');
895
+ if (mid > 0) return mid;
896
+
897
+ try {
898
+ const book = await this.getL2Book(coin);
899
+ if (book.midPrice > 0) return book.midPrice;
900
+ } catch (e) {
901
+ this.log(`Unable to fetch outcome L2 book for ${coin}:`, e);
902
+ }
903
+
904
+ throw new Error(`No outcome price for ${coin}. The market may not be open or may have no liquidity.`);
905
+ }
906
+
661
907
  /**
662
908
  * Load spot metadata into lookup maps.
663
909
  * Spot asset index for orders = 10000 + universe[i].index
@@ -2346,6 +2592,118 @@ export class HyperliquidClient {
2346
2592
  );
2347
2593
  }
2348
2594
 
2595
+ /**
2596
+ * Place a HIP-4 outcome order.
2597
+ * Outcome assets are spot-like, but encoded as:
2598
+ * encoding = 10 * outcome + side
2599
+ * assetId = 100_000_000 + encoding
2600
+ * coin = #<encoding>
2601
+ *
2602
+ * Side 0 is usually YES and side 1 is usually NO, per outcomeMeta.sideSpecs.
2603
+ */
2604
+ async outcomeOrder(
2605
+ outcomeRef: string | number,
2606
+ outcomeSide: string | number | undefined,
2607
+ isBuy: boolean,
2608
+ size: number,
2609
+ price: number,
2610
+ orderType: { limit: { tif: 'Gtc' | 'Ioc' | 'Alo' } },
2611
+ includeBuilder: boolean = true,
2612
+ szDecimalsOverride?: number,
2613
+ ): Promise<OrderResponse> {
2614
+ await this.requireTrading();
2615
+
2616
+ const resolved = this.resolveOutcomeRef(outcomeRef, outcomeSide);
2617
+ const szDecimals = szDecimalsOverride ?? await this.getOutcomeSzDecimals(resolved.outcome, resolved.side);
2618
+
2619
+ const orderWire = {
2620
+ a: resolved.assetId,
2621
+ b: isBuy,
2622
+ p: roundPrice(price, szDecimals, true),
2623
+ s: roundSize(size, szDecimals),
2624
+ r: false,
2625
+ t: orderType,
2626
+ };
2627
+
2628
+ this.log('Placing outcome order:', JSON.stringify({ resolved, orderWire }, null, 2));
2629
+
2630
+ const orderRequest: {
2631
+ orders: typeof orderWire[];
2632
+ grouping: 'na';
2633
+ builder?: BuilderInfo;
2634
+ } = {
2635
+ orders: [orderWire],
2636
+ grouping: 'na',
2637
+ };
2638
+
2639
+ if (includeBuilder && !this.isTestnet && this.config.builderAddress !== '0x0000000000000000000000000000000000000000') {
2640
+ orderRequest.builder = this.builderInfo;
2641
+ this.log('Including builder fee:', this.builderInfo);
2642
+ }
2643
+
2644
+ try {
2645
+ const response = await this.exchange.order(orderRequest, this.vaultParam);
2646
+ this.log('Outcome order response:', JSON.stringify(response, null, 2));
2647
+ return response as unknown as OrderResponse;
2648
+ } catch (error) {
2649
+ this.log('Outcome order error:', error);
2650
+ return {
2651
+ status: 'err',
2652
+ response: error instanceof Error ? error.message : String(error),
2653
+ };
2654
+ }
2655
+ }
2656
+
2657
+ async outcomeMarketOrder(
2658
+ outcomeRef: string | number,
2659
+ outcomeSide: string | number | undefined,
2660
+ isBuy: boolean,
2661
+ size: number,
2662
+ slippageBps?: number,
2663
+ szDecimalsOverride?: number,
2664
+ ): Promise<OrderResponse> {
2665
+ const resolved = this.resolveOutcomeRef(outcomeRef, outcomeSide);
2666
+ const midPrice = await this.getOutcomeMidPrice(resolved.outcome, resolved.side);
2667
+ const slippage = (slippageBps ?? this.config.slippageBps) / 10000;
2668
+ const limitPrice = isBuy
2669
+ ? midPrice * (1 + slippage)
2670
+ : midPrice * (1 - slippage);
2671
+
2672
+ this.log(`Outcome market order: ${resolved.coin} ${isBuy ? 'BUY' : 'SELL'} ${size} @ ${limitPrice} (mid: ${midPrice})`);
2673
+
2674
+ return this.outcomeOrder(
2675
+ outcomeRef,
2676
+ outcomeSide,
2677
+ isBuy,
2678
+ size,
2679
+ limitPrice,
2680
+ { limit: { tif: 'Ioc' } },
2681
+ true,
2682
+ szDecimalsOverride,
2683
+ );
2684
+ }
2685
+
2686
+ async outcomeLimitOrder(
2687
+ outcomeRef: string | number,
2688
+ outcomeSide: string | number | undefined,
2689
+ isBuy: boolean,
2690
+ size: number,
2691
+ price: number,
2692
+ tif: 'Gtc' | 'Ioc' | 'Alo' = 'Gtc',
2693
+ szDecimalsOverride?: number,
2694
+ ): Promise<OrderResponse> {
2695
+ return this.outcomeOrder(
2696
+ outcomeRef,
2697
+ outcomeSide,
2698
+ isBuy,
2699
+ size,
2700
+ price,
2701
+ { limit: { tif } },
2702
+ true,
2703
+ szDecimalsOverride,
2704
+ );
2705
+ }
2706
+
2349
2707
  /**
2350
2708
  * Cancel a spot order by coin and order ID.
2351
2709
  */
@@ -119,6 +119,56 @@ export interface MetaAndAssetCtxs {
119
119
  assetCtxs: AssetCtx[];
120
120
  }
121
121
 
122
+ // ============ Outcome / HIP-4 Types ============
123
+
124
+ export interface OutcomeSideSpec {
125
+ name: string;
126
+ token?: number;
127
+ }
128
+
129
+ export interface OutcomeMetaEntry {
130
+ outcome: number;
131
+ name: string;
132
+ description: string;
133
+ sideSpecs: OutcomeSideSpec[];
134
+ }
135
+
136
+ export interface OutcomeQuestion {
137
+ question: number;
138
+ name: string;
139
+ description: string;
140
+ fallbackOutcome: number;
141
+ namedOutcomes: number[];
142
+ settledNamedOutcomes?: number[];
143
+ }
144
+
145
+ export interface OutcomeMetaResponse {
146
+ outcomes: OutcomeMetaEntry[];
147
+ questions?: OutcomeQuestion[];
148
+ }
149
+
150
+ export interface OutcomeMarket {
151
+ outcome: number;
152
+ name: string;
153
+ description: string;
154
+ parsedDescription: Record<string, string>;
155
+ sides: Array<{
156
+ side: 0 | 1;
157
+ name: string;
158
+ encoding: number;
159
+ coin: string;
160
+ tokenName: string;
161
+ assetId: number;
162
+ token?: number;
163
+ szDecimals?: number;
164
+ midPx?: string;
165
+ markPx?: string;
166
+ prevDayPx?: string;
167
+ dayNtlVlm?: string;
168
+ }>;
169
+ question?: OutcomeQuestion;
170
+ }
171
+
122
172
  // ============ Account Types ============
123
173
 
124
174
  export interface Position {
@@ -4,7 +4,7 @@
4
4
  import { getClient } from '../core/client.js';
5
5
 
6
6
  interface Args {
7
- type?: 'perp' | 'spot' | 'hip3' | 'all';
7
+ type?: 'perp' | 'spot' | 'hip3' | 'outcome' | 'all';
8
8
  top?: number;
9
9
  verbose?: boolean;
10
10
  json?: boolean;
@@ -17,7 +17,7 @@ function parseArgs(): Args {
17
17
  const arg = process.argv[i];
18
18
  if (arg === '--type' && process.argv[i + 1]) {
19
19
  const val = process.argv[++i].toLowerCase();
20
- if (['perp', 'spot', 'hip3', 'all'].includes(val)) {
20
+ if (['perp', 'spot', 'hip3', 'outcome', 'all'].includes(val)) {
21
21
  args.type = val as Args['type'];
22
22
  }
23
23
  } else if (arg === '--top' && process.argv[i + 1]) {
@@ -33,7 +33,7 @@ All Markets - View all available markets on Hyperliquid
33
33
  Usage: npx tsx scripts/info/all-markets.ts [options]
34
34
 
35
35
  Options:
36
- --type <type> Market type: perp, spot, hip3, or all (default: all)
36
+ --type <type> Market type: perp, spot, hip3, outcome, or all (default: all)
37
37
  --top <n> Show only top N markets by volume
38
38
  --json Output as JSON (machine-readable)
39
39
  --verbose Show detailed output
@@ -44,6 +44,7 @@ Examples:
44
44
  npx tsx scripts/info/all-markets.ts --type perp # Show only main perps
45
45
  npx tsx scripts/info/all-markets.ts --type hip3 # Show only HIP-3 perps
46
46
  npx tsx scripts/info/all-markets.ts --type spot # Show only spot markets
47
+ npx tsx scripts/info/all-markets.ts --type outcome # Show only HIP-4 outcomes
47
48
  npx tsx scripts/info/all-markets.ts --top 20 # Show top 20 by volume
48
49
  npx tsx scripts/info/all-markets.ts --json # JSON output
49
50
  `);
@@ -77,7 +78,7 @@ function formatFunding(rate: string): string {
77
78
  }
78
79
 
79
80
  interface MarketRow {
80
- type: 'perp' | 'spot' | 'hip3';
81
+ type: 'perp' | 'spot' | 'hip3' | 'outcome';
81
82
  provider: string;
82
83
  coin: string;
83
84
  assetId: number;
@@ -85,6 +86,9 @@ interface MarketRow {
85
86
  volume24h: number;
86
87
  funding?: string;
87
88
  maxLeverage?: number;
89
+ outcome?: number;
90
+ outcomeSide?: string;
91
+ description?: string;
88
92
  }
89
93
 
90
94
  async function main() {
@@ -183,6 +187,30 @@ async function main() {
183
187
  }
184
188
  }
185
189
 
190
+ // Fetch HIP-4 outcomes
191
+ if (args.type === 'all' || args.type === 'outcome') {
192
+ try {
193
+ const outcomes = await client.getOutcomeMarkets();
194
+ for (const market of outcomes) {
195
+ for (const side of market.sides) {
196
+ allMarkets.push({
197
+ type: 'outcome',
198
+ provider: 'HIP-4',
199
+ coin: side.coin,
200
+ assetId: side.assetId,
201
+ price: side.midPx ?? side.markPx ?? '0',
202
+ volume24h: parseFloat(side.dayNtlVlm || '0'),
203
+ outcome: market.outcome,
204
+ outcomeSide: side.name,
205
+ description: market.description,
206
+ });
207
+ }
208
+ }
209
+ } catch (e) {
210
+ if (args.verbose) console.error('Failed to fetch HIP-4 outcomes:', e);
211
+ }
212
+ }
213
+
186
214
  // Sort by volume
187
215
  allMarkets.sort((a, b) => b.volume24h - a.volume24h);
188
216
 
@@ -198,6 +226,7 @@ async function main() {
198
226
  const perps = markets.filter((m) => m.type === 'perp');
199
227
  const hip3 = markets.filter((m) => m.type === 'hip3');
200
228
  const spots = markets.filter((m) => m.type === 'spot');
229
+ const outcomes = markets.filter((m) => m.type === 'outcome');
201
230
 
202
231
  // Print summary
203
232
  console.log('=== Market Summary ===\n');
@@ -205,6 +234,7 @@ async function main() {
205
234
  console.log(` - Main Perps: ${perps.length}`);
206
235
  console.log(` - HIP-3 Perps: ${hip3.length}`);
207
236
  console.log(` - Spot Markets: ${spots.length}`);
237
+ console.log(` - HIP-4 Outcomes: ${outcomes.length}`);
208
238
  console.log();
209
239
 
210
240
  // Print perps
@@ -245,6 +275,19 @@ async function main() {
245
275
  }
246
276
  console.log();
247
277
  }
278
+
279
+ // Print HIP-4 outcome markets
280
+ if (outcomes.length > 0) {
281
+ console.log('=== HIP-4 Outcomes ===\n');
282
+ console.log('Coin Outcome Side AssetID Price 24h Volume');
283
+ console.log('-'.repeat(78));
284
+ for (const m of outcomes) {
285
+ console.log(
286
+ `${m.coin.padEnd(14)} ${String(m.outcome ?? '-').padStart(7)} ${(m.outcomeSide ?? '-').padEnd(8)} ${String(m.assetId).padStart(9)} ${formatPrice(m.price).padStart(7)} ${formatVolume(m.volume24h).padStart(13)}`
287
+ );
288
+ }
289
+ console.log();
290
+ }
248
291
  }
249
292
 
250
293
  main().catch((e) => {
@@ -0,0 +1,200 @@
1
+ #!/usr/bin/env tsx
2
+ // HIP-4 Outcomes - search and inspect prediction markets
3
+
4
+ import { getClient } from '../core/client.js';
5
+
6
+ interface Args {
7
+ query?: string;
8
+ outcome?: string;
9
+ side?: string;
10
+ balances?: boolean;
11
+ top?: number;
12
+ verbose?: boolean;
13
+ json?: boolean;
14
+ }
15
+
16
+ function parseArgs(): Args {
17
+ const args: Args = {};
18
+
19
+ for (let i = 2; i < process.argv.length; i++) {
20
+ const arg = process.argv[i];
21
+ if ((arg === '--query' || arg === '-q') && process.argv[i + 1]) {
22
+ args.query = process.argv[++i];
23
+ } else if ((arg === '--outcome' || arg === '--id') && process.argv[i + 1]) {
24
+ args.outcome = process.argv[++i];
25
+ } else if (arg === '--side' && process.argv[i + 1]) {
26
+ args.side = process.argv[++i];
27
+ } else if (arg === '--balances') {
28
+ args.balances = true;
29
+ } else if (arg === '--top' && process.argv[i + 1]) {
30
+ args.top = parseInt(process.argv[++i], 10);
31
+ } else if (arg === '--verbose') {
32
+ args.verbose = true;
33
+ } else if (arg === '--json') {
34
+ args.json = true;
35
+ } else if (arg === '--help' || arg === '-h') {
36
+ printUsage();
37
+ process.exit(0);
38
+ } else if (!args.query && !arg.startsWith('-')) {
39
+ args.query = arg;
40
+ }
41
+ }
42
+
43
+ return args;
44
+ }
45
+
46
+ function printUsage() {
47
+ console.log(`
48
+ Open Broker - HIP-4 Outcomes
49
+ ============================
50
+
51
+ Search and inspect Hyperliquid outcome markets.
52
+
53
+ Usage:
54
+ openbroker outcomes [--query <text>] [--outcome <id|#encoding|+encoding>] [options]
55
+
56
+ Options:
57
+ --query, -q <text> Search market name, description, underlying, expiry, target
58
+ --outcome, --id <ref> Show one outcome by id or encoded coin (#1230 / +1230)
59
+ --side <yes|no|0|1> Select a side when using a plain outcome id
60
+ --balances Show outcome token balances for the configured account
61
+ --top <n> Show only top N matches
62
+ --json Output as JSON
63
+ --verbose Include raw descriptions and question metadata
64
+
65
+ Examples:
66
+ openbroker outcomes --query BTC
67
+ openbroker outcomes --outcome 123
68
+ openbroker outcomes --outcome 123 --side yes --json
69
+ openbroker outcomes --balances
70
+ `);
71
+ }
72
+
73
+ function formatPrice(value?: string): string {
74
+ if (!value) return '-';
75
+ const n = parseFloat(value);
76
+ if (!Number.isFinite(n)) return value;
77
+ return n.toFixed(4);
78
+ }
79
+
80
+ function formatVolume(value?: string): string {
81
+ if (!value) return '-';
82
+ const n = parseFloat(value);
83
+ if (!Number.isFinite(n)) return value;
84
+ if (n >= 1_000_000) return `$${(n / 1_000_000).toFixed(2)}M`;
85
+ if (n >= 1_000) return `$${(n / 1_000).toFixed(2)}K`;
86
+ return `$${n.toFixed(2)}`;
87
+ }
88
+
89
+ async function main() {
90
+ const args = parseArgs();
91
+ const client = getClient();
92
+ client.verbose = args.verbose ?? false;
93
+
94
+ if (!args.json) {
95
+ console.log('Open Broker - HIP-4 Outcomes');
96
+ console.log('============================\n');
97
+ }
98
+
99
+ try {
100
+ if (args.balances) {
101
+ const balances = await client.getSpotBalances();
102
+ const outcomeBalances = (balances.balances ?? []).filter((b) =>
103
+ b.coin.startsWith('+') || b.coin.startsWith('#')
104
+ );
105
+
106
+ if (args.json) {
107
+ console.log(JSON.stringify(outcomeBalances, null, 2));
108
+ return;
109
+ }
110
+
111
+ if (outcomeBalances.length === 0) {
112
+ console.log('No outcome token balances found.');
113
+ return;
114
+ }
115
+
116
+ console.log('Outcome Balances');
117
+ console.log('----------------');
118
+ console.log('Token Total Hold Entry Value');
119
+ console.log('-'.repeat(70));
120
+ for (const b of outcomeBalances) {
121
+ console.log(
122
+ `${b.coin.padEnd(12)} ${parseFloat(b.total).toFixed(6).padStart(18)} ` +
123
+ `${parseFloat(b.hold).toFixed(6).padStart(18)} ${formatVolume(b.entryNtl).padStart(15)}`
124
+ );
125
+ }
126
+ return;
127
+ }
128
+
129
+ let markets = await client.getOutcomeMarkets();
130
+
131
+ if (args.outcome) {
132
+ const resolved = client.resolveOutcomeRef(args.outcome, args.side);
133
+ markets = markets.filter((market) => market.outcome === resolved.outcome);
134
+ for (const market of markets) {
135
+ market.sides = market.sides.filter((side) => side.side === resolved.side);
136
+ }
137
+ }
138
+
139
+ if (args.query) {
140
+ const query = args.query.toUpperCase();
141
+ markets = markets.filter((market) => {
142
+ const parsed = Object.values(market.parsedDescription).join(' ');
143
+ const searchable = `${market.name} ${market.description} ${parsed}`.toUpperCase();
144
+ return searchable.includes(query);
145
+ });
146
+ }
147
+
148
+ markets.sort((a, b) => {
149
+ const aVol = Math.max(...a.sides.map((s) => parseFloat(s.dayNtlVlm ?? '0')));
150
+ const bVol = Math.max(...b.sides.map((s) => parseFloat(s.dayNtlVlm ?? '0')));
151
+ return bVol - aVol;
152
+ });
153
+
154
+ const displayMarkets = args.top ? markets.slice(0, args.top) : markets;
155
+
156
+ if (args.json) {
157
+ console.log(JSON.stringify(displayMarkets, null, 2));
158
+ return;
159
+ }
160
+
161
+ if (displayMarkets.length === 0) {
162
+ console.log('No outcome markets found.');
163
+ return;
164
+ }
165
+
166
+ console.log(`Found ${displayMarkets.length} outcome market(s)\n`);
167
+ console.log('Outcome Side Coin AssetID Price 24h Volume Market');
168
+ console.log('-'.repeat(98));
169
+
170
+ for (const market of displayMarkets) {
171
+ const spec = market.parsedDescription;
172
+ const labelParts = [
173
+ spec.underlying,
174
+ spec.expiry ? `exp ${spec.expiry}` : undefined,
175
+ spec.targetPrice ? `target ${spec.targetPrice}` : undefined,
176
+ ].filter(Boolean);
177
+ const label = labelParts.length > 0 ? labelParts.join(' | ') : market.description;
178
+
179
+ for (const side of market.sides) {
180
+ console.log(
181
+ `${String(market.outcome).padStart(7)} ${side.name.padEnd(4)} ${side.coin.padEnd(9)} ` +
182
+ `${String(side.assetId).padStart(10)} ${formatPrice(side.midPx ?? side.markPx).padStart(7)} ` +
183
+ `${formatVolume(side.dayNtlVlm).padStart(13)} ${label}`
184
+ );
185
+ }
186
+
187
+ if (args.verbose) {
188
+ console.log(` Description: ${market.description}`);
189
+ if (market.question) console.log(` Question: ${market.question.name}`);
190
+ }
191
+ }
192
+ } catch (error) {
193
+ const message = error instanceof Error ? error.message : String(error);
194
+ console.error(`Error: ${message}`);
195
+ console.error('Note: Hyperliquid currently documents outcomeMeta as testnet-only.');
196
+ process.exit(1);
197
+ }
198
+ }
199
+
200
+ main();
@@ -5,7 +5,7 @@ import { getClient } from '../core/client.js';
5
5
 
6
6
  interface Args {
7
7
  query: string;
8
- type?: 'perp' | 'spot' | 'hip3' | 'all';
8
+ type?: 'perp' | 'spot' | 'hip3' | 'outcome' | 'all';
9
9
  verbose?: boolean;
10
10
  json?: boolean;
11
11
  }
@@ -19,7 +19,7 @@ function parseArgs(): Args {
19
19
  args.query = process.argv[++i];
20
20
  } else if (arg === '--type' && process.argv[i + 1]) {
21
21
  const val = process.argv[++i].toLowerCase();
22
- if (['perp', 'spot', 'hip3', 'all'].includes(val)) {
22
+ if (['perp', 'spot', 'hip3', 'outcome', 'all'].includes(val)) {
23
23
  args.type = val as Args['type'];
24
24
  }
25
25
  } else if (arg === '--verbose') {
@@ -34,7 +34,7 @@ Usage: npx tsx scripts/info/search-markets.ts --query <search> [options]
34
34
 
35
35
  Options:
36
36
  --query <search> Search term (required) - matches coin name
37
- --type <type> Filter by market type: perp, spot, hip3, or all (default: all)
37
+ --type <type> Filter by market type: perp, spot, hip3, outcome, or all (default: all)
38
38
  --json Output as JSON (machine-readable)
39
39
  --verbose Show detailed output
40
40
  --help Show this help
@@ -44,6 +44,7 @@ Examples:
44
44
  npx tsx scripts/info/search-markets.ts --query BTC # Find all BTC markets
45
45
  npx tsx scripts/info/search-markets.ts --query ETH --type perp # ETH perps only
46
46
  npx tsx scripts/info/search-markets.ts --query PURR --type spot # PURR spot only
47
+ npx tsx scripts/info/search-markets.ts --query BTC --type outcome # HIP-4 outcomes only
47
48
  npx tsx scripts/info/search-markets.ts --query HYPE --json # JSON output
48
49
  `);
49
50
  process.exit(0);
@@ -95,7 +96,7 @@ async function main() {
95
96
  }
96
97
 
97
98
  interface Result {
98
- type: 'perp' | 'spot' | 'hip3';
99
+ type: 'perp' | 'spot' | 'hip3' | 'outcome';
99
100
  provider: string;
100
101
  coin: string;
101
102
  assetId: number;
@@ -104,6 +105,9 @@ async function main() {
104
105
  funding?: string;
105
106
  maxLeverage?: number;
106
107
  openInterest?: string;
108
+ outcome?: number;
109
+ outcomeSide?: string;
110
+ description?: string;
107
111
  }
108
112
 
109
113
  const results: Result[] = [];
@@ -224,6 +228,34 @@ async function main() {
224
228
  }
225
229
  }
226
230
 
231
+ // Search HIP-4 outcome markets
232
+ if (args.type === 'all' || args.type === 'outcome') {
233
+ try {
234
+ const outcomes = await client.getOutcomeMarkets();
235
+ for (const market of outcomes) {
236
+ const parsed = Object.values(market.parsedDescription).join(' ');
237
+ const searchable = `${market.name} ${market.description} ${parsed}`.toUpperCase();
238
+ if (!searchable.includes(query)) continue;
239
+
240
+ for (const side of market.sides) {
241
+ results.push({
242
+ type: 'outcome',
243
+ provider: 'HIP-4',
244
+ coin: side.coin,
245
+ assetId: side.assetId,
246
+ price: side.midPx ?? side.markPx ?? '0',
247
+ volume24h: parseFloat(side.dayNtlVlm || '0'),
248
+ outcome: market.outcome,
249
+ outcomeSide: side.name,
250
+ description: market.description,
251
+ });
252
+ }
253
+ }
254
+ } catch (e) {
255
+ if (args.verbose) console.error('Failed to fetch HIP-4 outcomes:', e);
256
+ }
257
+ }
258
+
227
259
  // Sort by volume
228
260
  results.sort((a, b) => b.volume24h - a.volume24h);
229
261
 
@@ -242,11 +274,14 @@ async function main() {
242
274
  console.log('-'.repeat(112));
243
275
 
244
276
  for (const m of results) {
245
- const typeStr = m.type === 'hip3' ? 'HIP-3' : m.type.charAt(0).toUpperCase() + m.type.slice(1);
277
+ const typeStr = m.type === 'hip3' ? 'HIP-3' : m.type === 'outcome' ? 'HIP-4' : m.type.charAt(0).toUpperCase() + m.type.slice(1);
246
278
  const oi = m.openInterest ? formatVolume(parseFloat(m.openInterest)) : '-';
247
279
  console.log(
248
280
  `${typeStr.padEnd(8)} ${m.provider.padEnd(16)} ${m.coin.padEnd(14)} ${String(m.assetId).padStart(7)} ${formatPrice(m.price).padStart(16)} ${formatVolume(m.volume24h).padStart(13)} ${(m.funding ? formatFunding(m.funding) : '-').padStart(14)} ${oi.padStart(10)}`
249
281
  );
282
+ if (m.type === 'outcome' && args.verbose) {
283
+ console.log(` Outcome ${m.outcome} ${m.outcomeSide}: ${m.description}`);
284
+ }
250
285
  }
251
286
 
252
287
  // Show comparison if same asset on multiple providers
@@ -0,0 +1,185 @@
1
+ #!/usr/bin/env npx tsx
2
+ // Execute a HIP-4 outcome order on Hyperliquid
3
+
4
+ import { getClient } from '../core/client.js';
5
+ import { checkBuilderFeeApproval, formatUsd, parseArgs } from '../core/utils.js';
6
+
7
+ function printUsage() {
8
+ console.log(`
9
+ Open Broker - HIP-4 Outcome Order
10
+ =================================
11
+
12
+ Buy or sell a YES/NO outcome token.
13
+
14
+ Usage:
15
+ openbroker outcome-order --outcome <id|#encoding|+encoding> --outcome-side <yes|no> --side <buy|sell> --size <SIZE>
16
+
17
+ Options:
18
+ --outcome Outcome id, outcome spot coin (#1230), or token name (+1230)
19
+ --outcome-side Outcome side when --outcome is a plain id: yes/no or 0/1 (default: yes)
20
+ --side Trade side: buy or sell
21
+ --size Order size in outcome token units
22
+ --price Limit price between 0.001 and 0.999 (omit for market IOC)
23
+ --tif Time-in-force for limit orders: Gtc, Ioc, Alo (default: Gtc)
24
+ --slippage Slippage tolerance in bps for market orders (default: config, usually 50)
25
+ --sz-decimals Override size decimals if outcome metadata omits token decimals
26
+ --dry Dry run - show order details without executing
27
+ --verbose Show full API request/response for debugging
28
+
29
+ Examples:
30
+ openbroker outcomes --query BTC
31
+ openbroker outcome-order --outcome 123 --outcome-side yes --side buy --size 10 --dry
32
+ openbroker outcome-buy --outcome 123 --outcome-side no --size 5 --price 0.42
33
+ openbroker outcome-sell --outcome #1230 --size 10
34
+ `);
35
+ }
36
+
37
+ function formatOutcomePrice(price: number): string {
38
+ return price.toFixed(4);
39
+ }
40
+
41
+ async function main() {
42
+ const args = parseArgs(process.argv.slice(2));
43
+
44
+ if (args.help || args.h) {
45
+ printUsage();
46
+ process.exit(0);
47
+ }
48
+
49
+ const outcomeRef = args.outcome as string;
50
+ const outcomeSide = args['outcome-side'] as string | undefined;
51
+ const side = args.side as string;
52
+ const size = parseFloat(args.size as string);
53
+ const price = args.price ? parseFloat(args.price as string) : undefined;
54
+ const tif = (args.tif as 'Gtc' | 'Ioc' | 'Alo') ?? 'Gtc';
55
+ const slippage = args.slippage ? parseInt(args.slippage as string) : undefined;
56
+ const szDecimals = args['sz-decimals'] ? parseInt(args['sz-decimals'] as string, 10) : undefined;
57
+ const dryRun = args.dry as boolean;
58
+
59
+ if (!outcomeRef || !side || isNaN(size)) {
60
+ printUsage();
61
+ process.exit(1);
62
+ }
63
+
64
+ if (side !== 'buy' && side !== 'sell') {
65
+ console.error('Error: --side must be "buy" or "sell"');
66
+ process.exit(1);
67
+ }
68
+
69
+ if (size <= 0) {
70
+ console.error('Error: --size must be positive');
71
+ process.exit(1);
72
+ }
73
+
74
+ if (price !== undefined && (price <= 0 || price >= 1)) {
75
+ console.error('Error: --price must be between 0 and 1 for outcome tokens');
76
+ process.exit(1);
77
+ }
78
+
79
+ if (szDecimals !== undefined && (szDecimals < 0 || szDecimals > 8)) {
80
+ console.error('Error: --sz-decimals must be between 0 and 8');
81
+ process.exit(1);
82
+ }
83
+
84
+ const client = getClient();
85
+ if (args.verbose) client.verbose = true;
86
+
87
+ const isBuy = side === 'buy';
88
+ const isMarket = price === undefined;
89
+
90
+ console.log('Open Broker - HIP-4 Outcome Order');
91
+ console.log('=================================\n');
92
+
93
+ await checkBuilderFeeApproval(client);
94
+
95
+ try {
96
+ const resolved = client.resolveOutcomeRef(outcomeRef, outcomeSide);
97
+ const market = await client.getOutcomeMarket(resolved.outcome);
98
+ const marketSide = market?.sides.find((s) => s.side === resolved.side);
99
+ const sideName = marketSide?.name ?? (resolved.side === 0 ? 'Yes' : 'No');
100
+ const midPrice = await client.getOutcomeMidPrice(resolved.outcome, resolved.side);
101
+ const slippageBps = slippage ?? 50;
102
+ const limitPrice = isMarket
103
+ ? (isBuy ? midPrice * (1 + slippageBps / 10000) : midPrice * (1 - slippageBps / 10000))
104
+ : price;
105
+ const notional = midPrice * size;
106
+
107
+ console.log('Order Details');
108
+ console.log('-------------');
109
+ console.log(`Outcome: ${resolved.outcome}`);
110
+ console.log(`Market: ${market?.name ?? 'Unknown'}${market?.parsedDescription.underlying ? ` (${market.parsedDescription.underlying})` : ''}`);
111
+ if (market?.parsedDescription.expiry) console.log(`Expiry: ${market.parsedDescription.expiry}`);
112
+ if (market?.parsedDescription.targetPrice) console.log(`Target: ${market.parsedDescription.targetPrice}`);
113
+ console.log(`Outcome Side: ${sideName.toUpperCase()} (${resolved.side})`);
114
+ console.log(`Coin: ${resolved.coin}`);
115
+ console.log(`Asset ID: ${resolved.assetId}`);
116
+ console.log(`Trade Side: ${isBuy ? 'BUY' : 'SELL'}`);
117
+ console.log(`Size: ${size}`);
118
+ console.log(`Mid Price: ${formatOutcomePrice(midPrice)}`);
119
+ if (isMarket) {
120
+ console.log(`Type: Market (IOC)`);
121
+ console.log(`Limit Price: ${formatOutcomePrice(limitPrice)} (${slippageBps} bps slippage)`);
122
+ } else {
123
+ console.log(`Type: Limit (${tif})`);
124
+ console.log(`Limit Price: ${formatOutcomePrice(price)}`);
125
+ }
126
+ console.log(`Notional: ~${formatUsd(notional)}`);
127
+ if (marketSide?.szDecimals !== undefined || szDecimals !== undefined) {
128
+ console.log(`Sz Decimals: ${szDecimals ?? marketSide?.szDecimals}`);
129
+ }
130
+ console.log(`Builder Fee: ${client.builderInfo.f / 10} bps`);
131
+
132
+ if (dryRun) {
133
+ console.log('\nDry run - order not submitted');
134
+ return;
135
+ }
136
+
137
+ console.log('\nExecuting...');
138
+
139
+ const response = isMarket
140
+ ? await client.outcomeMarketOrder(outcomeRef, outcomeSide, isBuy, size, slippage, szDecimals)
141
+ : await client.outcomeLimitOrder(outcomeRef, outcomeSide, isBuy, size, price!, tif, szDecimals);
142
+
143
+ console.log('\nResult');
144
+ console.log('------');
145
+
146
+ if (args.verbose || process.env.VERBOSE) {
147
+ console.log('\nFull Response:');
148
+ console.log(JSON.stringify(response, null, 2));
149
+ }
150
+
151
+ if (response.status === 'ok' && response.response && typeof response.response === 'object') {
152
+ const statuses = response.response.data.statuses;
153
+ for (const status of statuses) {
154
+ if (status.filled) {
155
+ const fillSz = parseFloat(status.filled.totalSz);
156
+ const avgPx = parseFloat(status.filled.avgPx);
157
+ console.log('Filled');
158
+ console.log(` Order ID: ${status.filled.oid}`);
159
+ console.log(` Size: ${fillSz}`);
160
+ console.log(` Avg Price: ${formatOutcomePrice(avgPx)}`);
161
+ console.log(` Notional: ${formatUsd(fillSz * avgPx)}`);
162
+ } else if (status.resting) {
163
+ console.log('Resting');
164
+ console.log(` Order ID: ${status.resting.oid}`);
165
+ } else if (status.error) {
166
+ console.log(`Error: ${status.error}`);
167
+ } else {
168
+ console.log('Unknown status:');
169
+ console.log(JSON.stringify(status, null, 2));
170
+ }
171
+ }
172
+ } else if (response.status === 'err') {
173
+ console.log(`API Error: ${response.response || JSON.stringify(response)}`);
174
+ } else {
175
+ console.log('Unexpected response:');
176
+ console.log(JSON.stringify(response, null, 2));
177
+ }
178
+ } catch (error) {
179
+ console.error('Error executing outcome order:', error);
180
+ console.error('Note: Hyperliquid currently documents outcomeMeta as testnet-only.');
181
+ process.exit(1);
182
+ }
183
+ }
184
+
185
+ main();