Reference / Migrating from Chainhook

Migrating from Chainhook

Chainhook ships in two forms: the original self-hosted Chainhook (v1), now archived, and a hosted Chainhook (v2) on the Hiro Platform, which doesn't cover devnet. Secondlayer subscriptions cover both: a webhook on raw chain events — a contract, an event, a function, a SIP trait — with signed, retried delivery. This guide maps your predicates over, whichever one you were on.

Subscriptions run either way, so you can match whichever Chainhook you came from. The API is identical; only where the indexer runs changes.

You were onMove toWhat changes
Hosted Chainhook (v2)Secondlayer hosted — the defaultOne API call, no node, no infra to operate — with devnet support.
Self-hosted Chainhook (v1)Secondlayer self-hostedRun your own node + our indexer — actively maintained.

The rest of this guide uses the hosted path; everything maps the same when self-hosting.

A Chainhook predicate is a file: an if_this scope plus a then_that HTTP target, registered against a node you run. A Secondlayer subscription is one API call: a url plus a triggers array (1–50). On the hosted platform we run the indexer; you receive POSTs.

import { trigger } from "@secondlayer/sdk";

await sl.subscriptions.create({
  name: "amm-swaps",
  url: "https://my-app.com/webhook",
  triggers: [
    trigger.contractCall({ contractId: "SP....amm", functionName: "swap-*" }),
  ],
});

That's the equivalent of a contract_call predicate with an http_post action — minus the predicate file, the node, and the registration step.

Each Chainhook if_this scope maps to a trigger.* factory. Wildcards (*) are allowed on names; trait scopes a trigger to a SIP/trait.

Chainhook if_this.scopeSecondlayer trigger
contract_call (contract_identifier, method)trigger.contractCall({ contractId, functionName, caller, trait })
print_event (contract_identifier, contains / matches_regex)trigger.printEvent({ contractId, topic, trait })
ft_eventmint / transfer / burntrigger.ftMint / trigger.ftTransfer / trigger.ftBurn (assetIdentifier, minAmount, trait)
nft_eventmint / transfer / burntrigger.nftMint / trigger.nftTransfer / trigger.nftBurn (assetIdentifier, trait)
stx_eventmint / transfer / locktrigger.stxMint / trigger.stxTransfer / trigger.stxLock (recipient, sender, minAmount)
contract_deployment (deployer, implement_trait)trigger.contractDeploy({ deployer, contractName }), or scope by trait
txidNo trigger — query Index by transaction instead

The raw object form is equivalent to the factory: trigger.contractCall({ contractId, functionName }) is { type: "contract_call", contractId, functionName }. See the full trigger reference for every field.

Example: a contract_call predicate

Chainhook:

{
  "if_this": {
    "scope": "contract_call",
    "contract_identifier": "SP....amm",
    "method": "swap-x-for-y"
  },
  "then_that": {
    "http_post": { "url": "https://my-app.com/webhook" }
  }
}

Secondlayer:

await sl.subscriptions.create({
  name: "amm-swaps",
  url: "https://my-app.com/webhook",
  triggers: [
    trigger.contractCall({ contractId: "SP....amm", functionName: "swap-x-for-y" }),
  ],
});

Chainhook delivers apply / rollback payloads. Secondlayer delivers a chain.{type}.apply envelope per matched event, and chain.reorg.rollback when a block is orphaned — so you can undo anything you committed off a fork. The full envelopes are in the subscriptions delivery reference.

Verify the signature

Every delivery is HMAC-signed (Standard Webhooks) and carries a universal ed25519 signature. Verify with the SDK's verifyWebhookSignature before trusting the payload — see Verify the signature. Where Chainhook used a bearer token, here the signature is the auth.

  • Forward-looking by default. A chain subscription starts at the chain tip — no historical scan like a predicate's start_block. To deliver history, use replay over an existing subscription (idempotent, capped at 100,000 blocks, never moves the live cursor).
  • You choose where the indexer runs. Self-hosted Chainhook ran on a Stacks node you operated; hosted Chainhook ran it for you. On Secondlayer's hosted platform you run neither — just your receiver. Prefer to own the stack? Self-host the indexer against your own node.

Run the whole loop locally before you point it at mainnet — deploy a subgraph and fire subscriptions against your Clarinet devnet. See Devnet.