Honey Money

By: dreev and bsoule
Spec level:
Gissue: #4463
Changelog
2023-10-20: Reorg, change some cat names, fixes
2023-10-18: Improve IOU categories
2023-10-18: Switch to yootles style: honeyBal/Tee just for caching
2023-10-12: Notes on sting immediacy and deadbeating
2023-10-10: Flesh out the category field for IOUs
2023-10-09: Reorganizing, putting more in the sidebar
2023-10-09: Dispatch some To-Dos, add IPSO_FACTO_SUCCESS state
2023-10-02: More open questions and other tweaks
2023-10-02: Notes about `tee` and `lag` in "crazy usury" parenthetical
2023-09-08: Appendix for shirk-n-turking mana <-> honey
2023-09-05: Nix magic string stuff in favor of explicit charge status
2023-09-03: Onomastic fussery (fro/yon)
2023-09-01: Lots of careful rewriting and expounding
2023-08-31: Business logic becoming coherent
2023-08-30: Bee and dreev keep braindumping ideas into this
2020-07-31: Predecessor of this appeneded to the credit spec
2020-03-25: Premium credit launched

Honeypot overflowing with money, via DALL-E 3

Background and Motivation

(See doc.bmndr.co/honeygrams.)

Alright, so how are we implementing this?

Nom Nom: Database Fields and Constants

It’s crucial to always do double-entry accounting (see sidebar) and that means Beeminder itself has a honey money balance. But nothing special is needed for that. We just use the meta user.

User Table

Every user has fields for their honey balance:

But! Those are only for caching purposes. If you leave them both zero, everything works fine. A user’s balance is computed in real time by starting at time honeyTee and walking forward to each new IOU since then, recomputing the balance.

The credit spec defines the core function that does that, turning an existing honeyBal / honeyTee into an updated honeyBal / honeyTee in light of a new IOU. See the iou function there. And if you want a user’s balance as of Right Now, you first make sure you’ve computed it as of the most recent IOU and then call an NPV() function — also defined in the credit spec — to compute the interest as of any past, present, or future time. We’ll talk about IOUs momentarily.

First, one more user field:

We’re ignoring for now the generalization to multiple credit cards. There’s only honey plus at most one physical payment method. The generalization to a list of payment methods will still work later (cf. black-box generalized splitch). In the meantime, being a honeyholder means honey is used last for derailments. (For premium, honey is always used first — there’s no choice about that.) If the user opts to not be a honeyholder then honey is used first for everything.

IOU Table

This is the core thing for honey money. It’s a separate table with the following fields:

Some of those will make more sense with the examples in the next section but we’ll run through them all briefly now.

The first five are the standard ledger entry fields, starting with the date/time and amount of the IOU. For our purposes we expect either the fro (aka from) or the yon (aka to) to always be Beeminder — the house account — but since that’s just another user, nothing prevents honey transfers between arbitrary users whenever we may want to support that. (Manifold calls this feature “managrams”; ours is “honeygrams”.) And of course the why (aka memo or comment) field is a human-readable explanation.

This brings us to xid which connects an IOU to a corresponding external object. Mostly that means a Stripe charge or PaymentIntent, as Stripe calls it. More rarely but conceptually the same: If someone does a transfer from honey to mana or vice versa, there’ll be a Managram on Manifold to move the mana (see the appendix for details of that) and xid stores the Managram ID for that.

(In the future we may have Derail objects and want the IOU to point to the corresponding derailment. Probably we’d add a new did field if so, if we don’t want to overload xid.)

Focusing on Stripe, it’s a key invariant that any charge we make to a user’s payment method needs a corresponding IOU of the same amount, with the why field referring specifically to the charge. See again the sidebar on double-entry accounting and equal-and-opposite IOUs.

We also track the status of the PaymentIntent with the st8 field and use that field more generally to track the status on the Beeminder side. For example, the charge for an IOU may start in the “SCHEDULED” state, when we’re in the 24-hour window between the derailment happening and the user’s credit card being charged. In the next section we’ll see how all that plays out, like when we charge a user for less than the derailment amount because some of the amount was covered by the user’s honey balance.

