@promptai.credit/cli 0.4.2 → 0.4.3

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.
@@ -31,3 +31,7 @@ CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 claude --plugin-dir /path/to/product/claude-
31
31
  terminal. It is fetched with `curl` and converted with `sips` (macOS) or ImageMagick;
32
32
  SVGs need `qlmanage` (macOS) or `rsvg-convert`. If any step is missing the card simply
33
33
  draws without a logo. Results are cached under `~/.promptai/logos/`.
34
+ - Jev ticker: the plugin polls `GET /stocks/ticker` (Jev's current top picks, with
35
+ Robinhood Chain prices and 24h change from DefiLlama). The picks show on the ad
36
+ card, the credited slices show on the verified strip, and a compact ticker rides the
37
+ prompt hint while the agent works. If the server has Jev off, the ticker stays hidden.
@@ -7,21 +7,28 @@
7
7
  * 3. ui.render AbovePrompt — compact sponsored card + countdown
8
8
  * 4. clock tick — when minWatchMs elapsed, POST /ads/complete
9
9
  * 5. CTA press — beacon click, open destination in OS browser
10
+ * 6. Jev ticker — GET /stocks/ticker (top picks + Robinhood Chain quotes),
11
+ * drawn on the ad card and on the prompt hint while working
10
12
  *
11
13
  * Stop / transcript pricing / credit redeem stay on the command-hook CLI path.
12
14
  *
13
15
  * Requires CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 until Mods are generally available.
14
16
  */
15
- import type { EngineInterface as Engine, Register, Timer } from "claude-code";
17
+ import type { EngineInterface as Engine, Register, RenderChildren, Timer } from "claude-code";
16
18
  import { WORK_PX, bitmapToCells, decodeBase64, hashKey, parseBmp, type LogoCells } from "./logo.ts";
17
19
 
18
20
  const SOURCE = "claude-mods";
19
21
  /** Logo cell grid: half-blocks, so 12 columns × 6 rows is a square of 12×12 pixels. */
20
22
  const LOGO_COLS = 12;
21
23
  const LOGO_ROWS = 6;
24
+ /** Stock logos in the Jev tiles: 6×3 cells, one tile tall. */
25
+ const STOCK_LOGO_COLS = 6;
26
+ const STOCK_LOGO_ROWS = 3;
22
27
  const HEARTBEAT_FILE = "native-plugin.json";
23
28
  /** How long the CLI treats a heartbeat as "plugin is live" before falling back to /watch. */
24
29
  const HEARTBEAT_FRESH_MS = 2 * 60 * 60 * 1000;
30
+ /** Refetch the Jev ticker at most this often (the server caches it for 60s). */
31
+ const TICKER_STALE_MS = 5 * 60 * 1000;
25
32
 
