Skip to content
Thyme Docs

Balance Top-up

Keeps a wallet funded. On each run the task reads the recipient's balance of a token. If it is below minBalance, the task returns one transfer of amount from the Safe. Otherwise it skips.

Typical uses are a bot or gas wallet that needs a floor of a stablecoin, or a team wallet that spends steadily.

Task

functions/top-up/index.ts
import { defineTask, z } from '@thyme-labs/sdk'
import { encodeFunctionData, erc20Abi, formatUnits, parseUnits } from 'viem'
 
const decimalString = z.string().regex(/^\d+(\.\d+)?$/)
 
export default defineTask({
  schema: z.object({
    token: z.address(),
    recipient: z.address(),
    amount: decimalString,
    minBalance: decimalString,
  }),
  async run(ctx) {
    const { token, recipient } = ctx.args
 
    const decimals = await ctx.client.readContract({
      address: token,
      abi: erc20Abi,
      functionName: 'decimals',
    })
    const balance = await ctx.client.readContract({
      address: token,
      abi: erc20Abi,
      functionName: 'balanceOf',
      args: [recipient],
    })
    ctx.logger.info(`Recipient balance: ${formatUnits(balance, decimals)}`)
 
    if (balance >= parseUnits(ctx.args.minBalance, decimals)) {
      return { canExec: false, message: 'Balance is above the minimum' }
    }
 
    const amount = parseUnits(ctx.args.amount, decimals)
    const available = await ctx.client.readContract({
      address: token,
      abi: erc20Abi,
      functionName: 'balanceOf',
      args: [ctx.account],
    })
    if (available < amount) {
      return { canExec: false, message: 'The Safe does not hold enough to top up' }
    }
 
    return {
      canExec: true,
      calls: [
        {
          to: token,
          data: encodeFunctionData({
            abi: erc20Abi,
            functionName: 'transfer',
            args: [recipient, amount],
          }),
        },
      ],
    }
  },
})

amount and minBalance are decimal strings in token units, such as "100" or "12.5". The task converts them with the token's own decimals. A threshold in token units is the same as a USD threshold for a stablecoin. Flow has no built-in price source, so a volatile token needs a price feed read added to the task.

Arguments

functions/top-up/args.json
{
  "token": "0x...",
  "recipient": "0x...",
  "amount": "100",
  "minBalance": "25"
}

Replace the addresses with the token and recipient on your chain.

Permissions

functions/top-up/permissions.json
{
  "calls": [
    { "target": { "arg": "token" }, "function": "transfer(address,uint256)" }
  ]
}

Your Safe profile must allow the same token and transfer(address,uint256). A role allowed to call transfer can send any amount to any address, so keep only what the automation needs in the Safe. See what the role can and cannot limit.

Schedule

An interval of five minutes:

{ "type": "interval", "intervalMs": 300000 }

Pick an interval that leaves room for confirmation. If the recipient spends the top-up before the next run, the task tops it up again.