CONDUCTOR
Wallet

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.

shell
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

TopicRule
AddressesAccepted in any case. Always returned lowercase. Invalid => 400 invalid_address.
uint256balance, amount, tokenId, totalSupply, total are decimal strings in raw units. formatted = raw / 10^decimals, no rounding.
NumbersBlock numbers and counts are JSON numbers.
TimesISO-8601 UTC strings. blockTime may be null.
confirmedblockNumber <= 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.
limitDefault 100. Max 1000 for holders, events, transfers, balances; 200 for contracts, webhooks, deliveries. Out of range is clamped.
orderdesc (default, newest first) or asc, for events and transfers.
blockOptional as-of block on holder, balance, owner and snapshot reads. Must satisfy startBlock - 1 <= block <= indexedThrough, else 409 not_indexed.
Kindserc20, erc721 (holders = token counts), abi (events only; holder endpoints => 409 conflict).

Errors

json
{ "error": { "code": "not_found", "message": "contract 0xabc... is not registered" } }
HTTPcodeWhen
400bad_request, invalid_address, invalid_cursorValidation.
401unauthorizedWrite route, keys configured, x-api-key missing or unknown.
404not_foundUnknown contract, webhook or route.
409conflictEndpoint not valid for the contract kind; snapshot larger than the row limit.
409not_indexedblock outside the indexed range. Details carry indexedThrough and startBlock.
422unsupportedAddress has no code; kind not detectable and no ABI; ABI invalid.
422limit_reachedContract or webhook limit reached.
429rate_limitedPer-IP limit. x-ratelimit-* and retry-after headers are set.
500internalBug.
503unavailableDatabase unreachable.

Auth and limits

  • Reads need no auth.
  • Writes (POST /v1/contracts and every /v1/webhooks route) take x-api-key: <key> when the operator configures keys. With no keys configured, writes are open.
  • POST /v1/contracts also works without a key when public registration is on (the default), limited to 5 per minute per IP. The register form on /data uses 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 Automation contract 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).

ModeReady when
AnyAlways. Carries the poolId so the worker can wake the task on that pool's events.
PriceOutsideRangetick < tickLower or tick >= tickUpper.
PriceInsideRangetickLower <= tick < tickUpper.
LiquidityBelowliquidity < liquidityThreshold.
LiquidityAboveliquidity > liquidityThreshold.
  • A pool that was never initialised (sqrtPriceX96 == 0) is not ready in any mode except Any.
  • The range modes revert InvalidRange() when tickLower >= 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.

MarkerWakes on
v4.pool.swapSwap on the PoolManager for the task's pool.
v4.pool.liquidityModifyLiquidity for the task's pool.
v4.pool.initializeInitialize 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.anyAny 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.

IdNameStatusWhat it doesTrigger
pons-rewards/v1Claim hook fees -> fund rewardsliveSweep the curve, sweep the hook's pool fees, claim the escrow, swap on v4, send to rewards.PonsFeeChecker, pons.escrow.credited
v4-rebalance/v1Rebalance when price leaves the rangeplannedCollect, burn the position, mint around the current tick.PriceOutsideRange, v4.pool.swap
v4-oracle-refresh/v1Refresh a hook's oracleplannedCall hook.update() (owner-chosen selector).interval + v4.pool.swap
v4-dynamic-fee/v1Step a dynamic fee with volumeplannedhook.setFee(poolId, fee) from a volume band.interval, v4.pool.swap
v4-twamm/v1Run a large order in slicesplannedUniversal Router swap of a fixed slice.interval + PriceInsideRange
v4-liquidity-floor/v1Top up a pool below a liquidity floorplannedMint liquidity from the automation's balance.LiquidityBelow, v4.pool.liquidity
blankBlank automationliveCreate 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).

RouteReturns
GET /v1/hooks?limit&cursorHooks, most pools first: address, flags, permissions, pools, swaps in 24 h, first seen, known name.
GET /v1/hooks/:addressOne hook and its pools, newest first.
GET /v1/pools?hook=&limit&cursorPools: currencies, fee (dynamic or fixed), tick spacing, hook, tick, liquidity, swap count.
GET /v1/pools/:poolIdOne pool with its last 20 swaps and live tick, sqrtPriceX96, liquidity, lpFee from StateView (null when the RPC fails).

GET/v1/hooks?limit=&cursor=

GET/v1/hooks/:address

GET/v1/pools?hook=&limit=&cursor=

GET/v1/pools/:poolId

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.