The pre and nxt fields allow IOUs to point to each other. If we have a $5 IOU — call it D — for a derailment and then a $5 IOU called P for the actual charge, then D’s nxt field is the ID of P and P’s pre field is the ID of D.

Finally, the cat field is just for our own analytics, to keep track of the reasons for payments. Here are the possible categories:

The underail category is related to refunds but, as we’ll see in the business logic section, there’s usually no IOU for the refund itself. We just reverse the derail IOU and if the IOU for the actual charge hasn’t happened yet, we delete it. If it did already happen, it’s effectively/implicitly reclassified as a purchase of additional honey.

An IOU with category topup should have a pre pointing to the IOU that precipitated it. A topup IOU corresponds to the actual charge to pay what you owe for a derailment or for premium or whatever. It might be for less than the IOU that precipitated it if the user had a positive honey balance. If the user’s honey balance was greater than or equal to the amount they owed then there’d be no topup IOU at all.

The topup and honeybuy categories are special in that they need to have (or be scheduled to have — see the business logic section) an xid from somewhere. And some honeygram type IOUs will also have an xid pointing to their corresponding Managram ID. [TODOBEE: is the previous sentence correct? Maybe a honey/mana transfer is just a purchase of or sale of honey?]

A couple conventions: The value of the st8 field is in all caps to indicate it’s one of our own charge states, not one we’re mirroring from Stripe. And the “SCHEDULED” state means that it’s awaiting a charge sweeper which periodically queries for such IOUs that also have a timestamp tee up to and including now. The sweeper charges the charge on Stripe and updates the xid and st8 to what Stripe tells us.

Comparison with the old Charge table

The current Charge table has the following fields:

If the paytype is nil at collection time we fetch and use the default_paytype from the user.

When the charge is created it sends an email alert to the user letting them know they’re about to be charged. By default the charge_at is initialized to now + 1 day. The charge starts with a status of “pending”. Once we’ve tried it, it moves to either “succeeded” or “failed”. At any point before it hits “succeeded” it can be canceled by an admin. Once it reaches “succeeded” the only action allowed is to refund it.

The Business Logic

Let’s start with the simplest thing that can happen: A user directly purchases honey money from us. This means an IOU corresponding to a specific charge on Stripe and the only reason for it is for the user to make their honey balance go up. Call it $X of honey purchased at time T by user U. Or “you” which is how we’ll refer to the user from now on. So we credit your honey and debit Beeminder’s with the following IOU:

tee: T
amt: X
fro: bmndr
yon: U
why: "bought ${X} of honey money"
xid: (whatever Stripe gives us)
st8: (ditto)
pre: nil
nxt: nil
cat: "honeybuy"

What if you derail, or any event happens for which you owe us money — call it $X at time T for reason R, and you’re user U. First we debit your honey and credit Beeminder’s:

tee: T
amt: X
fro: U
yon: bmndr
why: R (typically something like "derailed usr/gol at $X")
xid: nil
st8: "IPSO_FACTO_SUCCESS"
pre: nil
nxt: (ID of other IOU created, if created per next step)
cat: "derail"

Great, you’ve officially paid your debt on the ledger (equivalently: your debt is logged on the ledger). The “IPSO_FACTO_SUCCESS” state is because your debt is paid by virtue of this IOU existing. We proceed to immediately compute the amount to physically charge your credit card, which we’ll call $Y. By “immediately” we mean that the derailment officially happened at time T and any delay in processing doesn’t matter. It’s computed as of time T and we know honeyTee will equal T because we just logged the IOU for the derailment and that includes updating honeyBal and honeyTee. (Again, see the credit spec for details on updating honeyBal and honeyTee. TODO: rewrite this in light of the fact that honeyBal/Tee are just for caching now, not that it changes anything conceptually.)

