Skip to content

Spend & Save ​

Each time the user spends, a share of the spend is saved, either to a Spend & Save balance or into a Target Savings goal. The balance can be withdrawn on one day each month.

Enrol ​

enroll sets how much of each spend is saved.

ts
import { toBaseUnits } from '@loktin/sdk';

await spendSave.enroll({
  skim: { percentBps: 500, roundUpUnit: toBaseUnits('1') },
  monthlyCap: toBaseUnits('100'),
  destination: { kind: 'goal', goalId: goal.result },
  withdrawalDay: 15,
});

The settings are:

  • Percentage: percentBps, 100 to 5,000 basis points of the spend.
  • Round-up: roundUpUnit, which rounds the spend up to a multiple of this unit, in base units. Set this, the percentage, or both.
  • Monthly cap: monthlyCap, the most saved in one UTC month. It is optional. Once it is reached, skims shrink to fit it. The payment is never blocked by the cap.
  • Destination: { kind: 'balance' }, or { kind: 'goal', goalId } for one of the user's open goals. A skim sent to a goal follows that goal's rules.
  • Withdrawal day: withdrawalDay, a day of the month from 1 to 28, in UTC.

A user has one enrolment per integrator, and at most 5 in all.

Pay and save ​

payAndSave pays a G, M or C address and saves the skim in the same transaction.

ts
import { fromBaseUnits } from '@loktin/sdk';

const paid = await spendSave.payAndSave(shopAddress, toBaseUnits('12.50'));
const saved = paid.result.toBalance + paid.result.toGoal;
console.log('Saved', fromBaseUnits(saved), 'USDC');

A skim never exceeds the spend. A skim never blocks a payment: if the wallet cannot cover the payment plus the skim, the skim shrinks to what remains, down to zero. The result's shortfall shows how much the wallet could not cover.

For a spend made by other means, saveForSpend saves the skim on its own. previewSkim shows how a spend would be skimmed.

The withdrawal window ​

The withdrawal window is the whole UTC day of the user's withdrawal day, every month. It can open up to 60 seconds early in real time.

nextWindow gives the current window if one is open, otherwise the next one, in UTC. Its end is the first second after the window. windowInTimeZone formats it in the device's time zone, or one you name.

ts
import { windowInTimeZone } from '@loktin/sdk';

const next = windowInTimeZone(await spendSave.nextWindow());
console.log(`You can withdraw from ${next.start} until ${next.end}.`);

Change settings ​

updateSettings replaces the skim settings, cap and destination at once. They may change at any time. Fee rates never change.

ts
await spendSave.updateSettings({
  skim: { percentBps: 300 },
  monthlyCap: toBaseUnits('50'),
  destination: { kind: 'balance' },
  withdrawalDay: 20,
});

A new withdrawal day takes effect from next month. Until then the current month keeps the old day. enrollment shows the day that applies now as effectiveWithdrawalDay. That value is for display only: at the edge of a month the chain decides.

Withdraw ​

withdraw withdraws an amount from the balance. Within the window, the user may withdraw any amount from 1 base unit up to the balance. The chain refuses it outside the window. previewWithdraw shows how a withdrawal would split, and does not check the window.

ts
const split = await spendSave.previewWithdraw(toBaseUnits('25'));
console.log('The user receives', fromBaseUnits(split.toUser), 'USDC');

const withdrawal = await spendSave.withdraw(toBaseUnits('25'));
console.log('Paid', fromBaseUnits(withdrawal.result.toUser), 'USDC');

The user receives toUser. The integrator fee accrues to the integrator, and the Loktin fee to Loktin's fee recipient. Spend & Save has no penalty.

close closes the enrolment once its balance is zero.

Built on Stellar