> ## Documentation Index
> Fetch the complete documentation index at: https://vowena-dependabot-github-actions-actions-checkout-7.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Keeper Guide

> Run an automated keeper that charges due subscriptions on schedule. How keepers work, the charge loop, and production patterns (serial submissions, sequence numbers, retries).

## What is a keeper?

A keeper is any script that calls `charge()` 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.

<Info>
  You don't have to build this yourself. The [Vowena
  dashboard](/dashboard/keeper-setup) ships a managed keeper that runs as a
  Vercel cron every minute. This page is for teams that want to self-host.
</Info>

***

## How it works

1. Keeper enumerates every subscription that might be due (discovery).
2. For each subscription, it builds a `charge()` tx via `client.buildCharge`.
3. It signs with its own keypair and submits to the network.
4. If the charge is due and the allowance is sufficient, USDC moves subscriber → merchant and `charge_ok` is emitted. If not, the contract rejects harmlessly — no funds move, nothing breaks.

The keeper never holds USDC. It only holds a small XLM balance for tx fees.

***

## A minimal keeper loop

The only two non-obvious bits are (a) how to discover subs, and (b) why submissions must be **serial**.

```typescript theme={null}
import {
  VowenaClient,
  NETWORKS,
  Keypair,
  rpc as SorobanRpc,
  TransactionBuilder,
  Networks,
} from "@vowena/sdk";

const keeper = Keypair.fromSecret(process.env.KEEPER_SECRET!);
const client = new VowenaClient({
  contractId: NETWORKS.testnet.contractId,
  rpcUrl: NETWORKS.testnet.rpcUrl,
  networkPassphrase: NETWORKS.testnet.networkPassphrase,
});
const server = new SorobanRpc.Server(NETWORKS.testnet.rpcUrl);

async function runOnce(merchant: string) {
  // 1. Discovery — parallel, read-only
  const planIds = await client.getMerchantPlans(merchant, keeper.publicKey());
  const subLists = await Promise.all(
    planIds.map((id) =>
      client.getPlanSubscribers(id, keeper.publicKey()).catch(() => []),
    ),
  );
  const subIds = Array.from(new Set(subLists.flat()));

  // 2. Submissions — SERIAL with a fresh account each time
  for (const subId of subIds) {
    await chargeOne(subId);
  }
}

async function chargeOne(subId: number) {
  // Fetch a fresh account to pick up the current sequence number. Skipping
  // this and reusing one account snapshot across a batch causes every tx
  // after the first to be rejected as tx_bad_seq.
  const account = await server.getAccount(keeper.publicKey());
  const xdr = await client.buildCharge(keeper.publicKey(), subId);
  const tx = TransactionBuilder.fromXDR(xdr, Networks.TESTNET);
  tx.sign(keeper);
  await server.sendTransaction(tx);
}
```

<Warning>
  **Don't parallelize submissions from one signer.** Stellar requires sequence
  numbers to be the exact next value per source account. Firing `charge()`
  transactions in parallel with the same keeper means every tx after the first
  lands with `tx_bad_seq`. Serial + fresh `server.getAccount()` per tx is the
  safe pattern.
</Warning>

***

## 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.

```typescript theme={null}
setInterval(() => runOnce("GMERCHANT...ADDR").catch(console.error), 60_000);
```

***

## 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/cron` every 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.

See [Keeper setup](/dashboard/keeper-setup) for step-by-step configuration.

***

## 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              |

All of these are handled automatically by calling `charge()` — the keeper doesn't need to inspect subscription state first.
