Class: Ask::Tokens::Wallet

Inherits:
Object
  • Object
show all
Defined in:
lib/ask/tokens/wallet.rb

Overview

The wallet engine: all the business logic of token accounting, written once against the Store port. No database knowledge lives here.

wallet = Ask::Tokens.wallet_for("user:42")
wallet.grant!(1000, reason: :signup, expires_at: 7.days.from_now)
wallet.spend!(:chat_message, input: "hi", output: "yo") { LLM.chat(...) }
wallet.balance   # => 998

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(owner:, store:, time: -> { Time.now }) ⇒ Wallet

Returns a new instance of Wallet.



16
17
18
19
20
# File 'lib/ask/tokens/wallet.rb', line 16

def initialize(owner:, store:, time: -> { Time.now })
  @owner = owner
  @store = store
  @time = time
end

Instance Attribute Details

#ownerObject (readonly)

Returns the value of attribute owner.



14
15
16
# File 'lib/ask/tokens/wallet.rb', line 14

def owner
  @owner
end

Instance Method Details

#adjust_balance_to!(amount, reason:, metadata: {}) ⇒ Object

Set the balance to an exact amount, recording the delta as an adjustment entry. Used by billing systems to reset monthly allowances (replace remaining balance) or fix incorrect totals.

wallet.adjust_balance_to!(10_000, reason: :monthly_reset)

Raises:

  • (ArgumentError)


104
105
106
107
108
109
110
111
112
113
114
115
116
117
# File 'lib/ask/tokens/wallet.rb', line 104

def adjust_balance_to!(amount, reason:, metadata: {})
  amount = amount.to_i
  raise ArgumentError, "balance cannot be negative (got #{amount})" if amount.negative? && !Ask::Tokens.config.negatives

  @store.with_lock(owner) do
    previous = @store.balance(owner).to_i
    delta = amount - previous
    return nil if delta.zero?

    entry = record(:adjustment, delta, reason.to_s, amount, expires_at: nil, metadata: )
    dispatch(:on_adjust, :adjustment, delta, reason, , entry, previous, amount)
    entry
  end
end

#balanceObject

Current token balance.



25
26
27
# File 'lib/ask/tokens/wallet.rb', line 25

def balance
  @store.balance(owner).to_i
end

#deduct!(amount, reason:, metadata: {}, &block) ⇒ Object

Deduct amount tokens, raising InsufficientTokens when the balance is too low (unless negatives are allowed). With a block, the block runs first and tokens are charged only when it succeeds.

Raises:

  • (ArgumentError)


73
74
75
76
77
78
79
80
81
82
# File 'lib/ask/tokens/wallet.rb', line 73

def deduct!(amount, reason:, metadata: {}, &block)
  amount = amount.to_i
  raise ArgumentError, "deduct amount must be positive (got #{amount})" unless amount.positive?

  if block
    spend_after_success(amount, reason, , &block)
  else
    charge(amount, reason, )
  end
end

#enough_for?(activity_name, params = {}) ⇒ Boolean

True when the balance covers the estimated cost of an activity.

Returns:

  • (Boolean)


46
47
48
# File 'lib/ask/tokens/wallet.rb', line 46

def enough_for?(activity_name, params = {})
  has?(estimate(activity_name, params))
end

#entries(**filters) ⇒ Object

Ledger entries, oldest first. Filter by :kind (grant/debit/...) and :since (Time).



31
32
33
# File 'lib/ask/tokens/wallet.rb', line 31

def entries(**filters)
  @store.entries(owner, **filters)
end

#estimate(activity_name, params = {}) ⇒ Object

Estimated token cost of running activity with params.



41
42
43
# File 'lib/ask/tokens/wallet.rb', line 41

def estimate(activity_name, params = {})
  Ask::Tokens.estimate(activity_name, params)
end

#grant!(amount, reason:, expires_at: nil, metadata: {}) ⇒ Object

Add amount tokens. expires_at marks the grant as expiring. Returns the written LedgerEntry.

Raises:

  • (ArgumentError)


60
61
62
63
64
65
66
67
68
# File 'lib/ask/tokens/wallet.rb', line 60

def grant!(amount, reason:, expires_at: nil, metadata: {})
  amount = amount.to_i
  raise ArgumentError, "grant amount must be positive (got #{amount})" unless amount.positive?

  mutate(:grant, amount, reason, expires_at: expires_at, metadata: ) do |current|
    previous = current
    [previous + amount, previous, amount]
  end
end

#has?(amount) ⇒ Boolean

True when the balance covers amount.

Returns:

  • (Boolean)


36
37
38
# File 'lib/ask/tokens/wallet.rb', line 36

def has?(amount)
  balance >= amount.to_i
end

#spend!(activity_name, params = {}, &block) ⇒ Object

Spend the estimated token cost of activity given params. With a block, charge only if the block succeeds — the standard wrapper for LLM calls and render jobs.



87
88
89
90
# File 'lib/ask/tokens/wallet.rb', line 87

def spend!(activity_name, params = {}, &block)
  amount = estimate(activity_name, params)
  deduct!(amount, reason: activity_name.to_s, metadata: params.dup, &block)
end

#spend_amount!(amount, reason:, metadata: {}, &block) ⇒ Object

Deduct an exact token cost (same as deduct!, but stays at wallet level for callers that prefer namespacing).



94
95
96
# File 'lib/ask/tokens/wallet.rb', line 94

def spend_amount!(amount, reason:, metadata: {}, &block)
  deduct!(amount, reason: reason, metadata: , &block)
end

#used_since(time) ⇒ Object

Total tokens debited since time (absolute value). Useful for monthly-usage reports.



52
53
54
# File 'lib/ask/tokens/wallet.rb', line 52

def used_since(time)
  entries(kind: :debit, since: time).sum(&:amount).abs
end