Case 1: You’re a honeyholder

In this case we don’t care what your honey balance is — we’re just charging your credit card the full $X, fully reversing the IOU from the derailment so your honey balance won’t change:

Y = X

Case 2: -1 < honeyBal < 0

In this case you have a tiny negative balance below the threshold for credit cards (that “$1” might want to be a constant somewhere?) and so we just charge the $1:

Y = 1

(Spoiler: Y can be anything and it doesn’t actually matter. Whatever we charge your credit card we give back as honey so everything will be fine and fair no matter what.)

Case 3: honeyBal < 0

In this case you also have negative balance but, since we’re not in case 2, it has big enough absolute value that you can pay exactly what you owe:

Y = -honeyBal

For example, consider the common case that you had a zero honey balance when you derailed at $5. That derailment will have put you at -$5 and so the amount, Y, that we’ll be charging your credit card is (drum roll) $5. Phew.

Case 4: You’re not a honeyholder and honeyBal >= 0

Y = 0

No charge happens at all in this case. You had enough honey to pay what you owed!

 
 

Now that we know how much we’re charging you, we make a postdated IOU:

tee: T2
amt: Y
fro: bmndr
yon: U
why: R + "(charging ${Y} to payment method {PMID} and deducting ${Y-X} 
          from your honey money balance)"
xid: nil
st8: "SCHEDULED"
pre: (precipitating IOU ID)
nxt: nil
cat: "topup"

Again, this is a topup IOU because you spent some honey by derailing and now we’re replenishing all or some of that honey. The time T2 is T plus whatever delay we use between derailing and getting charged — typically 24 hours.

(What about our crazy usury? Won’t that negative balance have accumulated interest between the derailment and the charge? Answer: Yes, but for typical amounts and typical charge delays, that will round to zero cents. In any case, we’re just biting the bullet on this. Technically when you derail at $X you pay $X plus interest. It’s fine. But if we really wanted to spit this bullet back out, it’s not hard. We’d store both T and T2 with the IOU for the charge, using T as the IOU’s official timestamp but waiting until T2 to effect the charge. Basically a nominal time and an actual time. Since obviously we’re hard-committed on these 3-letter fields, we’d have tee and lag so tee is the nominal time and tee+lag is how long we wait to effect the charge in the real world. Only tee matters for the math and lag is just for scheduling.)

Recall that R was the reason for the first IOU at time T, like derailing a goal. So we’re appending a parenthetical to the original reason explaining how much we’re taking from your honey balance vs charging to your payment method. We identify that payment method with PMID, e.g., the last 4 digits.

Special case: if Y-X is exactly 0 then omit the “and deducting…” part of why string.

As mentioned in the previous section, the charge sweeper queries for IOUs with times ≤ now and with st8 set to “SCHEDULED”. For each one it finds, it does the actual charge, gets a charge ID from Stripe, and updates xid and st8 with whatever Stripe tells us.

Stripe’s PaymentIntents

Stripe has a number of states that a PaymentIntent passes through, from creation to fully-executed-and-funds-in-our-account. We will give Stripe a webhook to listen for PaymentIntent or Charge events. When we get an event like “charge.succeeded” we can look up the corresponding IOU on our side using metadata we added to the Stripe object and update the status accordingly.

[TODOBEE: nail down the details of that metadata]

Stripe has the full list of PaymentIntent states in their documentation. Not all of the possible states are relevant for us, as we currently only charge credit cards. In the future we may support other payment types which might require us to listen for other events.

Here are ones that are relevant for us:

  1. requires_payment_method — this is the initial state when the PaymentIntent is created, and it returns to this state if the card is declined
  2. processing — in practice we don’t see this because credit cards process instantly and spend no time in this state
  3. succeeded — yay!
  4. canceled — boo.