26
33
  interface AdCreative {
27
34
  id: string;
@@ -42,6 +49,27 @@ interface PromptaiConfig {
42
49
  adsOptIn: boolean;
43
50
  }
44
51
 
52
+ interface TickerName {
53
+ ticker: string;
54
+ name: string;
55
+ logoUrl?: string;
56
+ price: number | null;
57
+ change24h: number | null;
58
+ weight: number | null;
59
+ }
60
+
61
+ interface JevTicker {
62
+ enabled: boolean;
63
+ rewardUsd: number;
64
+ picks: TickerName[];
65
+ names: TickerName[];
66
+ }
67
+
68
+ interface GrantedStock {
69
+ ticker: string;
70
+ notionalUsd: number;
71
+ }
72
+
45
73
  type AdPhase = "idle" | "loading" | "showing" | "verified" | "failed";
46
74
 
47
75
  interface ActiveAd {
@@ -52,12 +80,19 @@ interface ActiveAd {
52
80
  error?: string;
53
81
  tick?: Timer;
54
82
  logo?: LogoCells;
83
+ /** Paper stock slices granted on this verified watch (Jev inside promptai). */
84
+ stocks?: GrantedStock[];
55
85
  }
56
86
 
57
87
  const active: ActiveAd = { phase: "idle" };
58
88
  let config: PromptaiConfig | null = null;
59
89
  let homeDir = "";
60
90
  let startingSession = false;
91
+ let jev: JevTicker | null = null;
92
+ let jevFetchedAt = 0;
93
+ let jevFetching = false;
94
+ /** Stock logo cells by ticker; null once a load failed so it is not retried. */
95
+ const stockLogos = new Map<string, LogoCells | null>();
61
96
 
62
97
  function configPath(): string {
63
98
  return `${homeDir}/.promptai/config.json`;
@@ -159,6 +194,7 @@ function resetAd(phase: AdPhase = "idle"): void {
159
194
  active.shownAt = undefined;
160
195
  active.error = undefined;
161
196
  active.logo = undefined;
197
+ active.stocks = undefined;
162
198
  }
163
199
 
164
200
  function parseBody<T>(text: string): T {
@@ -193,16 +229,18 @@ async function completeAd($: Engine): Promise<void> {
193
229
  $.ui.invalidate("ui.render");
194
230
  return;
195
231
  }
232
+ const body = parseBody<{
233
+ verified?: boolean;
234
+ stocks?: Array<{ ticker?: string; notionalUsd?: number | string }>;
235
+ }>(res.text);
236
+ active.stocks = (body.stocks ?? [])
237
+ .filter((s) => typeof s.ticker === "string" && s.ticker)
238
+ .map((s) => ({ ticker: s.ticker as string, notionalUsd: Number(s.notionalUsd) || 0 }))
239
+ .slice(0, 5);
196
240
  active.phase = "verified";
197
241
  clearTick();
242
+ // The card stays open once banked; Hide or the next prompt's ad replaces it.
198
243
  $.ui.invalidate("ui.render");
199
- // Auto-collapse the verified strip after a short beat.
200
- $.clock.after(8_000, () => {
201
- if (active.phase === "verified") {
202
- resetAd("idle");
203
- $.ui.invalidate("ui.render");
204
- }
205
- });
206
244
  } catch (err) {
207
245
  active.phase = "failed";
208
246
  active.error = String(err);
@@ -380,7 +418,8 @@ async function beginAdSession($: Engine): Promise<void> {
380
418
  if (active.phase === "loading" || active.phase === "showing") return;
381
419
 
382
420
  startingSession = true;
383
- active.phase = "loading";
421
+ // A banked card stays open until now; drop its logo and credited stocks.
422
+ resetAd("loading");
384
423
  $.ui.invalidate("ui.render");
385
424
 
386
425
  try {
@@ -432,6 +471,73 @@ async function beginAdSession($: Engine): Promise<void> {
432
471
  }
433
472
  }
434
473
 
474
+ // --- Jev ticker ---
475
+
476
+ /** Fire-and-forget; keeps the last good ticker when a refresh fails. */
477
+ async function refreshTicker($: Engine, force = false): Promise<void> {
478
+ if (!config || jevFetching) return;
479
+ if (!force && Date.now() - jevFetchedAt < TICKER_STALE_MS) return;
480
+ jevFetching = true;
481
+ try {
482
+ const res = await $.http.fetch(`${config.serverUrl}/stocks/ticker`, { method: "GET" });
483
+ if (!res.ok) return;
484
+ const body = parseBody<Partial<JevTicker>>(res.text);
485
+ if (!Array.isArray(body.names)) return;
486
+ jev = {
487
+ enabled: Boolean(body.enabled),
488
+ rewardUsd: Number(body.rewardUsd) || 0,
489
+ picks: Array.isArray(body.picks) ? body.picks : [],
490
+ names: body.names,
491
+ };
492
+ jevFetchedAt = Date.now();
493
+ $.ui.invalidate("ui.render");
494
+ for (const name of jev.names) void loadStockLogo($, name);
495
+ } catch {
496
+ // ticker is decoration; never surface a fetch error
497
+ } finally {
498
+ jevFetching = false;
499
+ }
500
+ }
501
+
502
+ /** Fire-and-forget: tiles draw a text badge until the logo lands. */
503
+ async function loadStockLogo($: Engine, name: TickerName): Promise<void> {
504
+ if (!name.logoUrl || stockLogos.has(name.ticker)) return;
505
+ stockLogos.set(name.ticker, null);
506
+ const logo = await loadLogoCells($, {
507
+ url: name.logoUrl,
508
+ home: homeDir,
509
+ columns: STOCK_LOGO_COLS,
510
+ rows: STOCK_LOGO_ROWS,
511
+ });
512
+ if (logo) {
513
+ stockLogos.set(name.ticker, logo);
514
+ $.ui.invalidate("ui.render");
515
+ }
516
+ }
517
+
518
+ function tickerQuote(ticker: string): TickerName | undefined {
519
+ return jev?.names.find((n) => n.ticker === ticker);
520
+ }
521
+
522
+ function fmtPrice(price: number | null): string {
523
+ if (price == null) return "";
524
+ return price >= 1000 ? price.toFixed(0) : price.toFixed(2);
525
+ }
526
+
527
+ function fmtChange(change: number | null): { text: string; color: string } | null {
528
+ if (change == null) return null;
529
+ const up = change >= 0;
530
+ return { text: `${up ? "▲" : "▼"}${Math.abs(change).toFixed(1)}%`, color: up ? C.green : C.red };
531
+ }
532
+
533
+ /** Plain-text ticker for the one-string prompt hint: `NVDA 182.40 ▲1.2% · TSLA …`. */
534
+ function tickerLine(): string {
535
+ const names = jev?.picks.length ? jev.picks : [];
536
+ return names
537
+ .map((n) => [n.ticker, fmtPrice(n.price), fmtChange(n.change24h)?.text ?? ""].filter(Boolean).join(" "))
538
+ .join(" · ");
539
+ }
540
+
435
541
  function remainingSeconds($: Engine): number {
436
542
  if (!active.ad || active.shownAt == null) return 0;
437
543
  const left = active.ad.minWatchMs - (Date.now() - active.shownAt);
@@ -444,6 +550,7 @@ export const register: Register = (on) => {
444
550
  config = await loadConfig($);
445
551
  if (config?.adsOptIn) {
446
552
  await writeHeartbeat($);
553
+ void refreshTicker($, true);
447
554
  }
448
555
  return next(e);
449
556
  });
@@ -453,16 +560,67 @@ export const register: Register = (on) => {
453
560
  if (config?.adsOptIn) {
454
561
  void writeHeartbeat($);
455
562
  void beginAdSession($);
563
+ void refreshTicker($);
456
564
  }
457
565
  return next(e);
458
566
  });
459
567
 
568
+ // While the agent works and no card is up, ride Jev's picks on the hint line.
569
+ on("ui.render", { component: "PromptHint" }, ($, e, next) => {
570
+ if (!config?.adsOptIn || active.phase !== "idle" || !e.props.isWorking || e.props.isDraft) {
571
+ return next(e);
572
+ }
573
+ const line = tickerLine();
574
+ if (!line) return next(e);
575
+ const hint = e.props.hint ? `${e.props.hint} · ` : "";
576
+ return next({ ...e, props: { ...e.props, hint: `${hint}Jev ${line}` } });
577
+ });
578
+
460
579
  on("ui.render", { component: "AbovePrompt" }, async ($, e, next) => {
461
580
  if (!config?.adsOptIn || active.phase === "idle") {
462
581
  return next(e);
463
582
  }
464
583
 
465
584
  const t = await $.ui.resolve(e);
585
+ const Raster = "Raster" in t ? t.Raster : null;
586
+
587
+ // One Jev stock tile: logo, then ticker / quote / detail stacked beside it.
588
+ const stockTile = (ticker: string, detail: string, detailColor: string) => {
589
+ const quote = tickerQuote(ticker);
590
+ const change = fmtChange(quote?.change24h ?? null);
591
+ const logo = stockLogos.get(ticker);
592
+ return (
593
+ <t.Box key={`tile-${ticker}`} gap={1}>
594
+ {Raster && logo ? (
595
+ <Raster key={`logo-${ticker}`} columns={logo.columns} rows={logo.rows} cells={logo.cells} />
596
+ ) : (
597
+ <t.Box width={STOCK_LOGO_COLS} height={STOCK_LOGO_ROWS} justifyContent="center" alignItems="center">
598
+ <t.Text color={C.dim} bold>{ticker.slice(0, 2)}</t.Text>
599
+ </t.Box>
600
+ )}
601
+ <t.Box flexDirection="column">
602
+ <t.Text color={C.fg} bold>{ticker}</t.Text>
603
+ <t.Box gap={1}>
604
+ {quote?.price != null ? <t.Text color={C.soft}>{fmtPrice(quote.price)}</t.Text> : null}
605
+ {change ? <t.Text color={change.color}>{change.text}</t.Text> : null}
606
+ </t.Box>
607
+ <t.Text color={detailColor}>{detail}</t.Text>
608
+ </t.Box>
609
+ </t.Box>
610
+ );
611
+ };
612
+
613
+ const jevSection = (title: string, aside: string, tiles: RenderChildren[]) => (
614
+ <t.Box flexDirection="column" borderStyle="round" borderColor={C.faint} paddingX={1} marginTop={1}>
615
+ <t.Box justifyContent="space-between">
616
+ <t.Text color={C.amber} bold>{`◇ ${title}`}</t.Text>
617
+ <t.Text color={C.dim}>{aside}</t.Text>
618
+ </t.Box>
619
+ <t.Box gap={4} flexWrap="wrap" marginTop={1}>
620
+ {tiles}
621
+ </t.Box>
622
+ </t.Box>
623
+ );
466
624
 
467
625
  if (active.phase === "loading") {
468
626
  return (
@@ -482,26 +640,8 @@ export const register: Register = (on) => {
482
640
  );
483
641
  }
484
642
 
485
- if (active.phase === "verified") {
486
- return (
487
- <t.Box borderStyle="round" borderColor={C.green} paddingX={1} gap={1}>
488
- <t.Text color={C.green} bold>✓ verified</t.Text>
489
- <t.Text color={C.soft}>credit banked</t.Text>
490
- <t.Text color={C.faint}>·</t.Text>
491
- <t.Button
492
- key="hide"
493
- label="Hide"
494
- plain
495
- onPress={() => {
496
- resetAd("idle");
497
- $.ui.invalidate("ui.render");
498
- }}
499
- />
500
- </t.Box>
501
- );
502
- }
503
-
504
- if (active.phase !== "showing" || !active.ad || active.shownAt == null) {
643
+ const verified = active.phase === "verified";
644
+ if ((active.phase !== "showing" && !verified) || !active.ad || active.shownAt == null) {
505
645
  return next(e);
506
646
  }
507
647
 
@@ -511,8 +651,8 @@ export const register: Register = (on) => {
511
651
  const secs = remainingSeconds($);
512
652
  const shownLines = (ad.lines ?? []).slice(0, 4);
513
653
 
514
- const Raster = "Raster" in t ? t.Raster : null;
515
654
  const logo = active.logo;
655
+ const granted = verified ? (active.stocks ?? []) : [];
516
656
 
517
657
  const details = (
518
658
  <t.Box flexDirection="column" flexGrow={1}>
@@ -533,7 +673,7 @@ export const register: Register = (on) => {
533
673
  );
534
674
 
535
675
  return (
536
- <t.Box flexDirection="column" borderStyle="round" borderColor={C.faint} paddingX={1}>
676
+ <t.Box flexDirection="column" borderStyle="round" borderColor={verified ? C.green : C.faint} paddingX={1}>
537
677
  {Raster && logo ? (
538
678
  <t.Box gap={2}>
539
679
  <Raster key="logo" columns={logo.columns} rows={logo.rows} cells={logo.cells} />
@@ -542,14 +682,46 @@ export const register: Register = (on) => {
542
682
  ) : (
543
683
  details
544
684
  )}
545
- <t.Box gap={1}>
546
- <t.Box>
547
- <t.Text color={C.green}>{bar.filled}</t.Text>
548
- <t.Text color={C.faint}>{bar.empty}</t.Text>
685
+ {granted.length
686
+ ? jevSection(
687
+ "Jev credited",
688
+ `$${granted.reduce((sum, g) => sum + g.notionalUsd, 0).toFixed(2)} paper`,
689
+ granted.map((g) => stockTile(g.ticker, `+$${g.notionalUsd.toFixed(4)}`, C.green)),
690
+ )
691
+ : jev?.enabled && jev.picks.length && !verified
692
+ ? jevSection(
693
+ "Jev top picks",
694
+ `watch → $${jev.rewardUsd.toFixed(2)} slice`,
695
+ jev.picks.map((p) =>
696
+ stockTile(p.ticker, p.weight != null ? `${Math.round(p.weight * 100)}% of book` : "", C.faint),
697
+ ),
698
+ )
699
+ : null}
700
+ {verified ? (
701
+ <t.Box gap={1} marginTop={1}>
702
+ <t.Text color={C.green} bold>✓ verified</t.Text>
703
+ <t.Text color={C.soft}>credit banked</t.Text>
704
+ <t.Text color={C.faint}>·</t.Text>
705
+ <t.Button
706
+ key="hide"
707
+ label="Hide"
708
+ plain
709
+ onPress={() => {
710
+ resetAd("idle");
711
+ $.ui.invalidate("ui.render");
712
+ }}
713
+ />
549
714
  </t.Box>
550
- <t.Text color={C.fg} bold>{`${secs}s`}</t.Text>
551
- <t.Text color={C.faint}>opt-in rewarded ad</t.Text>
552
- </t.Box>
715
+ ) : (
716
+ <t.Box gap={1} marginTop={1}>
717
+ <t.Box>
718
+ <t.Text color={C.green}>{bar.filled}</t.Text>
719
+ <t.Text color={C.faint}>{bar.empty}</t.Text>
720
+ </t.Box>
721
+ <t.Text color={C.fg} bold>{`${secs}s`}</t.Text>
722
+ <t.Text color={C.faint}>opt-in rewarded ad</t.Text>
723
+ </t.Box>
724
+ )}
553
725
  <t.Button key="cta" label={`${ad.ctaLabel || "Learn more"} →`} onPress={() => void onCtaPress($)} />
554
726
  </t.Box>
555
727
  );
package/dist/index.js CHANGED
@@ -486,15 +486,18 @@ function uninstallClaudePlugin() {
486
486
 
487
487
  // src/pricing.ts
488
488
  var TABLE = [
489
- { match: ["fable"], inputPerMtok: 3, outputPerMtok: 15 },
489
+ { match: ["fable"], inputPerMtok: 10, outputPerMtok: 50 },
490
+ { match: ["opus-5.5", "opus 5.5", "claude-opus-5"], inputPerMtok: 4, outputPerMtok: 20 },
490
491
  { match: ["opus"], inputPerMtok: 15, outputPerMtok: 75 },
491
492
  { match: ["sonnet"], inputPerMtok: 3, outputPerMtok: 15 },
492
493
  { match: ["haiku"], inputPerMtok: 0.8, outputPerMtok: 4 },
494
+ { match: ["gpt-5.6", "gpt5.6", "5.6-sol", "5.6 sol"], inputPerMtok: 4, outputPerMtok: 20 },
493
495
  { match: ["gpt-5", "gpt5", "codex"], inputPerMtok: 1.25, outputPerMtok: 10 },
494
496
  { match: ["gpt-4", "gpt4", "o3-", "o4-"], inputPerMtok: 2, outputPerMtok: 8 },
497
+ { match: ["gemini-3.8", "gemini 3.8", "3.8-flash", "3.8 flash"], inputPerMtok: 0.5, outputPerMtok: 5 },
495
498
  { match: ["gemini"], inputPerMtok: 1.25, outputPerMtok: 10 },
496
- { match: ["grok"], inputPerMtok: 3, outputPerMtok: 15 },
497
- { match: ["composer"], inputPerMtok: 1.25, outputPerMtok: 10 },
499
+ { match: ["grok"], inputPerMtok: 2, outputPerMtok: 6.25 },
500
+ { match: ["composer"], inputPerMtok: 0.3, outputPerMtok: 4 },
498
501
  { match: ["deepseek", "kimi", "qwen"], inputPerMtok: 0.6, outputPerMtok: 2.5 }
499
502
  ];
500
503
  var DEFAULT_PRICE = { inputPerMtok: 2, outputPerMtok: 8 };
@@ -744,14 +747,21 @@ async function handleStop(payload) {
744
747
  const cost = costUsd(usage.model, usage.inputTokens, usage.outputTokens);
745
748
  const promptId = crypto3.randomUUID();
746
749
  let verified = false;
750
+ let creditedUsd;
751
+ let stocks;
747
752
  if (config.adsOptIn && cost > 0) {
748
- verified = await redeemAgainstAd(
753
+ const redeem = await redeemAgainstAd(
749
754
  config.serverUrl,
750
755
  config.deviceId,
751
756
  promptId,
752
757
  cost,
753
758
  resolveEmail(config)
754
759
  );
760
+ if (redeem) {
761
+ verified = true;
762
+ creditedUsd = redeem.creditedUsd;
763
+ stocks = redeem.stocks;
764
+ }
755
765
  }
756
766
  state.prompts.unshift({
757
767
  id: promptId,
@@ -762,7 +772,9 @@ async function handleStop(payload) {
762
772
  inputTokens: usage.inputTokens,
763
773
  outputTokens: usage.outputTokens,
764
774
  costUsd: cost,
765
- verified
775
+ verified,
776
+ creditedUsd,
777
+ stocks
766
778
  });
767
779
  saveState(state);
768
780
  log(
@@ -774,19 +786,22 @@ async function redeemAgainstAd(serverUrl, deviceId, promptId, cost, email) {
774
786
  const { sessions } = await listVerifiedSessions(serverUrl, deviceId);
775
787
  if (sessions.length === 0) {
776
788
  log(`[claude] no verified ad session available for prompt ${promptId}`);
777
- return false;
789
+ return null;
778
790
  }
779
- await redeemCredit(serverUrl, {
791
+ const result = await redeemCredit(serverUrl, {
780
792
  sessionId: sessions[0].sessionId,
781
793
  deviceId,
782
794
  promptId,
783
795
  amountUsd: Math.min(cost, MAX_CREDIT_PER_AD_USD),
784
796
  email: email || void 0
785
797
  });
786
- return true;
798
+ return {
799
+ creditedUsd: result.creditedUsd,
800
+ stocks: result.stocks ?? []
801
+ };
787
802
  } catch (err) {
788
803
  log(`[claude] credit redeem failed for prompt ${promptId}: ${String(err)}`);
789
- return false;
804
+ return null;
790
805
  }
791
806
  }
792
807
 
@@ -982,14 +997,21 @@ async function handleCursorStop(payload) {
982
997
  const cost = costUsd(model, inputTokens, outputTokens);
983
998
  const promptId = crypto4.randomUUID();
984
999
  let verified = false;
1000
+ let creditedUsd;
1001
+ let stocks;
985
1002
  if (config.adsOptIn && cost > 0) {
986
- verified = await redeemAgainstAd2(
1003
+ const redeem = await redeemAgainstAd2(
987
1004
  config.serverUrl,
988
1005
  config.deviceId,
989
1006
  promptId,
990
1007
  cost,
991
1008
  resolveEmail(config)
992
1009
  );
1010
+ if (redeem) {
1011
+ verified = true;
1012
+ creditedUsd = redeem.creditedUsd;
1013
+ stocks = redeem.stocks;
1014
+ }
993
1015
  }
994
1016
  state.prompts.unshift({
995
1017
  id: promptId,
@@ -1001,7 +1023,9 @@ async function handleCursorStop(payload) {
1001
1023
  outputTokens,
1002
1024
  costUsd: cost,
1003
1025
  verified,
1004
- estimated
1026
+ estimated,
1027
+ creditedUsd,
1028
+ stocks
1005
1029
  });
1006
1030
  saveState(state);
1007
1031
  log(
@@ -1013,19 +1037,22 @@ async function redeemAgainstAd2(serverUrl, deviceId, promptId, cost, email) {
1013
1037
  const { sessions } = await listVerifiedSessions(serverUrl, deviceId);
1014
1038
  if (sessions.length === 0) {
1015
1039
  log(`[cursor] no verified ad session available for prompt ${promptId}`);
1016
- return false;
1040
+ return null;
1017
1041
  }
1018
- await redeemCredit(serverUrl, {
1042
+ const result = await redeemCredit(serverUrl, {
1019
1043
  sessionId: sessions[0].sessionId,
1020
1044
  deviceId,
1021
1045
  promptId,
1022
1046
  amountUsd: Math.min(cost, MAX_CREDIT_PER_AD_USD2),
1023
1047
  email: email || void 0
1024
1048
  });
1025
- return true;
1049
+ return {
1050
+ creditedUsd: result.creditedUsd,
1051
+ stocks: result.stocks ?? []
1052
+ };
1026
1053
  } catch (err) {
1027
1054
  log(`[cursor] credit redeem failed for prompt ${promptId}: ${String(err)}`);
1028
- return false;
1055
+ return null;
1029
1056
  }
1030
1057
  }
1031
1058
 
@@ -1209,6 +1236,14 @@ async function cmdStatus() {
1209
1236
  `claims ${!balance.email ? "blocked (link an email first)" : balance.claimActivated ? "activated" : "blocked (waiting for admin activation)"}`
1210
1237
  );
1211
1238
  console.log(`banked ${verified.sessions.length} verified ad watch(es) ready to fund prompts`);
1239
+ const stocks = balance.stocks ?? [];
1240
+ if (stocks.length) {
1241
+ console.log(
1242
+ `stocks ${stocks.map((s) => `${s.ticker} $${Number(s.notionalUsd).toFixed(2)}`).join(" \xB7 ")} (paper \xB7 not claimable \xB7 Jev)`
1243
+ );
1244
+ } else {
1245
+ console.log(`stocks none yet (Jev picks NVDA, TSLA, Google, or SpaceX after a verified watch)`);
1246
+ }
1212
1247
  } catch (err) {
1213
1248
  console.log(`balance unavailable (${String(err)})`);
1214
1249
  }
@@ -1217,8 +1252,15 @@ async function cmdStatus() {
1217
1252
  for (const p of state.prompts.slice(0, 8)) {
1218
1253
  const when = new Date(p.ts).toISOString().slice(0, 16).replace("T", " ");
1219
1254
  const badge = p.verified ? "ad verified" : "no ad";
1255
+ const stocks = p.stocks ?? [];
1256
+ const stockBits = stocks.map((s) => `${s.ticker} $${Number(s.notionalUsd).toFixed(2)}`).join(" \xB7 ");
1257
+ const extras = [
1258
+ p.verified ? `credit $${Number(p.creditedUsd ?? p.costUsd).toFixed(4)}` : null,
1259
+ stockBits || null
1260
+ ].filter(Boolean).join(" \xB7 ");
1220
1261
  console.log(
1221
- ` ${when} ${p.model} in=${p.inputTokens} out=${p.outputTokens} $${p.costUsd.toFixed(4)} [${badge}]`
1262
+ ` ${when} ${p.model} in=${p.inputTokens} out=${p.outputTokens} $${p.costUsd.toFixed(4)} [${badge}]` + (extras ? `
1263
+ ${extras}` : "")
1222
1264
  );
1223
1265
  }
1224
1266
  }
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@promptai.credit/cli",
3
- "version": "0.4.2",
3
+ "version": "0.4.3",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
7
- "description": "Earn ad-subsidized prompt credits from terminal AI agents (Claude Code). Watch a dev-tool ad while your agent works; verified watches pay your prompt's token cost in USDC.",
7
+ "description": "Earn ad-subsidized prompt credits from terminal AI agents (Claude Code). Watch a dev-tool ad while your agent works; verified watches pay your prompt in USDC and bank Jev-picked tokenised stock credits.",
8
8
  "type": "module",
9
9
  "license": "MIT",
10
10
  "homepage": "https://promptai.credit",