RoleWhoCan
Owneryour wallet (an EOA or a Safe)everything on its automation: tasks, policy, pause, revoke operators, withdraw any asset, ownerCall
Operatora keeper key in the OperatorRegistry (v0: Conductor's worker)execute tasks within the rules, abortRun after a timeout, get paid a capped fee
Protocol ownerdeployer key, then a multisigadd operators and set the implementation for NEW automations; never moves your funds
Anyonesimulate, 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: withdraw and ownerCall ignore 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.

ChangeApplies
Lower a cap, remove a target or recipient, restrict operators, raise the timelockimmediately
New target, higher cap, new recipient, looser fee policy, shorter timelockimmediately 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.

DefaultValue
maxGasPrice1 gwei (4663 runs near 0.02 gwei)
premium20 %
max per execute0.002 ETH
budget per 7-day fee epoch0.02 ETH
L1 feeincluded (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).

TriggerHolds when
intervalat least N seconds since the last run started
balance thresholdan asset balance of the automation (or another holder) is at least X
checkera stateless checker returns true, e.g. PonsFeeChecker: claimable creator fees >= X
nonemanual: 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.

The policy engine: execute, trigger, then for each step template binding, call check, call and balance deltas, then caps and recipients, then the operator fee.for each stepexecute()operator authorized, task activetriggerinterval, threshold, checker (run start)template bindingbytes outside slots fixed; slots resolvedcall checktarget + selector, value, validatorcalltarget.call{value}(data), no delegatecallbalance deltaspre/post per tracked assetcaps + recipientsepoch + per-run caps, recipientsoperator feecapped, never reverts the run
One transaction. Solid arrows run in order; the dashed loop repeats the step stages for every call of the task. Any check that fails reverts the whole transaction; only allowFailure steps may fail on their own.
StageRuleRevert
Authorize!paused && registry.isActiveOperator(op) && !revoked[op] && (!operatorRestricted || allowed[op]); the task exists and is active.NotAuthorizedOperator, TaskNotFound, TaskInactive
TriggerAt run start only (nextStep == 0): every non-zero condition holds (interval, balance threshold, checker). All-zero = manual.TriggerNotMet(reason)
BindSame 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
Calltarget.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)
DeltasEvery tracked asset (max 16, ETH first) is read before and after every call; gross inflow and outflow are summed per asset.
CapsOutflow 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
Feemin(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.

OpTightening (immediate)Loosening (timelocked when policyDelay > 0)
SET_TARGETadds a validator where none existed, allowValue not increasednew permission, allowValue false -> true, validator changed or removed
REMOVE_TARGETalways
SET_ASSETlower cap, same epochLengthnew asset, higher cap, any epochLength change
REMOVE_ASSETalways
SET_RECIPIENTallowed = false, or allowed = true for an address already allowed (no-op)allowed = true for an address not yet allowed
SET_FEE_POLICYevery limit equal or lower, feeEpochLength equal or longer, includeL1Fee not enabledany limit higher, epoch shorter, includeL1Fee false -> true
SET_OPERATOR_RESTRICTIONfalse -> truetrue -> false
SET_OPERATOR_ALLOWEDallowed = false, or allowed = true for an operator already allowed (no-op)allowed = true for an operator not yet allowed
SET_POLICY_DELAYincreasedecrease

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).

Factory per the API—
API chain · synced block—
Deploy block (record)95,393,762

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.

Conductor contracts on chain 4663
ContractAddress · BlockscoutRole
AutomationFactorychecking0xf5ca3e665269ab3fa5da098c093c58fabf5a746fcreates automations (EIP-1167 clones) and indexes them by owner
Automation (implementation)0x754afeb54c37756db86e110df90289ce12a97996the code every automation clone runs; initializers disabled
OperatorRegistry0x5f19edd6447af259bdffe8c847ec4e9f516be085allowlist of operators with an active flag
IntervalChecker0x6275074a33492d8da99d9e6a9b73a0c5ad1bb36bstateless: at least N seconds since the last run
BalanceThresholdChecker0x93cc92e8aa4d26dd0b9c12d9a740cd56a4aec9dastateless: a balance is at least X
PonsFeeChecker0x4fa61399164b06b5ca4499d6eac3b9b237782905stateless: claimable pons creator fees are at least X
UniversalRouterV4Validator0x8fecea6e128ab91d89102ecad7b40324398335b8the only accepted Universal Router call shape
PonsRewardsFlowBuilder0x5228b68010acb6d3f926e44d3ee2fbac64debb8ccanonical encoder of the fees -> rewards task (view)

Called by the fees -> rewards template (verified addresses)