Unfortunately “refunded” is not noted in the status of the PaymentIntent itself. Stripe creates a refund as a separate object in their API, and they don’t change the status of the associated Charge or PaymentIntent. To create a refund you call Stripe::Refund.create() and pass either the charge_id, or the payment_intent_id of the associated payment you wish to refund. Subsequently the Charge and the PaymentIntent status will still be succeeded, but the refunded attribute on the Charge will be populated. So to check refund status of a charge, or a PaymentIntent, you query charge.refunded, or payment_intent.latest_charge.refunded. The refund information is not reflected directly in the PaymentIntent itself.

For fastidiousness and debuggability we transition the IOU state like so:

  1. Postdated IOU created in state “SCHEDULED”
  2. Charge sweeper picks it up and changes to “ABOUT_TO_SEND_TO_STRIPE”
  3. Hit the Stripe API to charge the charge and change to “SUBMITTED_TO_STRIPE”
  4. Get the response from Stripe and change to whatever Stripe tells us

Again, if you have enough honey then we just debit it by logging the IOU and that’s a successful “charge”. The status of that IOU will only ever by “IPSO_FACTO_SUCCESS”: successful by virtue of the IOU existing.

[TODOBEE: If we’re relying on webhooks from Stripe then maybe we want to keep track of whether we actually submitted the charge and are waiting for Stripe or what.]

What if the user calls non-legit?

If it’s before the charge goes through at time T2 or so:

  1. Delete the postdated, scheduled IOU
  2. Undo the one at time T with a reversal IOU:
tee: T
amt: Y
fro: U
yon: bmndr
why: "non-legit: {R}"
xid: nil
st8: nil
pre: the ID of the original derail IOU
nxt: nil
cat: "underail"

So we’re prefixing the original reason for the IOU (like “derailed usr/gol at $X”) with “non-legit”. Also update the original derail IOU so its nxt field points to this one.

[TODOBEE: say something about how to find the original IOU?]

If the charge already went through it’s just as easy:

  1. We don’t delete the postdated one — that was just a successful purchase of more honey
  2. We do undo the one at time T, same as above.

That’s it, you’ve been refunded in honey!

Maybe you’re a whiny-whinypants and need your actual credit card refunded? That’s just doing the two steps above plus a purchase of negative honey, which is not really even a special case. We first refund $Y via Stripe (probably the same Y as above but it doesn’t matter — do a partial refund, whatever). And then it’s exactly like the case at the top of this section — purchasing honey money — but you’re buying a negative amount of it:

tee: T
amt: Y
fro: U
yon: bmndr
why: "non-legit derail"
xid: TODOBEE
st8: TODOBEE
pre: [TODOBEE: does this point to precipitating derail or underail?]
nxt: nil
cat: "honeysell"

That IOU is in one sense cashing out $Y of honey, which is emphatically not allowed. But if the refund is only happening because of something that was Beeminder’s fault and not at the user’s discretion then it’s ok. We can refund the purchase of honey money like any other purchase if there’s a specific reason the purchase was in error or shouldn’t have happened.

Bonus: Charitable Donations

Tracking charitable donations is much nicer in this new world order. It involves creating a dummy Beeminder account for each possible charity. If the user has an active Beemium plan at the time they pay us (on the ledger) for a derailment and has opted for a charity then we log a chain of IOUs:

  1. The IOU from the user to Beeminder for the full cost of the derailment.
  2. An IOU from Beeminder to the user’s chosen charity account for half the cost of the derailment.
  3. The IOU from Beeminder back to the user for topping up their honey balance / paying for the derailment.
  4. An IOU from the charity account back to Beeminder when Beeminder makes the donation to the charity in the real world.

Using the pre and nxt fields, we record that 1 precipitates 2 which precipitates… [TODOBEE: decide what’s most useful for those pointers, compare to status quo]

In conclusion, it’s all much cleaner and nicer having honey accounts for the charities!

Charge Delays [WIP]

As currently spec’d, anyone opting to hold on to their honey is still going to have their honey debited when they derail, just that we’re then going to fully replenish it 24 hours later. Will that seem buggy?

