What is a keeper?
A keeper is any script that callscharge() on the Vowena contract for subscriptions that are due. charge() is permissionless — the contract validates every precondition on-chain, so the keeper just signs an invoke op and submits it. The keeper pays the tiny transaction fee (~$0.00001 in XLM) while USDC flows directly from subscriber to merchant.
You don’t have to build this yourself. The Vowena
dashboard ships a managed keeper that runs as a
Vercel cron every minute. This page is for teams that want to self-host.
How it works
- Keeper enumerates every subscription that might be due (discovery).
- For each subscription, it builds a
charge()tx viaclient.buildCharge. - It signs with its own keypair and submits to the network.
- If the charge is due and the allowance is sufficient, USDC moves subscriber → merchant and
charge_okis emitted. If not, the contract rejects harmlessly — no funds move, nothing breaks.
A minimal keeper loop
The only two non-obvious bits are (a) how to discover subs, and (b) why submissions must be serial.Scheduling
A cron or setInterval call every 60 seconds is plenty for most plans.charge() is idempotent within a period — calling it before the next period is due simply no-ops, so you can’t over-bill by polling too often.
Using the dashboard’s managed keeper
If you don’t want to operate your own infra, enable the dashboard’s managed keeper instead:- Vercel cron hits
/api/cronevery minute. - The cron signs with
VOWENA_ISSUER_SECRET(set once in your Vercel env). - Submissions are serial with a fresh account fetch per tx — same pattern as above.
Failure modes
| Failure | What the contract does | What the keeper should do |
|---|---|---|
| Subscription not yet due | Returns false, emits nothing | Keep trying next period |
| Insufficient balance | Emits charge_fail, sets failed_at, starts the grace window | Retry until grace expires |
| Grace expired, still failing | Transitions subscription to Paused | Stop trying until subscriber reactivates |
| Max periods reached | Transitions subscription to Expired | Remove from the charge list |
| Subscriber cancelled | No effect (contract already handled it) | Remove from the charge list |
charge() — the keeper doesn’t need to inspect subscription state first.