pons v2 and Uniswap v4 contracts on chain 4663
ContractAddress · BlockscoutRole
pons v2 factory0x7ed598bcef8bd9edd8c97a195c6d13f40801ec7elaunch records; transferCreatorFeeRecipient
pons v2 meme hook0xe5e702641ea86f4ae6cc3cdaed2b886f976be044pool hook; sweepPoolFees
pons v2 fee escrow0xd3afeb2a57f70ef218aa82451c51b2fb0416ac9ecreator fee escrow; claim()
Uniswap v4 Universal Router0x8876789976decbfcbbbe364623c63652db8c0904the swap step (V4_SWAP, exact in)
Uniswap v4 PoolManager0x8366a39cc670b4001a1121b8f6a443a643e40951pools of graduated pons launches
Uniswap v4 V4Quoter0x987e643e0d2b5a2b56ab52a0c0036c508d3451faquotes 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.

#CallOn failureFilled at run time
0curve.sweepFees(0)continue—
1memeHook.sweepPoolFees(poolId, 0, 0)continue—
2feeEscrow.claim()continue—
3universalRouter.execute{value}(V4_SWAP, ...)revert allvalue = ETH balance - reserve; minOut by the operator (quote - slippage); deadline = now + 300
4rewardToken.transfer(target, balance) or approve + fund(uint256)revert allamount = reward token balance
FieldMeaningDefault
feeTokenthe pons v2 launch whose creator fees are claimed (ETH-quoted)required
rewardTokentoken bought with the fees: a graduated (phase 2) ETH-quoted launch; may be the same tokenrequired
rewardsTargetrewards contract, distributor or wallet that receives the tokensrequired
fundModetransfer, or approveAndCall with fundSelector (one uint256 argument)transfer
minClaimableclaimable fees that make a run worthwhile (wei)required
minIntervalseconds between runs3600
nativeReserveETH kept for operator fees0.001 ETH
maxSwapInPerRunper-run ETH cap, 0 = none0
nativeCapPerEpochETH outflow allowance per epochrequired
rewardCapPerEpochreward-token outflow allowanceuncapped
epochLengthfixed window in seconds604800 (7 days)
maxFeePerRunmax 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 automation updatePolicy then createTask.
  • 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.

FailureWhat happens
not enough claimableeligibility says so; the worker re-checks on the escrow's Credited event
sweep needs pons's operatorstep 0/1 fails, the run continues
price movedthe swap reverts on minOut; the run fails with slippage and retries with a fresh quote
epoch allowance used upAllowanceExceeded in simulation; next check at the epoch end
paused / operator revokednothing 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.

GET/v1/automation-stats

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"

GET/v1/automations/:address

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"

GET/v1/operators

curl -s "http://127.0.0.1:8787/v1/operators"

GET/v1/workflows

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.

GET/v1/simulations/:id

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

POST/v1/policy/encode

actionargssigner
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.

terminal

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