(This is making me think even more that honeyholding should be buried in advanced settings, so to speak. We could also frame it as “auto-replenish any honey I lose from derailments”. I kind of like that. At the extreme we could say there’s no such thing as honeyholding but as a workaround if you want sting immediacy, give away your honey or loan it out or store it as mana.)

If we want honeyholding but don’t like the thing where your honey balance is negative for 24 hours, we could do away with the 24-hour delay. As in, intentionally decide to trade-off 24-hour processing delay in exchange for honeyholding.

Currently when we charge your credit card, we insert a 24-hour delay between when you actually derail and when we debit the money from your card. We get a few things from this delay:

If we do refunds only to honey (except when Extenuating Circumstances) then it’s actually kind of in our financial interest to do the charge right away… Which is a strong argument for not doing away with the delay, since we are always trying to be extra assiduous about financial stuff because of the “wait! you make money when your users fail?” perception.

Arguments for doing away with the 24-hour delay include:

Things we could do (TODOBEE: brainstorm a list, then pick one and try running with it)

  1. Do away with charge delay. Auto-topup payments just happen immediately.
  2. Use the timestamp of the future charge when displaying honey.Bal to the user, ie, whatever the IOU with the largest timestamp is.
  3. Post-date both IOUs (i.e. the “you derailed and thus owe us money” IOU is also delayed by 24 hours).
  4. Handle negative honey balances differently (e.g. do what we’d do if we were invoicing (whatever that is?); don’t set the deadbeat flag until an attempt to charge your card fails? (would require different behavior for card+honey users vs honey-only users probably?)

User Model [WIP]

More technical details about the implementation of IOUs. Each user has a honeypot with the following fields:

Honeypot

The honeypot is a model that is embedded in the User, and it serves as a way to collect the logic and data associated with the user’s honey. It has the following fields:

And the following methods:

Mapping existing HoneyMoney functionality into this implementation:

yoo.honeyX => yoo.honeypot.bal yoo.honeyT => yoo.honeypot.tee yoo.honeyLog => yoo.honeypot.to_a yoo.iou(args..) => you.honeypot.iou(args..)

Open Questions / Dumping Ground

  1. Write transformer that turns something like “honey money purchased: topping up balance for $5 derail on alice/foo” into the 22-character chargeblurb
  2. Do we track derailments separately from IOUs? Does a goal have a list of derail events? Or can we query these from the IOU table? If so, do we need fields for the user and goal? What all should we ideally track about derailments? (tderail, wasLegit, tcharge, amt, …)
  3. Document the special signup link that gives you free honey, bypassing need for payment method.
  4. A way to preserve/archive PayPal payments history for users before ripping all that code out of Beebody.
  5. As a sanity check, does everything in the mancharge spec still make sense in the honey money new world order? [Quite possibly not! We had maybe better put a pause on the mancharge thing and make sure we’ve spelled out how admins will pause or delay charges in IOU-world, and then decide if it is still worth going forward with the mancharge changes before we’ve completed IOU changes, based on what we decide about IOU-world delays.]
  6. For derailments do we still do the thing where we charge at least $1 to a real payment method before dipping into the honey? It’s nice to keep making sure the payment method works. That would also give us a smidge of sting-immediacy. You may have $1000 of honey but when you derail at $10 you still buy $1 more honey first. Then we deduct the $10, leaving you with $991. Bee says probably no to this.
  7. Do we keep the $1 minimum charge for premium payments? Bee says probably no to this as well. In which case we need to update help docs and think through what else this implies.
  8. Commitwall and Deadbeating: You need honey or a payment method to sign up. Furthermore we don’t let you create a goal unless you can pay for the derailment: payment method on file and/or enough honey.
  9. Decide about the “crazy usury” parenthetical and whether we want to bite the bullet on charging negligible amounts of interest when delaying charges or if we want to store the amount of lag explicitly.
  10. Finish the section on charge delays!
  11. In the old world order we have a failsafe where a derailment, when there’s already a pending charge, suppresses the new charge. Take another pass through this spec and add that failsafe back if needed, or explain why it’s not needed.
  12. Are the pre and nxt fields on the IOU overkill? Does one direction suffice, like where an IOU points to a spawned IOU but not vice versa? Or is it underkill and an IOU might spawn or be spawned by multiple IOUs?
  13. If we need to edit some historical IOU, how messy is that? Do we need to be able to to replay all IOUs from the dawn of time to recalculate balances?
  14. What do we need in terms of admin UI for workerbees to see the accounting they need to see? Like they have to be able to know if a derailment was refunded as honey, etc, and have one-click or few-click ways to do common things.
  15. Should refunds actually precipitate a pair of IOUs? One to indicate that we owe the money back to the user (typically because the derailment wasn’t legit) and the negative of that when we actually give the money back.
  16. Desideratum: Translog everything.
  17. The feature of premium credit where we’d always hit your credit card for at least $1 turned out to be kinda nice. Can we Pareto dominate that? Some other excuse to keep hitting your payment method periodically to make sure it still works?
  18. Revamp this in light of the conclusion out loud and in bee/dreev DMs that honeyTee and honeyBal should be totally dynamic: whenever you want a user’s balance you step through all their IOUs since the dawn of time. That will be computationally fine for now and when it’s eventually not, it’s easy to add a backstop. Basically cache it up to some particular point in the past and walk forward from there.
  19. Conclusion from out loud: We need to pick between (a) an IOU table that includes pending charges and tracks which ones were successful via the st8 field, or (b) an additional table recording pending charges where we move it to the IOU table only once it succeeds.

Cognata AKA Related Links

Appendix: Transferring Honey to Mana and Vice Versa

[TODO: update this with what we ended up doing]

Here’s how, as a trial, we can easily and shirk-n-turk-ily implement the ability to cash out one’s mana (Manifold’s play currency) as honey money and vice versa.

This assumes we implement a honeygram feature to let users send honey money to each other, which should be easy given how we’re implementing this.

Then we need to two dummy/house accounts, which we can think of as a “mana supply” account on Manifold and a “honey supply” account on Beeminder:

  1. Create a user on Manifold called “Beeminder” AKA bee@manifold
  2. Create a user on Beeminder called “manifold” AKA man@beeminder

Case 1: Converting mana to honey

Say that Manifold user Marvin decides to transfer/convert 100 mana to $1 of honey money. (That’s the exchange rate on Manifold: you can buy 100 mana for $1.) Marvin makes a managram, as they call it, to send 100 mana to the bee@manifold account, specifying his Beeminder username (say it’s “marvinminder”) in the comment.

We at Beeminder get the notification that we received mana.

We transfer $1 of honey from the man@beeminder account to user marvinminder. Aaaand, that’s it. Marvin has turned his 100 mana into $1 of honey! Beeminder has that mana in its bee@manifold account.

Case 2: Converting honey to mana

It’s all very similar in the other direction. User bonnie on Beeminder wants to cash out her honey as mana. She sends a honeygram to the man@beeminder account, specifying her Manifold username, say “bonniefold”. We (at Beeminder) notify ourselves and go send the managram from bee@manifold to bonniefold on Manifold.

Are we sure we can afford to do this?

A potential concern is that this could be expensive for Beeminder if a lot of people cash out their mana as honey. Beeminder gets a lot of mana, which can’t be turned into real money, but forgoes a lot of real revenue.

It’s a fair concern and we’d reserve the right to pull the plug on it, but we predict that this is money-making from Beeminder’s perspective. The biggest effect we expect is Manifold users creating Beeminder accounts in order to take advantage of it. Or just out of curiosity or just because it’s how they learn Beeminder exists in the first place.

Hopefully it’s similarly beneficial for Manifold! It’s another bullet item in the growing list of reasons to have value for mana, along with bounties and market subsidies and cashing out to charity.