@oracle-agent/oracle 0.25.1 → 0.25.2

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.
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env python3
2
2
  """Oracle-branded alert cards.
3
3
 
4
- RH AGENT cards stay RH AGENT. This module is the Oracle desk surface:
4
+ RH AGENT brand is dead. This module is the Oracle desk surface:
5
5
  same lime/red candles, Oracle splash kit, official chain lockup (logo, not
6
6
  a typeset fake), and price-only chrome for Polymarket / HIP-4.
7
7
 
@@ -66,6 +66,7 @@ def _init_fonts():
66
66
  roots = [
67
67
  Path.home() / ".local/share/fonts",
68
68
  Path.home() / ".local/share/fonts/dmmono",
69
+ Path(__file__).resolve().parent / "fonts",
69
70
  ]
70
71
  for root in roots:
71
72
  if not root.exists():
@@ -93,8 +94,48 @@ def _init_fonts():
93
94
  return _FONTS
94
95
 
95
96
 
97
+ def _oracle_display_fp(size: float):
98
+ """Oracle display face at a real weight. Variable Cormorant has no bold
99
+ instance in matplotlib — we ship a 700 static cut next to this file."""
100
+ from matplotlib import font_manager as fm
101
+
102
+ here = Path(__file__).resolve().parent / "fonts"
103
+ for name in ("CormorantGaramond-Bold.ttf", "CormorantGaramond-SemiBold.ttf"):
104
+ p = here / name
105
+ if p.exists():
106
+ try:
107
+ fm.fontManager.addfont(str(p))
108
+ except Exception:
109
+ pass
110
+ return fm.FontProperties(fname=str(p), size=size)
111
+ return fm.FontProperties(family=_FONTS.get("display") or "DejaVu Serif", size=size, weight="bold")
112
+
113
+
114
+ def _oracle_num_fp(size: float):
115
+ """Oracle data face for figures. Cormorant is for names; numbers use
116
+ DM Mono / Roboto Mono from a real file so they stay bold and tabular."""
117
+ from matplotlib import font_manager as fm
118
+
119
+ cands = [
120
+ Path(__file__).resolve().parent / "fonts" / "RobotoMono-Bold.ttf",
121
+ Path.home() / ".local/share/fonts/keyitdev/RobotoMono-Bold.ttf",
122
+ Path(__file__).resolve().parent / "fonts" / "dmmono-Medium.ttf",
123
+ Path.home() / ".local/share/fonts/dmmono/dmmono-Medium.ttf",
124
+ Path.home() / ".local/share/fonts/MapleMono/MapleMono-Bold.ttf",
125
+ ]
126
+ for p in cands:
127
+ if not p.exists():
128
+ continue
129
+ try:
130
+ fm.fontManager.addfont(str(p))
131
+ except Exception:
132
+ pass
133
+ return fm.FontProperties(fname=str(p), size=size)
134
+ return fm.FontProperties(family=_FONTS.get("mono") or "DejaVu Sans Mono", size=size, weight="bold")
135
+
136
+
96
137
  def _esc(s: str) -> str:
97
- return str(s or "").replace("$", r"\$")
138
+ return str(s or "")
98
139
 
99
140
 
100
141
  def _fmt_px(n: float) -> str:
@@ -208,7 +249,7 @@ def _load_logo(path: Path | None):
208
249
  return None
209
250
 
210
251
 
211
- def _chrome(fig, *, venue: str, eyebrow: str, hero: str, hero_color: str, chip: str, chip_bg: str, footer: str):
252
+ def _chrome(fig, *, venue: str, eyebrow: str, hero: str, hero_color: str, chip: str, chip_bg: str, footer: str, pair_label: str = ""):
212
253
  from matplotlib import patheffects as pe
213
254
  from matplotlib.lines import Line2D
214
255
 
@@ -228,7 +269,6 @@ def _chrome(fig, *, venue: str, eyebrow: str, hero: str, hero_color: str, chip:
228
269
  text_x = 0.048
229
270
  wm = fig.text(text_x, 0.922, "ORACLE", color=B["blue2"], fontsize=16, fontfamily=fonts["display"], fontweight="medium", va="center")
230
271
  wm.set_path_effects([pe.withStroke(linewidth=3.0, foreground=B["glow"], alpha=0.65)])
231
- # Chain tag: "CHAIN:" + the venue's official lockup (its real logo + font).
232
272
  tag_x = text_x + 0.118
233
273
  fig.text(tag_x, 0.922, "CHAIN:", color=B["mute"], fontsize=8.0, fontfamily=fonts["mono"], va="center")
234
274
  lockup = _load_logo(meta.get("logo"))
@@ -249,11 +289,43 @@ def _chrome(fig, *, venue: str, eyebrow: str, hero: str, hero_color: str, chip:
249
289
  fig.text(tag_x + 0.052, 0.922, meta["label"], color=meta["accent"], fontsize=8.0, fontfamily=fonts["mono"], va="center")
250
290
  fig.text(0.048, 0.815, eyebrow, color=B["blue"], fontsize=7.2, fontfamily=fonts["mono"], va="top")
251
291
  if hero:
252
- fig.text(0.952, 0.932, _esc(hero), color=hero_color or B["blue2"], fontsize=22, fontfamily=fonts["display"], ha="right", va="center")
292
+ hp = None
293
+ try:
294
+ hp = _oracle_num_fp(26)
295
+ except Exception:
296
+ hp = None
297
+ ht = fig.text(
298
+ 0.952, 0.932, _esc(hero), color=hero_color or B["blue2"],
299
+ fontsize=26, fontfamily=fonts["mono"], fontweight="bold",
300
+ fontproperties=hp, ha="right", va="center", zorder=9,
301
+ parse_math=False,
302
+ )
303
+ if pair_label:
304
+ tp = None
305
+ try:
306
+ tp = _oracle_display_fp(20)
307
+ except Exception:
308
+ tp = None
309
+ # Sit flush left of the MC using the real glyph box.
310
+ gap = 0.014
311
+ x = 0.74
312
+ try:
313
+ fig.canvas.draw()
314
+ bb = ht.get_window_extent(renderer=fig.canvas.get_renderer())
315
+ x0, _ = fig.transFigure.inverted().transform((bb.x0, bb.y0))
316
+ x = x0 - gap
317
+ except Exception:
318
+ x = 0.952 - max(0.18, len(_esc(hero)) * 0.014)
319
+ fig.text(
320
+ x, 0.932, _esc(pair_label),
321
+ color=B["ink"], fontsize=20, fontfamily=fonts["display"],
322
+ fontweight="bold", fontproperties=tp, ha="right", va="center",
323
+ zorder=9,
324
+ )
253
325
  if chip:
254
326
  fig.text(0.952, 0.868, chip, color=B["blue2"], fontsize=8.2, fontfamily=fonts["mono"], ha="right", va="center")
255
327
  fig.add_artist(Line2D([0.045, 0.955], [0.072, 0.072], transform=fig.transFigure, color=B["line"], linewidth=1.0))
256
- fig.text(0.048, 0.040, os.environ.get("ORACLE_CARD_FOOTER", "oracle-agent"), color=B["blue2"], fontsize=6.8, fontfamily=fonts["mono"], fontweight="bold", va="center")
328
+ fig.text(0.048, 0.040, os.environ.get("ORACLE_CARD_FOOTER", "oracle.demi.la"), color=B["blue2"], fontsize=6.8, fontfamily=fonts["mono"], fontweight="bold", va="center")
257
329
  tag = fig.text(0.500, 0.040, "T H E F U T U R E I S A G E N T I C", color=B["blue2"], fontsize=6.8, fontfamily=fonts["display"], ha="center", va="center")
258
330
  tag.set_path_effects([pe.withStroke(linewidth=2.2, foreground=B["glow"], alpha=0.6)])
259
331
  right = f"{_esc(footer)} · " if footer else ""
@@ -285,6 +357,59 @@ def _bg(fig):
285
357
  pass
286
358
 
287
359
 
360
+ def render_spot_card(
361
+ *,
362
+ symbol: str,
363
+ venue: str = "robinhood",
364
+ hero: str | None = None,
365
+ chip: str | None = None,
366
+ mid: str | None = None,
367
+ sub: str | None = None,
368
+ reason: str = "OHLCV UNAVAILABLE",
369
+ out_path: str | None = None,
370
+ ) -> str | None:
371
+ """Oracle chrome with live stats but no fake candles.
372
+
373
+ Used when Gecko OHLCV 429s. Honest: no data, no fake lines.
374
+ """
375
+ import matplotlib
376
+
377
+ matplotlib.use("Agg")
378
+ import matplotlib.pyplot as plt
379
+
380
+ fonts = _init_fonts()
381
+ B = ORACLE
382
+ fig = plt.figure(figsize=(11.2, 6.3), dpi=160, facecolor=B["bg"])
383
+ _bg(fig)
384
+ ax = fig.add_axes([0.075, 0.135, 0.875, 0.68])
385
+ ax.set_facecolor(B["panel"])
386
+ ax.set_xticks([])
387
+ ax.set_yticks([])
388
+ for sp in ax.spines.values():
389
+ sp.set_visible(False)
390
+ sym = str(symbol or "TOKEN").lstrip("$").upper()
391
+ ax.text(0.06, 0.72, f"${sym}", color=B["ink"], fontsize=28, fontfamily=fonts["display"], fontweight="medium", transform=ax.transAxes)
392
+ if mid:
393
+ ax.text(0.06, 0.48, mid, color=B["blue2"], fontsize=18, fontfamily=fonts["display"], transform=ax.transAxes)
394
+ if sub:
395
+ ax.text(0.06, 0.30, sub, color=B["mute"], fontsize=12, fontfamily=fonts["mono"], transform=ax.transAxes)
396
+ ax.text(0.06, 0.12, reason, color=B["mute"], fontsize=8.5, fontfamily=fonts["mono"], transform=ax.transAxes)
397
+ _chrome(
398
+ fig,
399
+ venue=venue,
400
+ eyebrow=f"{sym} · SPOT",
401
+ hero=hero or "",
402
+ hero_color=B["blue2"],
403
+ chip=chip or "",
404
+ chip_bg=B["blue"],
405
+ footer=reason,
406
+ )
407
+ dest = out_path or str(Path(tempfile.gettempdir()) / f"oracle-spot-{sym[:12]}.png")
408
+ fig.savefig(dest, facecolor=fig.get_facecolor())
409
+ plt.close(fig)
410
+ return dest if Path(dest).exists() else None
411
+
412
+
288
413
  def render_ohlc_card(
289
414
  rows: list[dict],
290
415
  *,
@@ -302,6 +427,9 @@ def render_ohlc_card(
302
427
  hero: str | None = None,
303
428
  chip: str | None = None,
304
429
  chip_bg: str | None = None,
430
+ edge_px: float | None = None,
431
+ holders_delta_24h: int | None = None,
432
+ holders_now: int | None = None,
305
433
  ) -> str | None:
306
434
  """OHLC card with optional position overlays.
