Vercel
No persistent process here — a function caps at 300s. Cron invokes a bounded sweep instead, and the checkpoint carries state across invocations.
{ "crons": [{ "path": "/api/index", "schedule": "*/2 * * * *" }] }import { Index } from "@secondlayer/sdk";
export const maxDuration = 300;
export async function GET(req: Request) {
if (req.headers.get("authorization") !== `Bearer ${process.env.CRON_SECRET}`) {
return new Response("unauthorized", { status: 401 });
}
await new Index().contractCalls.consume({
mode: "bounded", // return on the first empty page instead of holding the tip
signal: AbortSignal.timeout(280_000), // stop before the platform does
fromCursor: await loadCheckpoint(),
onBatch: async (calls, envelope, ctx) => {
await commitRowsAndCheckpoint(calls, ctx.cursor);
return ctx.cursor;
},
});
return Response.json({ ok: true });
}mode: "bounded" is what makes the invocation end. The signal deadline sits inside maxDuration, so the loop exits between batches with the checkpoint committed rather than being killed mid-write.
| Piece | Why |
|---|---|
CRON_SECRET | Vercel sends it as a bearer; without the check the route is public |
mode: "bounded" | Default tail never returns — the function would always time out |
AbortSignal.timeout | Ends cleanly under the cap; the next cron picks up the cursor |
| Idempotent writes | Overlapping invocations are possible — see idempotency |
Don't backfill from genesis here
fromHeight: 0 over millions of blocks in 300-second slices takes days of cron ticks. Run the backfill once as a persistent deploy or locally, then point cron at the resulting checkpoint to tail.
Cron deploys need no bound port — the platform never health-checks them. Read progress from the checkpoints row instead, or log envelope.tip.block_height per run.