Docs · hooks, protocol + API v1 · interactive
Conductor docs
Keeper jobs for Uniswap v4 hooks: the pool checker, the event markers and the hook job templates. Then how automations work, the policy engine, the hook fees -> rewards workflow, and every API route: automation, the control room and the data layer underneath. Reads need no key and run right here. All paths are prefixed with /v1.
On this page
Overview
Conductor runs the work your v4 hook can't: keeper jobs for Uniswap v4 hooks on Robinhood Chain (chainId 4663), run by operators inside rules the hook's owner sets on chain, with an open indexer underneath. Hooks and automations: see the next sections. Data layer: register an address, then read holders, balances, transfers and decoded events over HTTP, subscribe to signed webhooks, and export holder snapshots at any indexed block.
Base URL: the API host, or this website, which proxies /v1/* to it. Locally the API listens on http://127.0.0.1:8787. JSON in, JSON out, except the CSV snapshot.
export LATTICE="http://127.0.0.1:8787"
curl -s "$LATTICE/v1/health"Every request on this page comes in three forms (curl, the TypeScript SDK, the lattice CLI) generated from one definition, pointed at this site's origin. Pick a form once and every example follows. Read routes have a Try it panel that sends the real request to this site's /v1 and shows the answer.
Conventions
| Topic | Rule |
|---|---|
| Addresses | Accepted in any case. Always returned lowercase. Invalid => 400 invalid_address. |
| uint256 | balance, amount, tokenId, totalSupply, total are decimal strings in raw units. formatted = raw / 10^decimals, no rounding. |
| Numbers | Block numbers and counts are JSON numbers. |
| Times | ISO-8601 UTC strings. blockTime may be null. |
| confirmed | blockNumber <= safeBlock, where safeBlock = headBlock - 12 by default. |
| Lists | { "data": [...], "next": "<cursor>" | null }. Pass next back as ?cursor= with the same other params. Bad cursor => 400 invalid_cursor. |
| limit | Default 100. Max 1000 for holders, events, transfers, balances; 200 for contracts, webhooks, deliveries. Out of range is clamped. |
| order | desc (default, newest first) or asc, for events and transfers. |
| block | Optional as-of block on holder, balance, owner and snapshot reads. Must satisfy startBlock - 1 <= block <= indexedThrough, else 409 not_indexed. |
| Kinds | erc20, erc721 (holders = token counts), abi (events only; holder endpoints => 409 conflict). |
Errors
{ "error": { "code": "not_found", "message": "contract 0xabc... is not registered" } }| HTTP | code | When |
|---|---|---|
| 400 | bad_request, invalid_address, invalid_cursor | Validation. |
| 401 | unauthorized | Write route, keys configured, x-api-key missing or unknown. |
| 404 | not_found | Unknown contract, webhook or route. |
| 409 | conflict | Endpoint not valid for the contract kind; snapshot larger than the row limit. |
| 409 | not_indexed | block outside the indexed range. Details carry indexedThrough and startBlock. |
| 422 | unsupported | Address has no code; kind not detectable and no ABI; ABI invalid. |
| 422 | limit_reached | Contract or webhook limit reached. |
| 429 | rate_limited | Per-IP limit. x-ratelimit-* and retry-after headers are set. |
| 500 | internal | Bug. |
| 503 | unavailable | Database unreachable. |
Auth and limits
- Reads need no auth.
- Writes (
POST /v1/contractsand every/v1/webhooksroute) takex-api-key: <key>when the operator configures keys. With no keys configured, writes are open. POST /v1/contractsalso works without a key when public registration is on (the default), limited to 5 per minute per IP. The register form on/datauses this.- Webhooks are scoped to the key that created them: list, get, patch and delete only see your own.
- Reads: 300 requests per minute per IP. Requests with a valid key are exempt.
Keeper jobs for v4 hooks
Conductor runs the work your v4 hook can't. A hook only wakes up inside a swap or a liquidity call. It cannot read the clock, rebalance, refresh an oracle, step a dynamic fee, claim its own fees or pay anyone out on a schedule. Conductor's operators do that, inside rules the hook's owner sets on chain, with every run recorded.
A keeper job is an ordinary automation task: a trigger, a list of calls, and the policy that bounds them. What makes it hook-aware is the trigger: it can read the pool's state through the UniswapV4PoolChecker and wake on the pool's events through an event marker.
- The hook's owner (or the project behind it) owns the
Automationcontract and its policy. - Operators watch the pool, simulate the job, and submit
execute; the contract rejects any call the policy does not allow. - Every run is recorded with its trigger, transactions and balance changes, like any other run.
The v4 pool checker
UniswapV4PoolChecker reads getSlot0(poolId) and getLiquidity(poolId) from the v4 StateView. A task sets it as trigger.checker with checkerData = abi.encode(PoolCondition), where PoolCondition is { mode, poolId, tickLower, tickUpper, liquidityThreshold }. When it is ready its payload is abi.encode(tick, sqrtPriceX96, liquidity, lpFee).
| Mode | Ready when |
|---|---|
Any | Always. Carries the poolId so the worker can wake the task on that pool's events. |
PriceOutsideRange | tick < tickLower or tick >= tickUpper. |
PriceInsideRange | tickLower <= tick < tickUpper. |
LiquidityBelow | liquidity < liquidityThreshold. |
LiquidityAbove | liquidity > liquidityThreshold. |
- A pool that was never initialised (
sqrtPriceX96 == 0) is not ready in any mode exceptAny. - The range modes revert
InvalidRange()whentickLower >= tickUpper. - Task eligibility for these tasks carries
{ tick, sqrtPriceX96, liquidity, lpFee, poolId, mode, inRange }.
Event markers
Markers are off-chain wake-ups: trigger.eventMarker is the keccak256 of the marker string, and the worker re-checks the task as soon as a matching PoolManager event is indexed instead of waiting for the next interval. The task's pool is the poolId in its PoolCondition; a v4 marker on a task with another checker is ignored.
| Marker | Wakes on |
|---|---|
v4.pool.swap | Swap on the PoolManager for the task's pool. |
v4.pool.liquidity | ModifyLiquidity for the task's pool. |
v4.pool.initialize | Initialize whose hooks is the task's hook. The pool is not known in advance: the condition's poolId is zero and the hook address is read from balanceHolder. |
v4.pool.any | Any of the above for the task's pool. |
Hook job templates
The build console at /app/new and GET /v1/workflows list these. Planned templates describe the shape they will ship with; until then, start from a blank automation and add the task by hand.
| Id | Name | Status | What it does | Trigger |
|---|---|---|---|---|
pons-rewards/v1 | Claim hook fees -> fund rewards | live | Sweep the curve, sweep the hook's pool fees, claim the escrow, swap on v4, send to rewards. | PonsFeeChecker, pons.escrow.credited |
v4-rebalance/v1 | Rebalance when price leaves the range | planned | Collect, burn the position, mint around the current tick. | PriceOutsideRange, v4.pool.swap |
v4-oracle-refresh/v1 | Refresh a hook's oracle | planned | Call hook.update() (owner-chosen selector). | interval + v4.pool.swap |
v4-dynamic-fee/v1 | Step a dynamic fee with volume | planned | hook.setFee(poolId, fee) from a volume band. | interval, v4.pool.swap |
v4-twamm/v1 | Run a large order in slices | planned | Universal Router swap of a fixed slice. | interval + PriceInsideRange |
v4-liquidity-floor/v1 | Top up a pool below a liquidity floor | planned | Mint liquidity from the automation's balance. | LiquidityBelow, v4.pool.liquidity |
blank | Blank automation | live | Create with a deposit. | none |
Hooks and pools API
Read-only, no key. The indexer mirrors the PoolManager's Initialize, Swap and ModifyLiquidity events into hooks and pools. A hook's flags are the low 14 bits of its address; permissions spells them out (beforeInitialize is bit 13, afterRemoveLiquidityReturnDelta bit 0).
| Route | Returns |
|---|---|
GET /v1/hooks?limit&cursor | Hooks, most pools first: address, flags, permissions, pools, swaps in 24 h, first seen, known name. |
GET /v1/hooks/:address | One hook and its pools, newest first. |
GET /v1/pools?hook=&limit&cursor | Pools: currencies, fee (dynamic or fixed), tick spacing, hook, tick, liquidity, swap count. |
GET /v1/pools/:poolId | One pool with its last 20 swaps and live tick, sqrtPriceX96, liquidity, lpFee from StateView (null when the RPC fails). |
How automation works
Tell your contract what to do, and when. Each project owns an Automation contract: the policy (which calls it may make, which assets may move, to whom, how much per epoch) and its tasks. Conductor's operators watch the triggers, simulate, and submit execute; the contract rejects anything outside the rules. Every run is recorded.
| Role | Who | Can |
|---|---|---|
| Owner | your wallet (an EOA or a Safe) | everything on its automation: tasks, policy, pause, revoke operators, withdraw any asset, ownerCall |
| Operator | a keeper key in the OperatorRegistry (v0: Conductor's worker) | execute tasks within the rules, abortRun after a timeout, get paid a capped fee |
| Protocol owner | deployer key, then a multisig | add operators and set the implementation for NEW automations; never moves your funds |
| Anyone | simulate, every view, deposit ETH |
- Writes never come from Conductor: the API returns transactions (
TxRequest) and your wallet signs them. - Pause, revoking an operator, deactivating a task, withdrawing and tightening the policy are always immediate.
- The owner can always exit:
withdrawandownerCallignore pause, policy and timelock.
Policy and limits
A call made by execute is allowed only if its (target, selector) is permitted, it sends ETH only where allowValue is set, its validator (if any) accepts it, and any token transfer/approve goes to an allowed recipient. The Universal Router is only reachable through the UniversalRouterV4Validator, which accepts one exact-in v4 swap whose output returns to the automation.
Every tracked asset (up to 16, ETH first) is measured before and after each call. Gross outflow is charged against a per-asset cap per epoch. Epochs are fixed windows aligned to unix time: a 7-day epoch resets every Thursday 00:00 UTC. Around a boundary up to 2x the cap can move in a short time (end of one epoch, start of the next). Cap 0 = inflow only; uint256 max = uncapped but tracked.
| Change | Applies |
|---|---|
| Lower a cap, remove a target or recipient, restrict operators, raise the timelock | immediately |
| New target, higher cap, new recipient, looser fee policy, shorter timelock | immediately when policyDelay is 0; otherwise queued, executable after the delay |
The timelock is optional and defaults to 0. It cannot protect the owner from themselves; it is a public promise to holders that the rules cannot be loosened without notice (max 30 days).
Operator fees
Paid in ETH from the automation to the operator at the end of an execute in which at least one step succeeded: gasUsed x min(gasprice, maxGasPrice) x (1 + premium) + L1 fee + flat fee, then capped by the per-execute cap, the task's per-run cap, the per-epoch fee budget and the automation's balance. The fee never reverts a run.
| Default | Value |
|---|---|
| maxGasPrice | 1 gwei (4663 runs near 0.02 gwei) |
| premium | 20 % |
| max per execute | 0.002 ETH |
| budget per 7-day fee epoch | 0.02 ETH |
| L1 fee | included (ArbGasInfo) |
Keep some ETH in the automation: when its balance is below one run's max fee, or the fee budget is used up, the dashboard asks for a deposit (a plain ETH send to the automation).
Tasks, triggers and runs
A task is up to 16 steps; each step is one call template plus up to 8 slots. Every byte outside a slot is fixed by the owner. Slots are filled at run time: by the contract (a balance, the run's inflow, now + N, a copy of another slot) or, only for OPERATOR slots, by the operator within the owner's bounds (e.g. a swap's minimum output).
| Trigger | Holds when |
|---|---|
| interval | at least N seconds since the last run started |
| balance threshold | an asset balance of the automation (or another holder) is at least X |
| checker | a stateless checker returns true, e.g. PonsFeeChecker: claimable creator fees >= X |
| none | manual: any authorized operator may run it (limits still apply) |
Atomic tasks run in one transaction: a failing required step reverts everything and nothing is charged. allowFailure steps (sweeps, claims) may fail without stopping the run. Run states: pending -> simulating -> submitted -> succeeded | failed; submitted -> stuck (no receipt after gas bumps; the dashboard says recovering) -> resubmitted or failed; skipped when not eligible.
The policy engine
Every run goes through one function, Automation.execute(taskId, resolvedCalls). The operator proposes the calls; the contract binds them to the owner's template, checks each final call against the policy, measures what moved, charges the allowances and pays a capped fee. Anything outside the rules reverts the whole transaction.
allowFailure steps may fail on their own.| Stage | Rule | Revert |
|---|---|---|
| Authorize | !paused && registry.isActiveOperator(op) && !revoked[op] && (!operatorRestricted || allowed[op]); the task exists and is active. | NotAuthorizedOperator, TaskNotFound, TaskInactive |
| Trigger | At run start only (nextStep == 0): every non-zero condition holds (interval, balance threshold, checker). All-zero = manual. | TriggerNotMet(reason) |
| Bind | Same target and calldata length as the template; every byte outside a slot window equals the owner's; without a VALUE slot the value is the template's. Non-OPERATOR slots are computed by the contract and written over the operator's word. | TemplateMismatch(step), OperatorSlotOutOfRange, SlotValueOverflow |
| Check | (target, selector) permitted; value only where allowValue; the validator (if any) accepts the final call; never the automation itself, the factory or the registry. | CallNotPermitted, ValueNotPermitted, ValidatorRejected, ForbiddenTarget |
| Call | target.call{value: v}(data) with all gas minus a 50k reserve. An allowFailure step may fail; a required step reverts an atomic run. | StepFailed(step, reason) |
| Deltas | Every tracked asset (max 16, ETH first) is read before and after every call; gross inflow and outflow are summed per asset. | |
| Caps | Outflow is charged against the per-epoch cap and the task's per-run cap. ERC-20 transfer / approve / transferFrom go only to allowed recipients; approvals are reset to 0 before execute returns. | AllowanceExceeded, RunCapExceeded, RecipientNotAllowed, UntrackedAsset |
| Fee | min(computed, maxFeePerExecute, task cap, epoch fee budget, balance) in ETH to the operator, only when a step succeeded. Capped fees emit FeeCapped. | never |
What an operator can choose: when to run (within the trigger), OPERATOR slot values within the owner's bounds (e.g. a swap's minOut), skipping allowFailure steps, and stopping a stepped run. What it cannot: change a target, selector, recipient or any template byte, add calls, or move more than the allowances.
Loosening and tightening
Every policy change is a PolicyUpdate{op, data}. Tightening applies at once. Loosening applies at once when policyDelay is 0; otherwise updatePolicy reverts LooseningRequiresTimelock(index) and the owner queues it: queuePolicyUpdate, then executeQueuedPolicyUpdate at or after the eta (max delay 30 days). Pause, revoke, deactivate and withdraw are never delayed.
| Op | Tightening (immediate) | Loosening (timelocked when policyDelay > 0) |
|---|---|---|
SET_TARGET | adds a validator where none existed, allowValue not increased | new permission, allowValue false -> true, validator changed or removed |
REMOVE_TARGET | always | |
SET_ASSET | lower cap, same epochLength | new asset, higher cap, any epochLength change |
REMOVE_ASSET | always | |
SET_RECIPIENT | allowed = false, or allowed = true for an address already allowed (no-op) | allowed = true for an address not yet allowed |
SET_FEE_POLICY | every limit equal or lower, feeEpochLength equal or longer, includeL1Fee not enabled | any limit higher, epoch shorter, includeL1Fee false -> true |
SET_OPERATOR_RESTRICTION | false -> true | true -> false |
SET_OPERATOR_ALLOWED | allowed = false, or allowed = true for an operator already allowed (no-op) | allowed = true for an operator not yet allowed |
SET_POLICY_DELAY | increase | decrease |
Deployed contracts
Robinhood Chain, chainId 4663. The factory row is checked against what this site's API reports in GET /v1/automation-stats. Per-launch pons curve and token addresses are never listed: they are read from ponsV2Factory.getLaunchedToken(token).
Asking the API which factory it serves.
Verification pending
Source verification on Blockscout is pending for Conductor's contracts: the addresses below are the deployment record, the bytecode is on chain, and the source is contracts/src at commit 71ac4e2.
| Contract | Address · Blockscout | Role |
|---|---|---|
| AutomationFactorychecking | 0xf5ca3e665269ab3fa5da098c093c58fabf5a746f | creates automations (EIP-1167 clones) and indexes them by owner |
| Automation (implementation) | 0x754afeb54c37756db86e110df90289ce12a97996 | the code every automation clone runs; initializers disabled |
| OperatorRegistry | 0x5f19edd6447af259bdffe8c847ec4e9f516be085 | allowlist of operators with an active flag |
| IntervalChecker | 0x6275074a33492d8da99d9e6a9b73a0c5ad1bb36b | stateless: at least N seconds since the last run |
| BalanceThresholdChecker | 0x93cc92e8aa4d26dd0b9c12d9a740cd56a4aec9da | stateless: a balance is at least X |
| PonsFeeChecker | 0x4fa61399164b06b5ca4499d6eac3b9b237782905 | stateless: claimable pons creator fees are at least X |
| UniversalRouterV4Validator | 0x8fecea6e128ab91d89102ecad7b40324398335b8 | the only accepted Universal Router call shape |
| PonsRewardsFlowBuilder | 0x5228b68010acb6d3f926e44d3ee2fbac64debb8c | canonical encoder of the fees -> rewards task (view) |
Called by the fees -> rewards template (verified addresses)
| Contract | Address · Blockscout | Role |
|---|---|---|
| pons v2 factory | 0x7ed598bcef8bd9edd8c97a195c6d13f40801ec7e | launch records; transferCreatorFeeRecipient |
| pons v2 meme hook | 0xe5e702641ea86f4ae6cc3cdaed2b886f976be044 | pool hook; sweepPoolFees |
| pons v2 fee escrow | 0xd3afeb2a57f70ef218aa82451c51b2fb0416ac9e | creator fee escrow; claim() |
| Uniswap v4 Universal Router | 0x8876789976decbfcbbbe364623c63652db8c0904 | the swap step (V4_SWAP, exact in) |
| Uniswap v4 PoolManager | 0x8366a39cc670b4001a1121b8f6a443a643e40951 | pools of graduated pons launches |
| Uniswap v4 V4Quoter | 0x987e643e0d2b5a2b56ab52a0c0036c508d3451fa | quotes for the operator's minOut |
The workflow
Template pons-rewards/v1: whenever your pons v2 project has earned enough creator fees, claim them, swap the ETH to the reward token on its graduated Uniswap v4 pool, and fund your rewards contract. The automation itself becomes the creator fee recipient.
| # | Call | On failure | Filled at run time |
|---|---|---|---|
| 0 | curve.sweepFees(0) | continue | — |
| 1 | memeHook.sweepPoolFees(poolId, 0, 0) | continue | — |
| 2 | feeEscrow.claim() | continue | — |
| 3 | universalRouter.execute{value}(V4_SWAP, ...) | revert all | value = ETH balance - reserve; minOut by the operator (quote - slippage); deadline = now + 300 |
| 4 | rewardToken.transfer(target, balance) or approve + fund(uint256) | revert all | amount = reward token balance |
| Field | Meaning | Default |
|---|---|---|
| feeToken | the pons v2 launch whose creator fees are claimed (ETH-quoted) | required |
| rewardToken | token bought with the fees: a graduated (phase 2) ETH-quoted launch; may be the same token | required |
| rewardsTarget | rewards contract, distributor or wallet that receives the tokens | required |
| fundMode | transfer, or approveAndCall with fundSelector (one uint256 argument) | transfer |
| minClaimable | claimable fees that make a run worthwhile (wei) | required |
| minInterval | seconds between runs | 3600 |
| nativeReserve | ETH kept for operator fees | 0.001 ETH |
| maxSwapInPerRun | per-run ETH cap, 0 = none | 0 |
| nativeCapPerEpoch | ETH outflow allowance per epoch | required |
| rewardCapPerEpoch | reward-token outflow allowance | uncapped |
| epochLength | fixed window in seconds | 604800 (7 days) |
| maxFeePerRun | max operator fee per run (wei) | 0.002 ETH |
Activation
The rule builder previews the plan as you type, then simulates it against the chain with nothing deployed. Activation is blocked until the simulation passes. Then you sign, in order:
- Create the automation with the policy and the task (
factory.createAutomation), or for an existing automationupdatePolicythencreateTask. - Claim what is already credited to you (
escrow.claim()): credited fees do not move with the recipient. - Hand the fees to the automation:
ponsFactory.transferCreatorFeeRecipient(feeToken, automation), signed by the current fee recipient.
Undo at any time: the owner calls ownerCall with transferCreatorFeeRecipient(feeToken, ownerWallet) and withdraws.
| Failure | What happens |
|---|---|
| not enough claimable | eligibility says so; the worker re-checks on the escrow's Credited event |
| sweep needs pons's operator | step 0/1 fails, the run continues |
| price moved | the swap reverts on minOut; the run fails with slippage and retries with a fresh quote |
| epoch allowance used up | AllowanceExceeded in simulation; next check at the epoch end |
| paused / operator revoked | nothing runs until the owner changes it |
Reads
Public, no key. Lists are { data, next } with cursor pagination like every other route. Without deployed contracts every route below except /v1/automation-stats and /v1/workflows answers 503 not_configured. A freshly created automation can be 404 for a few seconds while the mirror catches up. Try it fills automation and run ids from the newest ones the API knows.
curl -s "http://127.0.0.1:8787/v1/automation-stats"GET/v1/automations?owner=&template=&paused=&limit=&cursor=
curl -s "http://127.0.0.1:8787/v1/automations?owner=0x3d8a0670d278c125966f884dc66bea34944a4220&limit=10"curl -s "http://127.0.0.1:8787/v1/automations/0x5b1f000000000000000000000000000000000000"GET/v1/automations/:address/tasks[/:taskId]
curl -s "http://127.0.0.1:8787/v1/automations/0x5b1f000000000000000000000000000000000000/tasks"curl -s "http://127.0.0.1:8787/v1/automations/0x5b1f000000000000000000000000000000000000/tasks/0"GET/v1/automations/:address/runs?taskId=&status=&limit=&cursor=
curl -s "http://127.0.0.1:8787/v1/automations/0x5b1f000000000000000000000000000000000000/runs?limit=20"GET/v1/runs?automation=&status=&limit=&cursor=
curl -s "http://127.0.0.1:8787/v1/runs?status=failed,stuck&limit=20"GET/v1/runs/:id
curl -s "http://127.0.0.1:8787/v1/runs/run_381"curl -s "http://127.0.0.1:8787/v1/operators"curl -s "http://127.0.0.1:8787/v1/workflows"Simulate and preview
POST/v1/simulate
Exactly one of task (an existing task, automation.simulate at latest) or draft (nothing deployed: the AutomationSimulator creation-code call). Always 200 with ok: false and the decoded revert when the execution would fail. Limited to SIMULATE_RATE_PER_MIN per IP.
curl -s -X POST "http://127.0.0.1:8787/v1/simulate" \
-H "content-type: application/json" \
-d '{"task":{"automation":"0x5b1f000000000000000000000000000000000000","taskId":0}}'Try it fills in the newest automation the API knows, so the task exists.
Every simulation is cached for 10 minutes under its id; worker-recorded simulations carry source: "worker" and a runId. 404 after expiry.
curl -s "http://127.0.0.1:8787/v1/simulations/sim_9c2b5e0d4a1f3e7b6c8d9a0b"POST/v1/workflows/pons-rewards/preview
The rule builder's single call: resolves both launches, quotes the swap, builds the task and policy, simulates, and returns the ordered txs to sign plus warnings.
curl -s -X POST "http://127.0.0.1:8787/v1/workflows/pons-rewards/preview" \
-H "content-type: application/json" \
-d '{"owner":"0x3d8a0670d278c125966f884dc66bea34944a4220","feeToken":"0xa24326d9bcbd110c918dcf386472441b9347cbf6","rewardToken":"0x68ff81dfff6eccefb74706fe1e0a86320cb2eb96","rewardsTarget":"0x3d8a0670d278c125966f884dc66bea34944a4220","fundMode":"transfer","minClaimable":"10000000000000000","nativeCapPerEpoch":"1000000000000000000"}'The preview accepts an optional slippageBps (default WORKER_SLIPPAGE_BPS). It answers 400 with details[{ path, message }] for missing or invalid fields and 422 unsupported for a fee or reward token that is not ETH-quoted or not graduated. Draft simulations run on a throwaway clone, so simulation.trigger.ok is usually false: judge the run by executed and stepResults.
Transactions to sign
These routes only encode. They return a TxRequest { chainId, to, data, value, from, description }; send it from the from wallet unchanged.
POST/v1/automations/prepare-create
POST/v1/automations/:address/calldata
| action | args | signer |
|---|---|---|
setPaused | { paused } | owner |
setTaskActive | { taskId, active } | owner |
setOperatorRevoked | { operator, revoked } | owner |
withdraw | { asset, to, amount } (amount may be "max") | owner |
deposit | { amount } (plain ETH send) | anyone |
createTask | { task } | owner |
updatePolicy / queuePolicyUpdate | { updates } | owner |
executeQueuedPolicyUpdate / cancelQueuedPolicyUpdate | { id } | owner |
abortRun | { taskId } | owner |
ownerCall | { calls, value? } | owner |
transferOwnership / acceptOwnership | { newOwner } / {} | owner / pending owner |
curl -s -X POST "http://127.0.0.1:8787/v1/automations/0x5b1f000000000000000000000000000000000000/calldata" \
-H "content-type: application/json" \
-d '{"action":"setPaused","args":{"paused":true}}'POST /v1/policy/encode ABI-encodes policy updates; with automation it also says which updates loosen the policy and whether they apply immediately.
curl -s -X POST "http://127.0.0.1:8787/v1/policy/encode" \
-H "content-type: application/json" \
-d '{"updates":[{"op":"setRecipient","recipient":"0x3d8a0670d278c125966f884dc66bea34944a4220","allowed":true}]}'Terminal
The lattice CLI's grammar over this site's API: the same console as /app/terminal and the drawer on every /app page (Ctrl+). Reads run here; writes print a transaction for your wallet. Tab completes commands and automation addresses, the arrow keys walk the history, --json prints raw JSON, tail` streams runs and logs.
Dashboard routes
Read-only routes behind /app, /explore and the terminal: an overview, an eligibility timeline, metric series, a leaderboard, the worker's logs and a live stream. Same error envelope, cursor and rate-limit rules as above. overview, timeline, timeseries and leaderboard answer 503 not_configured while AUTOMATION_FACTORY is unset; worker/logs and stream always work. owner (optional wherever accepted) scopes to one owner's automations; omitted = protocol-wide.
Pages on this site
| Route | What | Reads |
|---|---|---|
/app | the control room: tiles, alerts, live run feed, automation cards with allowance bars and next eligible, eligibility timeline, operator health; scoped to the connected wallet or protocol-wide | overview, timeline, runs, stream |
/app/terminal | the terminal full page; a slide-up drawer on every /app page, toggled with Ctrl+` | every read route; writes return calldata for the wallet |
/explore | charts (runs per day, ETH moved, success rate, operators, gas), the leaderboard and recent runs across the protocol | timeseries, leaderboard, runs, stream |
/app/a/:address | one automation: policy, allowances, balances, tasks, runs | automations/:address, runs |
/app/a/:address/runs/:id | one run: steps, transactions, deltas, quote vs realized | runs/:id |
/app/new | create an automation: the fees -> rewards builder or an empty one | workflows, preview, prepare-create |
Overview
curl -s "http://127.0.0.1:8787/v1/dashboard/overview?owner=0x3d8a0670d278c125966f884dc66bea34944a4220"{ "asOf": "2026-10-22T21:27:19.034Z", "chainId": 4663, "syncedBlock": 2000, "headBlock": 2700,
"tiles": { "automations": 3, "activeTasks": 4, "runs24h": 2, "runsSucceeded24h": 1, "runsFailed24h": 1,
"successRate30d": 0.6667, "ethMoved24h": "100000000000000000", "feesPaid24h": "1000000000000000",
"depositTotal": "15000000000000000", "runwayDays": 52.5, "operatorsActive": 1 },
"automations": [ { "address": "0x...d001", "owner": "0x...0e1", "label": "fees -> rewards", "paused": false,
"nativeBalance": "5000000000000000", "allowance": [ { "asset": "0x0000000000000000000000000000000000000000",
"symbol": "ETH", "decimals": 18, "usedInEpoch": "0", "capPerEpoch": "1000000000000000000",
"epochEndsAt": "2026-10-29T00:00:00.000Z" } ], "nextEligibleAt": "2026-10-22T21:26:18.863Z",
"nextEligibleReason": "interval", "lastRunAt": "2026-10-22T20:57:18.870Z", "lastRunStatus": "succeeded",
"runCount": 4, "feeBudgetRemaining": "1500000000000000" } ],
"alerts": [ { "id": "al_ebf536299673ade6", "kind": "fee_budget_low", "severity": "warning", "automation": "0x...d001",
"taskId": null, "message": "fee budget 0.0015 ETH is below 3x the average fee (0.000667 ETH)",
"at": "2026-10-22T21:27:19.034Z", "href": "/app/a/0x...d001" } ],
"operators": [ { "address": "0x...00be", "active": true, "heartbeatAt": "2026-10-22T21:27:18.887Z", "syncedBlock": 2000,
"balance": "30000000000000000", "pendingTxs": 0, "runs24h": 3, "lastError": "out of budget" } ] }| Field | Rule |
|---|---|
syncedBlock | the worker's sync:<chainId> cursor, else the highest automation syncedBlock; null before the first sync |
headBlock | eth_blockNumber (10 s cache, 1.5 s budget), else the indexer's chain_state (4663 only), else null |
tiles.runs*24h | runs started in the last 24 h (runs24h counts every status) |
tiles.successRate30d | succeeded / (succeeded + failed) over 30 days, 4 decimals; null with no finished runs |
tiles.ethMoved24h | sum of native ETH outflow deltas of runs started in the last 24 h (wei) |
tiles.depositTotal | sum of the automations' native balances (wei) |
tiles.runwayDays | depositTotal / (feesPaid over the last 7 days / 7), 2 decimals; null when no fee was paid in 7 days |
tiles.operatorsActive | active registry operators (protocol-wide even with owner) |
automations | most recent activity first (lastRunAt, then creation), at most 500; owner lowercase; allowance = the mirrored policy.assets |
nextEligible* | from the timeline: an eligible-now task wins, else the earliest dated task, else the first live task's reason (none without tasks, paused when paused) |
operators | registry operators that are active or have a worker row, plus workers' operators missing from the registry; balance is an RPC read (30 s cache), null when the RPC does not answer; lastError = newest failed run of that operator in 24 h |
Alerts are sorted critical, warning, info, then newest at first. id = al_ + 16 hex, stable for the same condition so the UI can remember dismissals. href is null for alerts without an automation.
| kind | severity | When |
|---|---|---|
fee_budget_low | warning; critical below 1x | active, unpaused automation with feeBudgetRemaining < 3 x the average fee of its last 10 fee-paying runs, or nativeBalance < 2 x the largest maxFeePerRun of its active tasks |
task_parked | warning | failed_attempts >= WORKER_MAX_ATTEMPTS and retry_after in the future (taskId set) |
policy_queued | info | a queued (timelocked) policy update; the message carries the eta |
operator_stalled | critical | a worker heartbeat older than 120 s |
run_failed | warning | a failed run started in the last 24 h (newest 20); href = /app/a/<automation>/runs/run_<n> |
worker_lagging | warning | headBlock - syncedBlock > 600 |
Timeline
Every task, newest automation first, then task id. hours (1-720, default 24) only sets the returned from / to drawing window; tasks due later are still listed.
curl -s "http://127.0.0.1:8787/v1/dashboard/timeline?hours=24"{ "data": [
{ "automation": "0x...d001", "label": "fees -> rewards", "taskId": 0, "taskLabel": "fees -> rewards",
"nextEligibleAt": "2026-10-22T21:57:18.870Z", "reason": "interval", "intervalSeconds": 3600,
"lastRunAt": "2026-10-22T20:57:18.870Z", "eligibleNow": false, "details": {} },
{ "automation": "0x...d003", "label": "fees -> rewards", "taskId": 1, "taskLabel": "checker task",
"nextEligibleAt": null, "reason": "checker", "intervalSeconds": 0, "lastRunAt": null, "eligibleNow": false,
"details": { "unswept": "12", "claimable": "0" } } ],
"from": "2026-10-22T21:27:19.121Z", "to": "2026-10-23T21:27:19.121Z" }Reason order: paused > inactive > parked (nextEligibleAt = retry_after) > interval (lastRunAt + intervalSeconds still in the future) > the worker's cached checkTrigger result (balance => threshold, checker_false / checker_reverted => checker, ok / run_open => eligibleNow: true) > the trigger shape when nothing is cached > unknown. details = the cached eligibility details (pons: claimable, unswept, phase).
Metrics and leaderboard
bucket defaults to hour. to defaults to now and from to 23 hours (hour) / 29 days (day) before to, so the default is 24 / 30 points. Both are truncated to the bucket and every bucket in between is present ("0" when empty). Values are decimal strings. More than 1000 points => 400 bad_request.
curl -s "http://127.0.0.1:8787/v1/metrics/timeseries?metric=runs&bucket=hour"| metric | unit | Value per bucket (by run `started_at`, UTC) |
|---|---|---|
runs (default) | count | runs started |
ethMoved | wei | native ETH outflow deltas |
successRate | ratio | succeeded / (succeeded + failed), up to 4 decimals, "0" when none finished |
gas | gas | sum of gasUsed |
feesPaid | wei | sum of operator fees |
automations | count | cumulative automations created before the end of the bucket |
operators | count | cumulative registry operators active at the end of the bucket; protocol-wide, owner is ignored |
by = runs | volume | fees, window = 24h | 7d | 30d | all. Defaults by=runs, window=7d, limit 20 (max 100). Only automations with at least one run in the window. volume = native ETH outflow; successRate is null when none finished. Ties: more runs, then address.
curl -s "http://127.0.0.1:8787/v1/leaderboard?by=volume&window=7d&limit=10"Worker logs
Newest first; cursor = {id}. limit default 200, max 1000. level is a minimum (warn = warn + error; debug lines exist only when the worker runs with LOG_LEVEL=debug). The worker batches every structured log line into the table each second without blocking its run loop and keeps the newest 5000 rows. loop is sync, run or submit (null for the app).
curl -s "http://127.0.0.1:8787/v1/worker/logs?level=warn&limit=50"{ "data": [ { "id": 123, "at": "2026-10-22T21:27:19.034Z", "level": "warn", "loop": "run", "msg": "run failed",
"fields": { "automation": "0x...d001", "taskId": 0, "runId": 12, "code": "simulation_failed" },
"automation": "0x...d001", "runId": "run_12" } ], "next": "eyJpZCI6MTIzfQ" }Live stream
GET/v1/stream
Server-Sent Events. types (default all of run, automation, worker, log, heartbeat) and owner filter; an unknown type => 400. Each event has an increasing id; reconnect with Last-Event-ID (EventSource does this) or ?lastEventId= to replay the last 500. A heartbeat arrives on connect and every 15 s.
curl -sN "http://127.0.0.1:8787/v1/stream?types=run,heartbeat"Try it listens for 8 seconds or 6 events, whichever comes first. A heartbeat arrives on connect.
retry: 3000
id: 1761168439001
event: heartbeat
data: {"t":"2026-10-22T21:27:19.034Z","headBlock":2700,"syncedBlock":2000}
id: 1761168439002
event: run
data: {"id":"run_12","automation":"0x...d001","taskId":0,"status":"submitted",...,"change":"created"}- Payloads:
run=RunRecord+change(created|updated);automation=AutomationSummary+change;worker= anoperators[]row of the overview;log= one worker log line. workerandheartbeatignoreowner;loglines without an automation only reach streams withoutowner.- At most 200 concurrent streams per API process; above that
503 unavailablewithretry-after: 10. A client that stops reading is disconnected after 1 MB buffered. - The site's live views degrade to polling
/v1/runsand/v1/worker/logsevery 5 s when the stream is unavailable.
Health
GET/v1/health
ok = database reachable and chain state updated within 60 s. 200 when ok, 503 otherwise, same body.
{ "ok": true, "chainId": 4663, "version": "0.1.0", "db": "ok",
"headBlock": 94596923, "safeBlock": 94596911, "headAgeSeconds": 1.4 }curl -s "http://127.0.0.1:8787/v1/health"Stats
GET/v1/stats
Public and cheap. The indexer strip on /data polls it every 10 s.
curl -s "http://127.0.0.1:8787/v1/stats"{ "chainId": 4663, "contractsIndexed": 12, "contractsLive": 11,
"eventsIndexed": 1840022, "transfersIndexed": 1530044, "holdersTracked": 40211,
"headBlock": 94596923, "safeBlock": 94596911, "indexedBlock": 94596920, "lagBlocks": 3,
"updatedAt": "2026-10-21T12:00:00.000Z" }Register a contract
POST/v1/contracts
| Field | Required | Notes |
|---|---|---|
address | yes | Must have code, else 422 unsupported. |
kind | no | erc20 | erc721 | abi. Omitted => detected by the indexer. |
abi | for abi | JSON ABI array, max 256 KB. Its events are decoded. For erc20/erc721 it adds extra events. |
startBlock | no | Omitted => the deploy block is found. With a later startBlock, balances are the net change since startBlock, holders are addresses with a positive net, and holders?block= is consistent with that. |
label | no | Max 64 chars. |
curl -s -X POST "http://127.0.0.1:8787/v1/contracts" \
-H "content-type: application/json" \
-d '{"address":"0xa24326d9bcbd110c918dcf386472441b9347cbf6","label":"GITFUND"}'Public registration needs no key (5 per minute per IP). The register form on /data uses it.
Response: 201 (new) or 200 (already registered, unchanged) with ContractInfo. Status moves pending -> backfilling -> live; error carries a reason; paused is set by the operator.
Read contracts
Query: status, kind, limit, cursor. Ordered createdAt DESC, address ASC. Returns Page<ContractInfo>.
curl -s "http://127.0.0.1:8787/v1/contracts?status=live&limit=20"curl -s "http://127.0.0.1:8787/v1/contracts/0xa24326d9bcbd110c918dcf386472441b9347cbf6"{ "address": "0xa24326d9bcbd110c918dcf386472441b9347cbf6", "kind": "erc20", "label": "GITFUND",
"name": "GitFund", "symbol": "GITFUND", "decimals": 18, "totalSupply": "1000000000000000000000000000",
"status": "live", "error": null, "startBlock": 92482929, "indexedThrough": 94596920, "progress": 1,
"holdersCount": 412, "transfersCount": 9811, "eventsCount": 15230, "hasAbi": false,
"createdAt": "2026-10-21T11:58:00.000Z", "updatedAt": "2026-10-21T12:00:00.000Z" }Holders and balances
GET/v1/contracts/:address/holders
Query: block (as-of), limit, cursor. Sorted balance DESC, holder ASC. Zero balances and the zero address never appear. share = balance / sum of all balances at that block. erc721: balance = tokens held.
curl -s "http://127.0.0.1:8787/v1/contracts/0xa24326d9bcbd110c918dcf386472441b9347cbf6/holders?limit=10"{ "contract": "0xa243...cbf6", "block": 94596920, "holderCount": 412, "decimals": 18,
"data": [
{ "holder": "0x5ea3...b5ef", "balance": "120000000000000000000000000", "formatted": "120000000", "share": 0.12 }
],
"next": "eyJiIjoiMTIwMDAw..." }GET/v1/contracts/:address/holders/:holder
Balance of one address. Query: block. A non-holder returns balance: "0" with 200. erc721 adds tokenIds (ascending, max 1000).
curl -s "http://127.0.0.1:8787/v1/contracts/0xa24326d9bcbd110c918dcf386472441b9347cbf6/holders/0x3d8a0670d278c125966f884dc66bea34944a4220"GET/v1/contracts/:address/tokens/:tokenId
erc721 only. Query: block. Returns { contract, tokenId, owner | null, block }. erc20 => 409 conflict.
curl -s "http://127.0.0.1:8787/v1/contracts/0xa24326d9bcbd110c918dcf386472441b9347cbf6/tokens/1"GET/v1/addresses/:address/balances
Every registered erc20/erc721 contract where the address has a non-zero balance. Query: limit, cursor.
curl -s "http://127.0.0.1:8787/v1/addresses/0x3d8a0670d278c125966f884dc66bea34944a4220/balances?limit=20"Transfers
GET/v1/contracts/:address/transfers
erc20 / erc721. Query: address (either side), direction = in | out | any (only with address), tokenId, fromBlock, toBlock (inclusive), order, limit, cursor. Returns Page<Transfer>.
curl -s "http://127.0.0.1:8787/v1/contracts/0xa24326d9bcbd110c918dcf386472441b9347cbf6/transfers?order=desc&limit=20"GET/v1/addresses/:address/transfers
Transfers touching an address across every registered contract. Query: contract, direction, fromBlock, toBlock, order, limit, cursor.
curl -s "http://127.0.0.1:8787/v1/addresses/0x3d8a0670d278c125966f884dc66bea34944a4220/transfers?contract=0xa24326d9bcbd110c918dcf386472441b9347cbf6&order=desc&limit=20"{ "data": [ { "contract": "0xa243...cbf6", "blockNumber": 94596900, "blockHash": "0x...",
"blockTime": "2026-10-21T11:59:58.000Z", "txHash": "0x...", "logIndex": 3,
"from": "0x...", "to": "0x...", "amount": "1000000000000000000",
"tokenId": null, "confirmed": true } ],
"next": null }Events
GET/v1/contracts/:address/events
Every log of the contract, decoded when the ABI matches (undecoded logs have name: null, args: null). Query: event (e.g. Transfer; _unknown = undecoded only), address (matches any address-typed arg), txHash, fromBlock, toBlock, order, limit, cursor.
curl -s "http://127.0.0.1:8787/v1/contracts/0xa24326d9bcbd110c918dcf386472441b9347cbf6/events?event=Transfer&order=desc&limit=20"{ "data": [ { "contract": "0xa243...cbf6", "blockNumber": 94596900, "blockHash": "0x...",
"blockTime": null, "txHash": "0x...", "txIndex": 1, "logIndex": 3,
"name": "Transfer", "signature": "Transfer(address,address,uint256)",
"args": { "from": "0x...", "to": "0x...", "value": "1000000000000000000" },
"topics": ["0xddf252ad...", "0x000...", "0x000..."], "data": "0x...",
"confirmed": true } ],
"next": "eyJuIjo5NDU5NjkwMCwiaSI6M30" }args is keyed by ABI input name (index string when unnamed); integers as decimal strings, addresses lowercase, bytes as hex, arrays and tuples as arrays and objects.
Snapshots
GET/v1/contracts/:address/snapshot
The full holder list at a block, for airdrops and holder-gated payouts. Not paginated. Query: block (default min(safeBlock, indexedThrough)), format = json | csv, minBalance (raw units, default 1). Too many holders => 409 conflict. Snapshots at or below the safe block are immutable and may be cached.
curl -s "http://127.0.0.1:8787/v1/contracts/0xa24326d9bcbd110c918dcf386472441b9347cbf6/snapshot?format=json"Set format to csv in the Try-it panel and every tab switches to the CSV download.
{ "contract": "0xa243...cbf6", "kind": "erc20", "symbol": "GITFUND", "decimals": 18, "block": 94596911,
"holderCount": 412, "total": "1000000000000000000000000000",
"rows": [ { "holder": "0x5ea3...b5ef", "balance": "120000000000000000000000000", "formatted": "120000000" } ] }holder,balance,formatted
0x5ea3...b5ef,120000000000000000000000000,120000000erc721 rows add tokenIds (JSON) or a token_ids column with ids separated by ; (CSV). Rows are sorted balance DESC, holder ASC.
Manage webhooks
A webhook subscribes to logs of one registered contract, optionally narrowed to one event name and/or one address (matched against any address-typed argument). Every route needs write auth.
| Method | Path | Body / query | Response |
|---|---|---|---|
| POST | /v1/webhooks | { contract, url, eventName?, address?, description?, fromBlock? } | 201 with secret, shown once |
| GET | /v1/webhooks | contract?, limit, cursor | Page<Webhook> |
| GET | /v1/webhooks/:id | Webhook | |
| PATCH | /v1/webhooks/:id | { active?, url?, description? } | Webhook; active: true resets failures |
| DELETE | /v1/webhooks/:id | 204 | |
| GET | /v1/webhooks/:id/deliveries | limit, cursor | Page<WebhookDelivery> |
| POST | /v1/webhooks/:id/test | 200 WebhookDelivery (sends a ping) |
Create
curl -s -X POST "http://127.0.0.1:8787/v1/webhooks" \
-H "x-api-key: $LATTICE_API_KEY" \
-H "content-type: application/json" \
-d '{"contract":"0xa24326d9bcbd110c918dcf386472441b9347cbf6","url":"https://example.com/hooks/lattice","eventName":"Transfer"}'contract must be registered (404). url must be http(s); https only in production; private and loopback hosts are rejected. eventName must exist in the ABI when it is known (400). fromBlock defaults to the current chain head; pass a lower value, e.g. 0, to also receive history. The webhook object returns it as fromBlock.
List, read, enable, test
The reads need your key too: paste it into the Try-it panel. It is sent with that one request and never stored.
curl -s "http://127.0.0.1:8787/v1/webhooks?contract=0xa24326d9bcbd110c918dcf386472441b9347cbf6&limit=20" \
-H "x-api-key: $LATTICE_API_KEY"curl -s "http://127.0.0.1:8787/v1/webhooks/wh_3f9a0c1d2e4b5a6978877665" \
-H "x-api-key: $LATTICE_API_KEY"curl -s -X PATCH "http://127.0.0.1:8787/v1/webhooks/wh_3f9a0c1d2e4b5a6978877665" \
-H "x-api-key: $LATTICE_API_KEY" \
-H "content-type: application/json" \
-d '{"active":true}'curl -s "http://127.0.0.1:8787/v1/webhooks/wh_3f9a0c1d2e4b5a6978877665/deliveries?limit=20" \
-H "x-api-key: $LATTICE_API_KEY"curl -s -X POST "http://127.0.0.1:8787/v1/webhooks/wh_3f9a0c1d2e4b5a6978877665/test" \
-H "x-api-key: $LATTICE_API_KEY"Deliveries
- Only confirmed logs are delivered: the POST goes out once the head is 12 blocks past the log (about 1-2 s on 4663).
- A reorg before delivery deletes the pending delivery. After delivery the row is marked
reverted; no retraction is sent in v0. - A webhook only delivers logs with
blockNumber >= fromBlock.fromBlockdefaults to the chain head at creation, so history is never replayed, not during a backfill and not on a later re-index. Opt in with"fromBlock": 0(or any block) on create to also receive history the indexer writes after that; expect one POST per log. - Order is best effort. Use
event.blockNumberandevent.logIndexto order.
POST <your url>
content-type: application/json
user-agent: lattice-webhooks/0.1
x-lattice-webhook-id: wh_3f9a0c1d2e4b5a6978877665
x-lattice-delivery-id: dlv_5512
x-lattice-event: Transfer
x-lattice-signature: t=1792617600,v1=5b0e...c4{
"id": "dlv_5512", "type": "event", "webhookId": "wh_3f9a0c1d2e4b5a6978877665",
"attempt": 1, "sentAt": "2026-10-21T12:00:03.120Z", "chainId": 4663,
"contract": "0xa24326d9bcbd110c918dcf386472441b9347cbf6",
"event": { "blockNumber": 94596900, "txHash": "0x...", "logIndex": 3,
"name": "Transfer", "args": { "from": "0x...", "to": "0x...", "value": "1000000000000000000" },
"confirmed": true, "...": "same fields as the events API" },
"transfer": { "from": "0x...", "to": "0x...", "amount": "1000000000000000000", "tokenId": null, "...": "..." }
}transfer is non-null only for Transfer logs of erc20/erc721 contracts. id is stable across retries: dedupe on it. Test pings send { "type": "ping", "id": "ping_...", ... } with x-lattice-event: ping.
Verify signatures
x-lattice-signature: t=<unix seconds>,v1=<hex HMAC-SHA256> over ${t}.${rawBody}, keyed with the whole whsec_... secret as UTF-8. Verify the raw body before parsing it, reject when |now - t| > 300, and compare in constant time. Every retry is re-signed with a fresh t.
import { createHmac, timingSafeEqual } from "node:crypto";
function verify(secret, rawBody, header, tolerance = 300) {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=").map((s) => s.trim())));
const t = Number(parts.t);
if (!Number.isInteger(t) || Math.abs(Date.now() / 1000 - t) > tolerance) return false;
const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
return expected.length === parts.v1?.length && timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}The SDK exports the same verifySignature(secret, rawBody, header, tolerance?), built on WebCrypto, for Node and browsers.
Retries and disabling
- Success = any 2xx within 10 s. The first 1024 bytes of the response are logged.
- Anything else (non-2xx, timeout, network error, a redirect) is a failed attempt. Redirects are not followed.
- Up to 8 attempts. Delays: 10 s, 30 s, 2 m, 10 m, 30 m, 1 h, 2 h, plus 0-10 % jitter.
410 Gonedisables the webhook immediately.- 20 consecutive failed deliveries disable it. Re-enable with
PATCH /v1/webhooks/:id {"active": true}; failures while disabled are not replayed. - Every attempt, pings included, is listed at
GET /v1/webhooks/:id/deliveries.
SDK and CLI
The TypeScript SDK has one method per route, async iterators for paginated reads, LatticeError (status, code, message), and verifySignature. Zero dependencies.
import { Lattice } from "@lattice/sdk";
const lattice = new Lattice({ baseUrl: process.env.LATTICE_API_URL });
const stats = await lattice.stats();
for await (const holder of lattice.iterateHolders("0xa24326d9bcbd110c918dcf386472441b9347cbf6")) {
console.log(holder.holder, holder.formatted);
}The CLI reads LATTICE_API_URL and LATTICE_API_KEY (or --api, --key).
lattice register 0xa24326d9bcbd110c918dcf386472441b9347cbf6 --label GITFUND --wait
lattice status 0xa24326d9bcbd110c918dcf386472441b9347cbf6
lattice holders 0xa24326d9bcbd110c918dcf386472441b9347cbf6 --limit 20
lattice snapshot 0xa24326d9bcbd110c918dcf386472441b9347cbf6 --block 94596911 --csv --out holders.csv
lattice webhook add|ls|rm|test ...