307
435
 
@@ -333,6 +461,17 @@ def render_ohlc_card(
333
461
  lows = [float(r["l"]) for r in rows]
334
462
  closes = [float(r["c"]) for r in rows]
335
463
  vols = [float(r.get("v") or 0) for r in rows]
464
+ last_c = closes[-1] if closes else 0.0
465
+ if last_c > 0:
466
+ keep = [i for i in range(len(closes)) if 0.4 * last_c <= closes[i] <= 2.5 * last_c]
467
+ if 8 <= len(keep) < len(closes):
468
+ xs = [xs[i] for i in keep]
469
+ opens = [opens[i] for i in keep]
470
+ highs = [highs[i] for i in keep]
471
+ lows = [lows[i] for i in keep]
472
+ closes = [closes[i] for i in keep]
473
+ vols = [vols[i] for i in keep]
474
+ rows = [rows[i] for i in keep]
336
475
  fig = plt.figure(figsize=(11.2, 6.3), dpi=160, facecolor=B["bg"])
337
476
  _bg(fig)
338
477
  ax = fig.add_axes([0.075, 0.195, 0.875, 0.575])
@@ -348,15 +487,25 @@ def render_ohlc_card(
348
487
  a.set_axisbelow(True)
349
488
  plt.setp(ax.get_xticklabels(), visible=False)
350
489
  xnum = mdates.date2num(xs)
351
- wide = (xnum[-1] - xnum[0]) / max(1, len(xnum)) * 0.66
490
+ deltas = [xnum[i] - xnum[i - 1] for i in range(1, len(xnum)) if xnum[i] > xnum[i - 1]]
491
+ med = sorted(deltas)[len(deltas) // 2] if deltas else (15.0 / 1440.0)
492
+ wide = min(max(med * 0.55, 4.0 / 1440.0), 18.0 / 1440.0)
493
+ last_px = closes[-1] if closes else 0.0
494
+ clip_wicks = last_px >= 20
352
495
  for i in range(len(rows)):
353
- up = closes[i] >= opens[i]
496
+ o, h, l, c = opens[i], highs[i], lows[i], closes[i]
497
+ if clip_wicks:
498
+ body_hi, body_lo = max(o, c), min(o, c)
499
+ cap = max(abs(c) * 0.012, (body_hi - body_lo) * 2.5, 0.15)
500
+ h = min(h, body_hi + cap)
501
+ l = max(l, body_lo - cap)
502
+ up = c >= o
354
503
  body_c = B["up"] if up else B["down"]
355
504
  wick_c = B["up_wick"] if up else B["down_wick"]
356
- ax.plot([xnum[i], xnum[i]], [lows[i], highs[i]], color=wick_c, linewidth=0.9, zorder=2, solid_capstyle="round")
357
- body_lo = min(opens[i], closes[i])
358
- body_h = abs(closes[i] - opens[i]) or (highs[i] - lows[i]) * 0.02 or 1e-12
359
- ax.add_patch(Rectangle((xnum[i] - wide / 2, body_lo), wide, body_h, facecolor=body_c, edgecolor=body_c, linewidth=0.5, zorder=3))
505
+ ax.plot([xnum[i], xnum[i]], [l, h], color=wick_c, linewidth=0.85, zorder=2, solid_capstyle="round")
506
+ body_lo = min(o, c)
507
+ body_h = abs(c - o) or (h - l) * 0.04 or abs(c) * 1e-4
508
+ ax.add_patch(Rectangle((xnum[i] - wide / 2, body_lo), wide, body_h, facecolor=body_c, edgecolor=body_c, linewidth=0.4, zorder=3))
360
509
  axv.bar(xnum[i], vols[i], width=wide, color=body_c, alpha=0.42, zorder=2)
361
510
  last = closes[-1]
362
511
  ax.axhline(last, color=B["blue"], linewidth=0.8, alpha=0.35, linestyle=(0, (4, 3)), zorder=4)
@@ -421,7 +570,7 @@ def render_ohlc_card(
421
570
  except Exception:
422
571
  continue
423
572
  ax.annotate(
424
- _esc(_fmt_px(last)), xy=(1.0, last), xycoords=("axes fraction", "data"),
573
+ _esc(_fmt_px(edge_px if edge_px is not None else last)), xy=(1.0, last), xycoords=("axes fraction", "data"),
425
574
  xytext=(4, 0), textcoords="offset points", va="center", ha="left",
426
575
  fontsize=7.0, fontfamily=fonts["mono"], fontweight="bold", color=B["chip_ink"],
427
576
  bbox={"boxstyle": "round,pad=0.30", "facecolor": B["blue2"], "edgecolor": "none"}, zorder=6, annotation_clip=False,
@@ -438,6 +587,8 @@ def render_ohlc_card(
438
587
  axv.xaxis_date()
439
588
  axv.xaxis.set_major_formatter(mdates.DateFormatter("%m-%d %H:%M"))
440
589
  axv.set_yticks([])
590
+ if last >= 1:
591
+ ax.yaxis.set_major_formatter(plt.FuncFormatter(lambda v, _p: f"{v:,.2f}"))
441
592
  # 24h % gain: change vs the close nearest 24h before the last bar.
442
593
  last_ts = xs[-1].timestamp()
443
594
  base = None
@@ -447,15 +598,29 @@ def render_ohlc_card(
447
598
  if base is None:
448
599
  base = closes[0]
449
600
  chg = (closes[-1] / base - 1) * 100 if base > 0 else 0
601
+ if last >= 20 and abs(chg) > 80:
602
+ chg = 0.0
603
+ if chip is None:
604
+ bits = []
605
+ if not (last >= 20 and abs((closes[-1] / base - 1) * 100 if base > 0 else 0) > 80):
606
+ bits.append(f"{chg:+.2f}% 24H")
607
+ elif chg != 0:
608
+ bits.append(f"{chg:+.2f}% 24H")
609
+ else:
610
+ bits.append("24H n/a")
611
+ if holders_delta_24h is not None:
612
+ bits.append(f"HLD {int(holders_delta_24h):+d}/24H")
613
+ elif holders_now is not None:
614
+ bits.append(f"HLD {int(holders_now)}")
615
+ chip = " · ".join(bits)
450
616
  _chip_bg = chip_bg or (B["up"] if chg >= 0 else B["down"])
451
- eyebrow_bits = [str(pair or symbol).upper()]
452
- if market:
453
- eyebrow_bits.append(str(market).upper())
454
617
  _chrome(
455
- fig, venue=venue, eyebrow=" · ".join(eyebrow_bits),
618
+ fig, venue=venue, eyebrow="",
456
619
  hero=hero if hero is not None else f"{_fmt_px(last)}", hero_color=B["blue2"],
457
- chip=chip if chip is not None else f"{chg:+.2f}% 24H", chip_bg=_chip_bg,
620
+ chip=chip,
621
+ chip_bg=_chip_bg,
458
622
  footer=f"{source} · CANDLES · {chain_meta(venue)['label']}",
623
+ pair_label=str(pair or "").upper(),
459
624
  )
460
625
  out = out_path or str(Path(tempfile.gettempdir()) / f"oracle-card-{venue}-{symbol}.png")
461
626
  fig.savefig(out, facecolor=fig.get_facecolor(), bbox_inches="tight", pad_inches=0.08)
@@ -598,7 +763,16 @@ def render_stat_card(
598
763
  cx = (i % per_row) * tile_w + tile_w / 2
599
764
  cy = 0.985 - (i // per_row) * tile_h
600
765
  ax.text(cx, cy - 0.055, str(lab).upper(), ha="center", va="top", fontsize=7.0, fontfamily=fonts["mono"], color=B["mute"])
601
- ax.text(cx, cy - 0.115, _esc(str(val)), ha="center", va="top", fontsize=15, fontfamily=fonts["display"], color=B["ink"])
766
+ np_ = None
767
+ try:
768
+ np_ = _oracle_num_fp(15)
769
+ except Exception:
770
+ np_ = None
771
+ ax.text(
772
+ cx, cy - 0.115, _esc(str(val)), ha="center", va="top",
773
+ fontsize=15, fontfamily=fonts["mono"], fontweight="bold",
774
+ fontproperties=np_, color=B["ink"], parse_math=False,
775
+ )
602
776
  table_top = 0.985 - n_tile_rows * tile_h - 0.03
603
777
  else:
604
778
  table_top = 0.95
@@ -669,6 +843,7 @@ def render_nft_card(
669
843
  volume_24h: float | None = None,
670
844
  sales_24h: int | None = None,
671
845
  listed: int | None = None,
846
+ listed_plus: bool = False,
672
847
  supply: int | None = None,
673
848
  owners: int | None = None,
674
849
  top_offer: float | None = None,
@@ -736,23 +911,33 @@ def render_nft_card(
736
911
  src = str(tmp_img)
737
912
  from PIL import Image
738
913
 
914
+ raw = Path(src).read_bytes()[:200]
915
+ if src.lower().endswith(".svg") or raw.lstrip().startswith(b"<svg") or b"<svg" in raw[:80]:
916
+ png = Path(src).with_suffix(".png")
917
+ import subprocess
918
+ subprocess.run(
919
+ ["rsvg-convert", "-w", "720", "-h", "720", "-o", str(png), src],
920
+ check=True, timeout=12, capture_output=True,
921
+ )
922
+ src = str(png)
739
923
  img_obj = Image.open(src).convert("RGB")
740
924
  except Exception:
741
925
  img_obj = None
742
926
 
743
- # Left: NFT image panel. Right: floor chart. Bottom: stat strip.
927
+ # Left: NFT image, flush under the collection name. Right: floor chart.
744
928
  chart_left = 0.075
929
+ panel_y, panel_h = 0.228, 0.538
745
930
  if img_obj is not None:
746
- iax = fig.add_axes([0.048, 0.245, 0.235, 0.520], zorder=5)
931
+ iax = fig.add_axes([0.048, panel_y, 0.248, panel_h], zorder=5)
747
932
  iax.imshow(img_obj)
748
933
  iax.set_xticks([])
749
934
  iax.set_yticks([])
750
935
  for sp in iax.spines.values():
751
936
  sp.set_color(B["blue"])
752
- sp.set_linewidth(1.2)
937
+ sp.set_linewidth(1.15)
753
938
  sp.set_alpha(0.55)
754
939
  chart_left = 0.318
755
- ax = fig.add_axes([chart_left, 0.245, 0.952 - chart_left, 0.520])
940
+ ax = fig.add_axes([chart_left, panel_y, 0.952 - chart_left, panel_h])
756
941
  ax.set_facecolor(B["panel"])
757
942
  ax.tick_params(colors=B["mute"], labelsize=6.8)
758
943
  for lbl in ax.get_xticklabels() + ax.get_yticklabels():
@@ -805,13 +990,23 @@ def render_nft_card(
805
990
  ax.text(0.5, 0.5, "NO FLOOR HISTORY", ha="center", va="center", fontsize=9, fontfamily=fonts["mono"], color=B["mute"], transform=ax.transAxes)
806
991
  # Bottom stat strip.
807
992
  chg_txt = f"{floor_chg_24h:+.1f}%" if floor_chg_24h is not None else "UNKNOWN"
993
+ if listed is None:
994
+ listed_txt = "UNKNOWN"
995
+ elif listed_plus and supply:
996
+ listed_txt = f"{listed:,}+/{supply:,}"
997
+ elif listed_plus:
998
+ listed_txt = f"{listed:,}+"
999
+ elif supply:
1000
+ listed_txt = f"{listed:,}/{supply:,}"
1001
+ else:
1002
+ listed_txt = f"{listed:,}"
808
1003
  strip = [
809
1004
  ("FLOOR", fmt(floor if floor is not None else (fy[-1] if fy else None))),
810
1005
  ("24H %", chg_txt),
811
1006
  ("TOP OFFER", fmt(top_offer)),
812
1007
  ("24H VOL", fmt(volume_24h)),
813
1008
  ("24H SALES", f"{sales_24h:,}" if sales_24h is not None else "UNKNOWN"),
814
- ("LISTED", f"{listed:,}/{supply:,}" if listed is not None and supply else (f"{listed:,}" if listed is not None else "UNKNOWN")),
1009
+ ("LISTED", listed_txt),
815
1010
  ("HOLDERS", f"{owners:,}" if owners is not None else "UNKNOWN"),
816
1011
  ]
817
1012
  n = len(strip)
@@ -821,7 +1016,16 @@ def render_nft_card(
821
1016
  if lab == "24H %" and floor_chg_24h is not None:
822
1017
  col = B["up"] if floor_chg_24h >= 0 else B["down"]
823
1018
  fig.text(cx, 0.185, lab, ha="center", va="center", fontsize=6.2, fontfamily=fonts["mono"], color=B["mute"])
824
- fig.text(cx, 0.135, _esc(str(val)), ha="center", va="center", fontsize=9.8, fontfamily=fonts["display"], color=col)
1019
+ np_ = None
1020
+ try:
1021
+ np_ = _oracle_num_fp(12.5)
1022
+ except Exception:
1023
+ np_ = None
1024
+ fig.text(
1025
+ cx, 0.135, _esc(str(val)), ha="center", va="center",
1026
+ fontsize=12.5, fontfamily=fonts["mono"], fontweight="bold",
1027
+ fontproperties=np_, color=col, parse_math=False,
1028
+ )
825
1029
  chip_bits = []
826
1030
  if floor_chg_24h is not None:
827
1031
  chip_bits.append(f"{floor_chg_24h:+.1f}% 24H")
@@ -835,24 +1039,23 @@ def render_nft_card(
835
1039
  chip_color = B["up"] if floor_chg_24h >= 0 else B["down"]
836
1040
  hero_color = ORACLE["blue2"]
837
1041
  _chrome(
838
- fig, venue=venue, eyebrow="" if img_obj is not None else collection[:60],
1042
+ fig, venue=venue, eyebrow="",
839
1043
  hero=fmt(floor if floor is not None else (fy[-1] if fy else None)),
840
1044
  hero_color=hero_color, chip=chip, chip_bg=chip_color,
841
1045
  footer=source,
842
1046
  )
843
- # Collection PFP thumb + name beside the eyebrow (like the venue lockup row).
844
- if img_obj is not None:
845
- try:
846
- pax = fig.add_axes([0.048, 0.786, 0.030, 0.053], zorder=9)
847
- pax.imshow(img_obj)
848
- pax.set_xticks([])
849
- pax.set_yticks([])
850
- for sp in pax.spines.values():
851
- sp.set_color(B["blue"])
852
- sp.set_linewidth(0.9)
853
- fig.text(0.086, 0.812, collection[:60], color=B["blue"], fontsize=8.4, fontfamily=fonts["mono"], fontweight="bold", va="center")
854
- except Exception:
855
- fig.text(0.048, 0.812, collection[:60], color=B["blue"], fontsize=7.2, fontfamily=fonts["mono"], va="center")
1047
+ # Collection name: Oracle display (Cormorant 700), ice-blue like IF/USD.
1048
+ name = collection[:44].upper()
1049
+ t = fig.text(
1050
+ 0.048, 0.800, name, color=B["blue2"],
1051
+ fontproperties=_oracle_display_fp(22.5),
1052
+ va="center", ha="left", zorder=9,
1053
+ )
1054
+ try:
1055
+ from matplotlib import patheffects as pe
1056
+ t.set_path_effects([pe.withStroke(linewidth=2.4, foreground=B["glow"], alpha=0.55)])
1057
+ except Exception:
1058
+ pass
856
1059
  out = out_path or str(Path(tempfile.gettempdir()) / f"oracle-nft-{venue}.png")
857
1060
  fig.savefig(out, facecolor=fig.get_facecolor(), bbox_inches="tight", pad_inches=0.08)
858
1061
  plt.close(fig)
@@ -97,7 +97,7 @@ does not prove npm users receive it. A locally hosted model/data/policy loop doe
97
97
  a bundled signer. A merged feature is not public until the npm artifact containing it is
98
98
  published and inspected.
99
99
 
100
- **Candidate release in this source tree:** public package `@oracle-agent/oracle@0.25.1`
100
+ **Candidate release in this source tree:** public package `@oracle-agent/oracle@0.25.2`
101
101
  ships the encrypted local vault + short-lived loopback signer. Hosted/keyless
102
102
  Oracle cannot hold keys or move funds. Self-host after `oracle sign init|import` is **auto-armed**
103
103
  for user-initiated actions. Public still charges the disclosed fee card; Administrator