REST API and CI

A fresh testnet for every CI run: made before the tests, deleted after them.

GitHub Actions

Add an API token from your Account page as a repository secret named FORKSTATE_TOKEN, then:

.github/workflows/test.yml
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: foundry-rs/foundry-toolchain@v1

      - id: fork
        uses: Forkscout/forkstate-action@v1
        with:
          token: ${{ secrets.FORKSTATE_TOKEN }}
          chain: base            # any id from /docs/chains
          # fork-block: 30000000 # pin it, so every run starts from the same state

      - run: forge test --fork-url ${{ steps.fork.outputs.rpc-url }}
  • The testnet is deleted when the job ends, whether the tests passed or not. Set keep: true to leave it for debugging.
  • Outputs: rpc-url, testnet-id, chain-id and fork-block. The RPC URL is also exported as FORKSTATE_RPC_URL and masked in logs.
  • Every testnet the action makes is tagged CI, so they are one click to find and filter out. Add more with tags: audit, nightly.
  • Pin fork-block for reproducible runs. The chain's cache is shared, so a pinned block is warm after the first run and costs almost nothing.

The API

Every request carries Authorization: Bearer <token>. Answers are JSON. The base URL is https://forkstate.forkscout.com/api/v1.

RequestDoes
GET /testnetsEvery testnet you have
POST /testnetsMakes one: { name, chain, project?, forkBlock?, chainId?, followHead?, tags? }
GET /testnets/:idOne testnet
DELETE /testnets/:idDeletes it and everything written to it
POST /testnets/:id/cloneA copy that starts exactly where it is: { name? }
GET /testnets/:id/snapshotsIts saved snapshots, newest first
POST /testnets/:id/snapshotsSaves one: { name }
POST /testnets/:id/snapshots/:sid/restoreGoes back to one
DELETE /testnets/:id/snapshots/:sidDeletes one
terminal
curl -s -X POST https://forkstate.forkscout.com/api/v1/testnets \
  -H "Authorization: Bearer $FORKSTATE_TOKEN" \
  -H "content-type: application/json" \
  -d '{"name":"nightly","chain":"ethereum","forkBlock":21000000}'

# {
#   "id": "5b0e…", "name": "nightly", "chain": "Ethereum",
#   "chainId": 7357001, "forkBlock": "0x1406f40", "followsHead": false,
#   "rpcUrl": "https://forkstate.forkscout.com/api/rpc/…", "createdAt": "…"
# }

Errors

StatusMeans
400The request is malformed: no name, an unknown chain, a body that is not JSON
401No token, or one that has been revoked
402The account is out of credit
404No such testnet or snapshot — or not yours, which looks the same on purpose
409At a limit: testnets per account, or snapshots per testnet
Every error has a body of { "error": "…" } with a sentence meant to be shown as it is.