RouteWhatReads
/appthe 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-wideoverview, timeline, runs, stream
/app/terminalthe terminal full page; a slide-up drawer on every /app page, toggled with Ctrl+`every read route; writes return calldata for the wallet
/explorecharts (runs per day, ETH moved, success rate, operators, gas), the leaderboard and recent runs across the protocoltimeseries, leaderboard, runs, stream
/app/a/:addressone automation: policy, allowances, balances, tasks, runsautomations/:address, runs
/app/a/:address/runs/:idone run: steps, transactions, deltas, quote vs realizedruns/:id
/app/newcreate an automation: the fees -> rewards builder or an empty oneworkflows, preview, prepare-create

Overview

GET/v1/dashboard/overview

curl -s "http://127.0.0.1:8787/v1/dashboard/overview?owner=0x3d8a0670d278c125966f884dc66bea34944a4220"
json
{ "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" } ] }
FieldRule
syncedBlockthe worker's sync:<chainId> cursor, else the highest automation syncedBlock; null before the first sync
headBlocketh_blockNumber (10 s cache, 1.5 s budget), else the indexer's chain_state (4663 only), else null
tiles.runs*24hruns started in the last 24 h (runs24h counts every status)
tiles.successRate30dsucceeded / (succeeded + failed) over 30 days, 4 decimals; null with no finished runs
tiles.ethMoved24hsum of native ETH outflow deltas of runs started in the last 24 h (wei)
tiles.depositTotalsum of the automations' native balances (wei)
tiles.runwayDaysdepositTotal / (feesPaid over the last 7 days / 7), 2 decimals; null when no fee was paid in 7 days
tiles.operatorsActiveactive registry operators (protocol-wide even with owner)
automationsmost 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)
operatorsregistry 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.

kindseverityWhen
fee_budget_lowwarning; critical below 1xactive, 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_parkedwarningfailed_attempts >= WORKER_MAX_ATTEMPTS and retry_after in the future (taskId set)
policy_queuedinfoa queued (timelocked) policy update; the message carries the eta
operator_stalledcriticala worker heartbeat older than 120 s
run_failedwarninga failed run started in the last 24 h (newest 20); href = /app/a/<automation>/runs/run_<n>
worker_laggingwarningheadBlock - syncedBlock > 600

Timeline

GET/v1/dashboard/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"
json
{ "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

GET/v1/metrics/timeseries

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"
metricunitValue per bucket (by run `started_at`, UTC)
runs (default)countruns started
ethMovedweinative ETH outflow deltas
successRateratiosucceeded / (succeeded + failed), up to 4 decimals, "0" when none finished
gasgassum of gasUsed
feesPaidweisum of operator fees
automationscountcumulative automations created before the end of the bucket
operatorscountcumulative registry operators active at the end of the bucket; protocol-wide, owner is ignored

GET/v1/leaderboard

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

GET/v1/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"
json
{ "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.

sse
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 = an operators[] row of the overview; log = one worker log line.
  • worker and heartbeat ignore owner; log lines without an automation only reach streams without owner.
  • At most 200 concurrent streams per API process; above that 503 unavailable with retry-after: 10. A client that stops reading is disconnected after 1 MB buffered.
  • The site's live views degrade to polling /v1/runs and /v1/worker/logs every 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.

json
{ "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"
json
{ "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

FieldRequiredNotes
addressyesMust have code, else 422 unsupported.
kindnoerc20 | erc721 | abi. Omitted => detected by the indexer.
abifor abiJSON ABI array, max 256 KB. Its events are decoded. For erc20/erc721 it adds extra events.
startBlocknoOmitted => 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.
labelnoMax 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

GET/v1/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"

GET/v1/contracts/:address

curl -s "http://127.0.0.1:8787/v1/contracts/0xa24326d9bcbd110c918dcf386472441b9347cbf6"
json
{ "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"
json
{ "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"
json
{ "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"
json
{ "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.

json
{ "contract": "0xa243...cbf6", "kind": "erc20", "symbol": "GITFUND", "decimals": 18, "block": 94596911,
  "holderCount": 412, "total": "1000000000000000000000000000",
  "rows": [ { "holder": "0x5ea3...b5ef", "balance": "120000000000000000000000000", "formatted": "120000000" } ] }
csv
holder,balance,formatted
0x5ea3...b5ef,120000000000000000000000000,120000000

erc721 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.

MethodPathBody / queryResponse
POST/v1/webhooks{ contract, url, eventName?, address?, description?, fromBlock? }201 with secret, shown once
GET/v1/webhookscontract?, limit, cursorPage<Webhook>
GET/v1/webhooks/:idWebhook
PATCH/v1/webhooks/:id{ active?, url?, description? }Webhook; active: true resets failures
DELETE/v1/webhooks/:id204
GET/v1/webhooks/:id/deliverieslimit, cursorPage<WebhookDelivery>
POST/v1/webhooks/:id/test200 WebhookDelivery (sends a ping)

Create

key
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.

key
curl -s "http://127.0.0.1:8787/v1/webhooks?contract=0xa24326d9bcbd110c918dcf386472441b9347cbf6&limit=20" \
  -H "x-api-key: $LATTICE_API_KEY"
key
curl -s "http://127.0.0.1:8787/v1/webhooks/wh_3f9a0c1d2e4b5a6978877665" \
  -H "x-api-key: $LATTICE_API_KEY"
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}'
key
curl -s "http://127.0.0.1:8787/v1/webhooks/wh_3f9a0c1d2e4b5a6978877665/deliveries?limit=20" \
  -H "x-api-key: $LATTICE_API_KEY"
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. fromBlock defaults 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.blockNumber and event.logIndex to order.
http
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
json
{
  "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.

node
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 Gone disables 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.

ts
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).

shell
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 ...

Keyboard shortcuts

  • Control roomga
  • New automationgn
  • Terminalgt
  • Runs, protocol-widegr
  • Explorege
  • Hooksgh
  • Datagd
  • Docsgo
  • Homegg
  • Toggle the terminal drawer (app pages)ctrl+`
  • Show this list?
  • Close a menu or this listesc