adsinagents 0.1.11 → 0.1.12

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/cli/dist/index.js CHANGED
@@ -4124,7 +4124,16 @@ var AdSchema = external_exports.object({
4124
4124
  * the daemon is running standalone (no server) — those imps earn nothing.
4125
4125
  * See shared/src/nonce.ts.
4126
4126
  */
4127
- nonce: external_exports.string().default("")
4127
+ nonce: external_exports.string().default(""),
4128
+ /**
4129
+ * Publisher payout RATES for this ad, in millicents, computed server-side from
4130
+ * the live CPM + share + click multiplier (earnings.ts) so the device never
4131
+ * hardcodes the math — change the campaign cpm and these move with it. Shown in
4132
+ * the opt-in payout segment ("+$0.002/view · +$0.10/click"). Default 0 so older
4133
+ * ads / standalone daemons (no server rate) simply render nothing.
4134
+ */
4135
+ payoutPerImpMillicents: external_exports.number().min(0).default(0),
4136
+ payoutPerClickMillicents: external_exports.number().min(0).default(0)
4128
4137
  });
4129
4138
  var GravityAdSchema = external_exports.object({
4130
4139
  adText: external_exports.string(),
@@ -4166,16 +4175,20 @@ var ConfigSchema = external_exports.object({
4166
4175
  deviceId: external_exports.string().default(""),
4167
4176
  /** Single-use token to claim this install at /claim (attach a login + payout). */
4168
4177
  claimToken: external_exports.string().default(""),
4169
- /** Ad cache poll interval (seconds) — how often a new ad rotates in. Billing
4170
- * is gated separately by rateCapSec, so a faster poll rotates creatives
4171
- * without changing earnings. */
4178
+ /** Ad cache poll interval (seconds) — how often a new ad rotates in. Synced
4179
+ * with the fire: rateCapSec defaults to the SAME value, so every new ad fires
4180
+ * exactly once (no silent "every other ad skipped"). The next ad is preloaded
4181
+ * a few seconds early so the swap is instant. */
4172
4182
  pollIntervalSec: external_exports.number().int().positive().default(30),
4173
4183
  /** Hard daily fired-impression cap. */
4174
4184
  dailyCap: external_exports.number().int().positive().default(300),
4175
- /** Min seconds of verified view between two fired impressions. */
4176
- rateCapSec: external_exports.number().int().positive().default(60),
4177
- /** Continuous focus seconds required before an impression verifies. */
4178
- focusDwellSec: external_exports.number().int().positive().default(5),
4185
+ /** Min seconds of verified view between two fired impressions. Defaults to the
4186
+ * rotation period (30s) so fire == rotation: each new ad booms once. */
4187
+ rateCapSec: external_exports.number().int().positive().default(30),
4188
+ /** Continuous focus seconds before an impression verifies. 1s = the IAB/MRC
4189
+ * viewability standard (≥1 continuous second in view). A real glance counts;
4190
+ * a sub-second flick-past still doesn't bill the advertiser. */
4191
+ focusDwellSec: external_exports.number().int().positive().default(1),
4179
4192
  /**
4180
4193
  * Seconds the ad stays painted after a turn ends (Stop hook). Past the grace
4181
4194
  * the status line goes clean until the next prompt. 0 = hide on Stop.
@@ -4186,6 +4199,8 @@ var ConfigSchema = external_exports.object({
4186
4199
  killSwitchUrl: external_exports.string().default(""),
4187
4200
  /** Show a live earnings segment in the status line (separate from the ad). Off by default. */
4188
4201
  showEarnings: external_exports.boolean().default(false),
4202
+ /** Show the per-event payout rate for the current ad ("+$0.002/view · +$0.10/click"). On by default. */
4203
+ showPayout: external_exports.boolean().default(true),
4189
4204
  /** Terminal styling for ad/earnings segments. Plain (no ANSI) by default. */
4190
4205
  theme: ThemeSchema.default("plain")
4191
4206
  });
@@ -4216,7 +4231,18 @@ var DaemonStateSchema = external_exports.object({
4216
4231
  rotatedAtMs: external_exports.number().int().nonnegative().default(0),
4217
4232
  /** Rotation period in ms (== pollIntervalSec * 1000). Bar denominator.
4218
4233
  * Additive — 0 from older daemons hides the bar. */
4219
- rotationMs: external_exports.number().int().nonnegative().default(0)
4234
+ rotationMs: external_exports.number().int().nonnegative().default(0),
4235
+ /** Unix ms of the last counted impression fire. Anchors the transient
4236
+ * "+$" impact flash; the statusline shows it while now-this < IMPACT_FLASH_MS.
4237
+ * Additive — 0 from older daemons hides the flash. */
4238
+ lastImpFiredAtMs: external_exports.number().int().nonnegative().default(0),
4239
+ /** Per-impression payout (millicents) of the last counted fire — the amount
4240
+ * the impact flash pops. Additive — 0 hides the flash. */
4241
+ lastImpEarnedMillicents: external_exports.number().int().nonnegative().default(0),
4242
+ /** Viewability machine's current reason string (why this tick did/didn't
4243
+ * fire). Drives the "why no boom" hint when the rotation bar is near full.
4244
+ * Additive — "" from older daemons hides the hint. */
4245
+ viewReason: external_exports.string().default("")
4220
4246
  });
4221
4247
  var ImpressionSchema = external_exports.object({
4222
4248
  id: external_exports.string(),
@@ -4131,7 +4131,16 @@ var AdSchema = external_exports.object({
4131
4131
  * the daemon is running standalone (no server) — those imps earn nothing.
4132
4132
  * See shared/src/nonce.ts.
4133
4133
  */
4134
- nonce: external_exports.string().default("")
4134
+ nonce: external_exports.string().default(""),
4135
+ /**
4136
+ * Publisher payout RATES for this ad, in millicents, computed server-side from
4137
+ * the live CPM + share + click multiplier (earnings.ts) so the device never
4138
+ * hardcodes the math — change the campaign cpm and these move with it. Shown in
4139
+ * the opt-in payout segment ("+$0.002/view · +$0.10/click"). Default 0 so older
4140
+ * ads / standalone daemons (no server rate) simply render nothing.
4141
+ */
4142
+ payoutPerImpMillicents: external_exports.number().min(0).default(0),
4143
+ payoutPerClickMillicents: external_exports.number().min(0).default(0)
4135
4144
  });
4136
4145
  var GravityAdSchema = external_exports.object({
4137
4146
  adText: external_exports.string(),
@@ -4173,16 +4182,20 @@ var ConfigSchema = external_exports.object({
4173
4182
  deviceId: external_exports.string().default(""),
4174
4183
  /** Single-use token to claim this install at /claim (attach a login + payout). */
4175
4184
  claimToken: external_exports.string().default(""),
4176
- /** Ad cache poll interval (seconds) — how often a new ad rotates in. Billing
4177
- * is gated separately by rateCapSec, so a faster poll rotates creatives
4178
- * without changing earnings. */
4185
+ /** Ad cache poll interval (seconds) — how often a new ad rotates in. Synced
4186
+ * with the fire: rateCapSec defaults to the SAME value, so every new ad fires
4187
+ * exactly once (no silent "every other ad skipped"). The next ad is preloaded
4188
+ * a few seconds early so the swap is instant. */
4179
4189
  pollIntervalSec: external_exports.number().int().positive().default(30),
4180
4190
  /** Hard daily fired-impression cap. */
4181
4191
  dailyCap: external_exports.number().int().positive().default(300),
4182
- /** Min seconds of verified view between two fired impressions. */
4183
- rateCapSec: external_exports.number().int().positive().default(60),
4184
- /** Continuous focus seconds required before an impression verifies. */
4185
- focusDwellSec: external_exports.number().int().positive().default(5),
4192
+ /** Min seconds of verified view between two fired impressions. Defaults to the
4193
+ * rotation period (30s) so fire == rotation: each new ad booms once. */
4194
+ rateCapSec: external_exports.number().int().positive().default(30),
4195
+ /** Continuous focus seconds before an impression verifies. 1s = the IAB/MRC
4196
+ * viewability standard (≥1 continuous second in view). A real glance counts;
4197
+ * a sub-second flick-past still doesn't bill the advertiser. */
4198
+ focusDwellSec: external_exports.number().int().positive().default(1),
4186
4199
  /**
4187
4200
  * Seconds the ad stays painted after a turn ends (Stop hook). Past the grace
4188
4201
  * the status line goes clean until the next prompt. 0 = hide on Stop.
@@ -4193,11 +4206,13 @@ var ConfigSchema = external_exports.object({
4193
4206
  killSwitchUrl: external_exports.string().default(""),
4194
4207
  /** Show a live earnings segment in the status line (separate from the ad). Off by default. */
4195
4208
  showEarnings: external_exports.boolean().default(false),
4209
+ /** Show the per-event payout rate for the current ad ("+$0.002/view · +$0.10/click"). On by default. */
4210
+ showPayout: external_exports.boolean().default(true),
4196
4211
  /** Terminal styling for ad/earnings segments. Plain (no ANSI) by default. */
4197
4212
  theme: ThemeSchema.default("plain")
4198
4213
  });
4199
4214
  var DEFAULT_CONFIG = ConfigSchema.parse({});
4200
- var DAEMON_STATE_SCHEMA_VERSION = 1;
4215
+ var DAEMON_STATE_SCHEMA_VERSION = 2;
4201
4216
  var DaemonStateSchema = external_exports.object({
4202
4217
  /** Contract version (see DAEMON_STATE_SCHEMA_VERSION). Missing => 1. */
4203
4218
  schemaVersion: external_exports.number().int().positive().default(1),
@@ -4224,7 +4239,18 @@ var DaemonStateSchema = external_exports.object({
4224
4239
  rotatedAtMs: external_exports.number().int().nonnegative().default(0),
4225
4240
  /** Rotation period in ms (== pollIntervalSec * 1000). Bar denominator.
4226
4241
  * Additive — 0 from older daemons hides the bar. */
4227
- rotationMs: external_exports.number().int().nonnegative().default(0)
4242
+ rotationMs: external_exports.number().int().nonnegative().default(0),
4243
+ /** Unix ms of the last counted impression fire. Anchors the transient
4244
+ * "+$" impact flash; the statusline shows it while now-this < IMPACT_FLASH_MS.
4245
+ * Additive — 0 from older daemons hides the flash. */
4246
+ lastImpFiredAtMs: external_exports.number().int().nonnegative().default(0),
4247
+ /** Per-impression payout (millicents) of the last counted fire — the amount
4248
+ * the impact flash pops. Additive — 0 hides the flash. */
4249
+ lastImpEarnedMillicents: external_exports.number().int().nonnegative().default(0),
4250
+ /** Viewability machine's current reason string (why this tick did/didn't
4251
+ * fire). Drives the "why no boom" hint when the rotation bar is near full.
4252
+ * Additive — "" from older daemons hides the hint. */
4253
+ viewReason: external_exports.string().default("")
4228
4254
  });
4229
4255
  var ImpressionSchema = external_exports.object({
4230
4256
  id: external_exports.string(),
@@ -5122,11 +5148,44 @@ var AdSource = class {
5122
5148
  this.cfg = cfg;
5123
5149
  }
5124
5150
  rotation = 0;
5151
+ // Preloaded next ad — fetched a few seconds before the swap so the rotation is
5152
+ // instant (no network gap, no "loading" flicker). Holding it is NOT a bill:
5153
+ // the impression only fires once this ad becomes the visible one and the
5154
+ // viewability gates pass. Fetch != fire.
5155
+ preloaded = null;
5156
+ preloading = false;
5125
5157
  setConfig(cfg) {
5126
5158
  this.cfg = cfg;
5127
5159
  }
5128
- /** Fetch the next ad. Returns null on explicit no-fill. */
5160
+ /**
5161
+ * Fetch the next ad. Returns null on explicit no-fill. Consumes a preloaded ad
5162
+ * if one is in hand (instant swap); otherwise fetches synchronously.
5163
+ */
5129
5164
  async next(sessionId, nowMs) {
5165
+ if (this.preloaded) {
5166
+ const ad = this.preloaded;
5167
+ this.preloaded = null;
5168
+ return ad;
5169
+ }
5170
+ return this.fetchOne(sessionId, nowMs);
5171
+ }
5172
+ /**
5173
+ * Warm the next ad into the buffer ahead of the swap. Idempotent + non-blocking
5174
+ * for the caller: if a fetch is already in flight or one is buffered, no-op.
5175
+ * Call this a few seconds before the rotation deadline.
5176
+ */
5177
+ async preloadNext(sessionId, nowMs) {
5178
+ if (this.preloaded || this.preloading) return;
5179
+ this.preloading = true;
5180
+ try {
5181
+ const ad = await this.fetchOne(sessionId, nowMs);
5182
+ if (ad) this.preloaded = ad;
5183
+ } finally {
5184
+ this.preloading = false;
5185
+ }
5186
+ }
5187
+ /** One ad from the server, or the built-in test rotation if unreachable. */
5188
+ async fetchOne(sessionId, nowMs) {
5130
5189
  const fromServer = await this.fromServer(sessionId, nowMs);
5131
5190
  if (fromServer !== void 0) return fromServer;
5132
5191
  return this.builtinTestAd(nowMs);
@@ -5365,6 +5424,7 @@ function pendingReason(s) {
5365
5424
 
5366
5425
  // ../daemon/src/main.ts
5367
5426
  var TICK_MS = 1e3;
5427
+ var AD_PRELOAD_LEAD_MS = 3e3;
5368
5428
  var SESSION_REAP_MS = 6 * 60 * 60 * 1e3;
5369
5429
  var SYNC_EVERY_TICKS = 30;
5370
5430
  var UNKNOWN_TERM_WARN_MS = 10 * 60 * 1e3;
@@ -5387,6 +5447,8 @@ async function main() {
5387
5447
  let currentAd = null;
5388
5448
  let lastPollMs = 0;
5389
5449
  let tickCount = 0;
5450
+ let lastImpFiredAtMs = 0;
5451
+ let lastImpEarnedMillicents = 0;
5390
5452
  const unknownTermWarnedAt = /* @__PURE__ */ new Map();
5391
5453
  let earnings = { balanceCents: 0, todayCents: 0 };
5392
5454
  const now = () => Date.now();
@@ -5414,7 +5476,15 @@ async function main() {
5414
5476
  if (tickCount % 10 === 0) await killSwitch.refreshRemote();
5415
5477
  const disabled = killSwitch.disabled() || cfg.placement === "off";
5416
5478
  const sessionLive = sessions.sessionLive();
5417
- if (!disabled && sessionLive && nowMs - lastPollMs >= cfg.pollIntervalSec * 1e3) {
5479
+ const pollMs = cfg.pollIntervalSec * 1e3;
5480
+ if (!disabled && sessionLive && lastPollMs > 0) {
5481
+ const untilSwap = pollMs - (nowMs - lastPollMs);
5482
+ if (untilSwap <= AD_PRELOAD_LEAD_MS && untilSwap > 0) {
5483
+ const sid = sessions.currentSessionId() ?? "anonymous";
5484
+ void adSource.preloadNext(sid, nowMs).catch((e) => fileLog(`preload error: ${String(e)}`));
5485
+ }
5486
+ }
5487
+ if (!disabled && sessionLive && nowMs - lastPollMs >= pollMs) {
5418
5488
  lastPollMs = nowMs;
5419
5489
  const sid = sessions.currentSessionId() ?? "anonymous";
5420
5490
  try {
@@ -5481,6 +5551,8 @@ async function main() {
5481
5551
  if (isNew) {
5482
5552
  view = markFired(view, result.fireAdId, nowMs);
5483
5553
  if (currentAd.impUrl) firePixel(currentAd.impUrl);
5554
+ lastImpFiredAtMs = nowMs;
5555
+ lastImpEarnedMillicents = currentAd.payoutPerImpMillicents;
5484
5556
  await fileLog(`FIRED ${result.fireAdId} (${currentAd.brandName})`);
5485
5557
  }
5486
5558
  }
@@ -5497,7 +5569,11 @@ async function main() {
5497
5569
  // Anchor + period for the statusline rotation countdown bar. lastPollMs
5498
5570
  // is the last ad swap; the bar fills over pollIntervalSec toward the next.
5499
5571
  rotatedAtMs: lastPollMs,
5500
- rotationMs: cfg.pollIntervalSec * 1e3
5572
+ rotationMs: cfg.pollIntervalSec * 1e3,
5573
+ lastImpFiredAtMs,
5574
+ lastImpEarnedMillicents,
5575
+ // Why this tick did/didn't fire — drives the "why no boom" hint.
5576
+ viewReason: result.reason
5501
5577
  };
5502
5578
  writeDaemonState(state);
5503
5579
  if (tickCount % SYNC_EVERY_TICKS === 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "adsinagents",
3
- "version": "0.1.11",
3
+ "version": "0.1.12",
4
4
  "description": "Get paid while you build. AdsInAgents is the terminal-native ad layer for AI coding agents. Verified impressions pay you.",
5
5
  "homepage": "https://adsinagents.com",
6
6
  "license": "UNLICENSED",
@@ -4108,7 +4108,16 @@ var AdSchema = external_exports.object({
4108
4108
  * the daemon is running standalone (no server) — those imps earn nothing.
4109
4109
  * See shared/src/nonce.ts.
4110
4110
  */
4111
- nonce: external_exports.string().default("")
4111
+ nonce: external_exports.string().default(""),
4112
+ /**
4113
+ * Publisher payout RATES for this ad, in millicents, computed server-side from
4114
+ * the live CPM + share + click multiplier (earnings.ts) so the device never
4115
+ * hardcodes the math — change the campaign cpm and these move with it. Shown in
4116
+ * the opt-in payout segment ("+$0.002/view · +$0.10/click"). Default 0 so older
4117
+ * ads / standalone daemons (no server rate) simply render nothing.
4118
+ */
4119
+ payoutPerImpMillicents: external_exports.number().min(0).default(0),
4120
+ payoutPerClickMillicents: external_exports.number().min(0).default(0)
4112
4121
  });
4113
4122
  var GravityAdSchema = external_exports.object({
4114
4123
  adText: external_exports.string(),
@@ -4150,16 +4159,20 @@ var ConfigSchema = external_exports.object({
4150
4159
  deviceId: external_exports.string().default(""),
4151
4160
  /** Single-use token to claim this install at /claim (attach a login + payout). */
4152
4161
  claimToken: external_exports.string().default(""),
4153
- /** Ad cache poll interval (seconds) — how often a new ad rotates in. Billing
4154
- * is gated separately by rateCapSec, so a faster poll rotates creatives
4155
- * without changing earnings. */
4162
+ /** Ad cache poll interval (seconds) — how often a new ad rotates in. Synced
4163
+ * with the fire: rateCapSec defaults to the SAME value, so every new ad fires
4164
+ * exactly once (no silent "every other ad skipped"). The next ad is preloaded
4165
+ * a few seconds early so the swap is instant. */
4156
4166
  pollIntervalSec: external_exports.number().int().positive().default(30),
4157
4167
  /** Hard daily fired-impression cap. */
4158
4168
  dailyCap: external_exports.number().int().positive().default(300),
4159
- /** Min seconds of verified view between two fired impressions. */
4160
- rateCapSec: external_exports.number().int().positive().default(60),
4161
- /** Continuous focus seconds required before an impression verifies. */
4162
- focusDwellSec: external_exports.number().int().positive().default(5),
4169
+ /** Min seconds of verified view between two fired impressions. Defaults to the
4170
+ * rotation period (30s) so fire == rotation: each new ad booms once. */
4171
+ rateCapSec: external_exports.number().int().positive().default(30),
4172
+ /** Continuous focus seconds before an impression verifies. 1s = the IAB/MRC
4173
+ * viewability standard (≥1 continuous second in view). A real glance counts;
4174
+ * a sub-second flick-past still doesn't bill the advertiser. */
4175
+ focusDwellSec: external_exports.number().int().positive().default(1),
4163
4176
  /**
4164
4177
  * Seconds the ad stays painted after a turn ends (Stop hook). Past the grace
4165
4178
  * the status line goes clean until the next prompt. 0 = hide on Stop.
@@ -4170,6 +4183,8 @@ var ConfigSchema = external_exports.object({
4170
4183
  killSwitchUrl: external_exports.string().default(""),
4171
4184
  /** Show a live earnings segment in the status line (separate from the ad). Off by default. */
4172
4185
  showEarnings: external_exports.boolean().default(false),
4186
+ /** Show the per-event payout rate for the current ad ("+$0.002/view · +$0.10/click"). On by default. */
4187
+ showPayout: external_exports.boolean().default(true),
4173
4188
  /** Terminal styling for ad/earnings segments. Plain (no ANSI) by default. */
4174
4189
  theme: ThemeSchema.default("plain")
4175
4190
  });
@@ -4200,7 +4215,18 @@ var DaemonStateSchema = external_exports.object({
4200
4215
  rotatedAtMs: external_exports.number().int().nonnegative().default(0),
4201
4216
  /** Rotation period in ms (== pollIntervalSec * 1000). Bar denominator.
4202
4217
  * Additive — 0 from older daemons hides the bar. */
4203
- rotationMs: external_exports.number().int().nonnegative().default(0)
4218
+ rotationMs: external_exports.number().int().nonnegative().default(0),
4219
+ /** Unix ms of the last counted impression fire. Anchors the transient
4220
+ * "+$" impact flash; the statusline shows it while now-this < IMPACT_FLASH_MS.
4221
+ * Additive — 0 from older daemons hides the flash. */
4222
+ lastImpFiredAtMs: external_exports.number().int().nonnegative().default(0),
4223
+ /** Per-impression payout (millicents) of the last counted fire — the amount
4224
+ * the impact flash pops. Additive — 0 hides the flash. */
4225
+ lastImpEarnedMillicents: external_exports.number().int().nonnegative().default(0),
4226
+ /** Viewability machine's current reason string (why this tick did/didn't
4227
+ * fire). Drives the "why no boom" hint when the rotation bar is near full.
4228
+ * Additive — "" from older daemons hides the hint. */
4229
+ viewReason: external_exports.string().default("")
4204
4230
  });
4205
4231
  var ImpressionSchema = external_exports.object({
4206
4232
  id: external_exports.string(),
@@ -4240,11 +4266,16 @@ var DIM = (s) => `\x1B[2m${s}\x1B[22m`;
4240
4266
  var BOLD = (s) => `\x1B[1m${s}\x1B[22m`;
4241
4267
  var GREEN = (s) => `\x1B[32m${s}\x1B[39m`;
4242
4268
  var AMBER = (s) => `\x1B[38;5;215m${s}\x1B[39m`;
4269
+ var FLASH = (s) => `\x1B[1;92m${s}\x1B[22;39m`;
4243
4270
  var THEMES = {
4244
- plain: { disclosure: ID, mark: ID, brand: ID, money: ID },
4245
- dim: { disclosure: DIM, mark: ID, brand: ID, money: ID },
4246
- accent: { disclosure: DIM, mark: AMBER, brand: BOLD, money: GREEN }
4271
+ // plain MUST emit zero ANSI (VS Code) the flash/hint are plain text there,
4272
+ // still appearing/disappearing, just uncolored.
4273
+ plain: { disclosure: ID, mark: ID, brand: ID, money: ID, flash: ID, hint: ID },
4274
+ // hint = amber even in dim, so an actionable "why no boom" block stands out.
4275
+ dim: { disclosure: DIM, mark: ID, brand: ID, money: ID, flash: FLASH, hint: AMBER },
4276
+ accent: { disclosure: DIM, mark: AMBER, brand: BOLD, money: GREEN, flash: FLASH, hint: AMBER }
4247
4277
  };
4278
+ var IMPACT_FLASH_MS = 3e3;
4248
4279
  var MAX_AD_TEXT = 60;
4249
4280
  function renderAdSegment(ad, opts) {
4250
4281
  const t = THEMES[opts.theme ?? "plain"];
@@ -4254,13 +4285,57 @@ function renderAdSegment(ad, opts) {
4254
4285
  const label = ad.brandName ? `${text} \xB7 ${t.brand(ad.brandName)}` : text;
4255
4286
  const clickUrl = `${opts.clickBase}/click/${encodeURIComponent(ad.id)}`;
4256
4287
  const linked = opts.supportsOsc8 ? osc8(clickUrl, `${star} ${label} ${arrow}`) : opts.plainLink ? `${star} ${label} \xB7 ${opts.plainLink} ${arrow}` : `${star} ${label} ${arrow}`;
4257
- return `${t.disclosure("Ad:")} ${linked} ${t.disclosure("\xB7 sponsored")}`;
4288
+ return `${t.disclosure("Ad:")} ${linked}`;
4258
4289
  }
4259
4290
  function renderEarningsSegment(balanceCents, todayCents, theme = "plain") {
4260
4291
  const t = THEMES[theme];
4261
4292
  const d = (c) => t.money(`$${(Math.max(0, c) / 100).toFixed(2)}`);
4262
4293
  return `${d(todayCents)} today \xB7 ${d(balanceCents)}`;
4263
4294
  }
4295
+ function renderPayoutSegment(perImpMillicents, perClickMillicents, theme = "plain") {
4296
+ const imp = Math.max(0, perImpMillicents);
4297
+ const click = Math.max(0, perClickMillicents);
4298
+ if (imp === 0 && click === 0)
4299
+ return "";
4300
+ const t = THEMES[theme];
4301
+ const fmt = (millicents) => {
4302
+ const dollars = millicents / 1e5;
4303
+ const s = dollars >= 0.01 ? dollars.toFixed(2) : dollars.toFixed(4).replace(/0+$/, "");
4304
+ return `$${s}`;
4305
+ };
4306
+ return `${t.money(`+${fmt(imp)}`)}/view \xB7 ${t.money(`+${fmt(click)}`)}/click`;
4307
+ }
4308
+ function renderImpactFlash(nowMs, firedAtMs, earnedMillicents, theme = "plain") {
4309
+ if (firedAtMs <= 0 || earnedMillicents <= 0)
4310
+ return "";
4311
+ if (nowMs - firedAtMs > IMPACT_FLASH_MS || nowMs < firedAtMs)
4312
+ return "";
4313
+ const dollars = earnedMillicents / 1e5;
4314
+ const s = dollars >= 0.01 ? dollars.toFixed(2) : dollars.toFixed(4).replace(/0+$/, "");
4315
+ return THEMES[theme].flash(`+$${s}`);
4316
+ }
4317
+ function classifyEarningState(reason) {
4318
+ if (reason === "verified \u2014 fire" || reason === "already fired this ad" || reason === "rate-capped" || reason.startsWith("dwell ")) {
4319
+ return { earning: true, why: "" };
4320
+ }
4321
+ const why = reason === "no live session" ? "no active session" : reason === "terminal not focused" ? "terminal not focused" : reason === "no recent prompt (idle)" ? "idle - prompt to resume" : reason === "screen locked" ? "screen locked" : reason === "display asleep" ? "display asleep" : reason === "disabled (kill switch)" ? "turned off" : reason === "daily cap reached" ? "daily max reached" : reason === "no ad cached" ? "loading ad" : "paused";
4322
+ return { earning: false, why };
4323
+ }
4324
+ var EARNING_PULSE_FRAMES = ["\u25CF", "\u25CD", "\u25C9", "\u25CD"];
4325
+ function renderEarningDot(reason, theme = "plain", nowMs) {
4326
+ const t = THEMES[theme];
4327
+ const { earning, why } = classifyEarningState(reason);
4328
+ if (earning) {
4329
+ if (nowMs === void 0)
4330
+ return t.flash("\u25CF earning");
4331
+ const step = Math.floor(nowMs / 1e3);
4332
+ const dot = EARNING_PULSE_FRAMES[step % EARNING_PULSE_FRAMES.length];
4333
+ const bright = dot === "\u25CF" || dot === "\u25C9";
4334
+ const paint = bright ? t.flash : t.money;
4335
+ return paint(`${dot} earning`);
4336
+ }
4337
+ return t.disclosure(`\u25CB paused: ${why}`);
4338
+ }
4264
4339
  var ROTATION_BAR_CELLS = 8;
4265
4340
  function renderRotationBar(nowMs, rotatedAtMs, rotationMs, theme = "plain") {
4266
4341
  if (rotationMs <= 0 || rotatedAtMs <= 0)
@@ -4373,6 +4448,7 @@ function main() {
4373
4448
  const stale = Date.now() - state.updatedAt > STATE_STALE_MS;
4374
4449
  const cfg = ConfigSchema.safeParse(readJson(PATHS.config));
4375
4450
  const showEarnings = cfg.success && cfg.data.showEarnings;
4451
+ const showPayout = cfg.success && cfg.data.showPayout;
4376
4452
  const theme = cfg.success ? cfg.data.theme : "plain";
4377
4453
  const earningsSeg = showEarnings && state.sessionLive && !stale ? renderEarningsSegment(state.balanceCents, state.todayCents, theme) : "";
4378
4454
  const adParsed = AdSchema.safeParse(readJson(PATHS.currentAd));
@@ -4389,13 +4465,24 @@ function main() {
4389
4465
  plainLink: plainClickLink(adParsed.data, serverUrl, clickBase),
4390
4466
  theme
4391
4467
  });
4392
- const seg = state.working ? `${workingGlyph(Date.now(), theme)} ${adSeg}` : adSeg;
4393
- const rotationSeg = renderRotationBar(
4394
- Date.now(),
4395
- state.rotatedAtMs,
4396
- state.rotationMs,
4468
+ const seg = adSeg;
4469
+ const payoutSeg = showPayout ? renderPayoutSegment(
4470
+ adParsed.data.payoutPerImpMillicents,
4471
+ adParsed.data.payoutPerClickMillicents,
4397
4472
  theme
4473
+ ) : "";
4474
+ const now = Date.now();
4475
+ const flashSeg = renderImpactFlash(
4476
+ now,
4477
+ state.lastImpFiredAtMs,
4478
+ state.lastImpEarnedMillicents,
4479
+ theme
4480
+ );
4481
+ const earningDotSeg = flashSeg === "" ? renderEarningDot(state.viewReason, theme, now) : "";
4482
+ const rotationSeg = renderRotationBar(now, state.rotatedAtMs, state.rotationMs, theme);
4483
+ const spinnerSeg = state.working ? workingGlyph(now, theme) : "";
4484
+ process.stdout.write(
4485
+ composeStatusLine(prior, seg, payoutSeg, flashSeg, earningDotSeg, rotationSeg, spinnerSeg, earningsSeg) + "\n"
4398
4486
  );
4399
- process.stdout.write(composeStatusLine(prior, seg, rotationSeg, earningsSeg) + "\n");
4400
4487
  }
4401
4488
  main();