Deploy / Vercel

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.

PieceWhy
CRON_SECRETVercel 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.timeoutEnds cleanly under the cap; the next cron picks up the cursor
Idempotent writesOverlapping 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.