# Flows documentation (agent-readable full text)
Source: generated from frontend/scripts/generate-docs-text.mjs. Regenerate with npm --prefix frontend run docs:llms.
# Deploy & Wallet
Flows deploys code that can move your money, but every move is governed by a policy that can refuse. The normal path is: create or scaffold an action, dry-run it locally, deploy it to the Lit network, promote it, and watch attested receipts.
## Automations and cron polling
Automations can run on a cadence. The trigger is set at install as trigger: { kind: 'cron', spec: ... } with exactly one of { everySec: 900 } (numeric seconds, minimum 15) or { cron: "*/5 * * * *" } (a 5-field cron string evaluated in UTC). Six-field cron strings with seconds are rejected at install time. For range-watching bots, use cron polling rather than price-trigger providers in v1; a useful Aerodrome concentrated-liquidity cadence is every five minutes (*/5 * * * *).
For a CUSTOM deployed action, the trigger is passed at install time on the deploy command: flows deploy --wallet --every OR --cron "*/5 * * * *". The two flags are mutually exclusive, and they only apply when --wallet installs (a dry-run-only deploy has no cadence). The trigger is NOT a manifest field — the manifest schema rejects a trigger key. A DRYRUN install ticks on its schedule too — for a deployed action every tick runs the full proposer + gate + seat simulation without signing (see State & sagas below); promotion switches the same loop to live dispatch.
A proposer should read chain state on each tick, then either return a proposal or a no-op result such as { act: false, reason: 'IN_RANGE' }. No-op ticks are expected operational events, not errors.
Provision an RPC for read-heavy proposers. A */5 cron proposer doing 5-6 reads per tick (position, pool, TWAP, balances) will hit the public mainnet.base.org rate limits. Use a dedicated Base RPC provider, declare that RPC host in the action manifest's egress.apiHosts (and the org policy's allowedApiHosts), and space reads out / retry with backoff instead of bursting.
## Which policy judges a dry-run
flows deploy WITHOUT --policy judges the preview against a SYNTHETIC default policy — permissive caps, egress auto-permitted from the manifest's declared hosts — NOT your org policy; the CLI prints a banner naming it. Live ticks are judged by the real org policy + standing-auth grants + install bounds, which may be far stricter (e.g. fail-closed egress). Trust a DENY absolutely; treat an ALLOW under the default policy as "passes the rules I gave it", not as your org policy's allow. To preview against a real policy, pass --policy @policy.json to flows deploy, or run flows action dry-run --params @fixtures/move.json --policy @policy.json with your exported org policy.
## State & sagas
Automations receive a bounded per-install state blob on each tick and may return nextState. Treat that state as untrusted hints. It is for cursors, debounce counters, last-seen ids, and saga progress; it must never be used as a gate fact, never override chain or venue reads, and never widen a policy decision.
For a multi-step rebalance, use a saga: one gated move per tick, with the cursor in state and ground truth re-derived before each move. The LP range-keeper reference steps are IN_RANGE -> DEBOUNCING (N out-of-range ticks) -> UNSTAKE -> EXIT (decrease liquidity + collect) -> ROUTE_DEX_SWAP or ROUTE_CB_DEPOSIT -> ROUTE_CB_TRADE -> ROUTE_CB_WITHDRAW -> MINT (a centered range) -> STAKE -> IN_RANGE. Note a deployed automation binds exactly ONE certified executor, so the ROUTE_DEX_SWAP leg (trade-spot, dex-swap seat) runs as a SEPARATE automation from the contract-call legs (onchain-call seat) — one install cannot span both seats.
Idempotency keys for venue legs should be derived from installId, sagaEpoch, and step. Never take the sagaEpoch raw from the state blob: clamp it against a durable record so it advances at most once per rebalance (the reference saga's clampEpoch), otherwise a buggy proposer that bumps the epoch every retry mints a fresh key each time and defeats venue dedupe. EVM transactions do not have an idempotency key, so on-chain legs need a host-side nonce/txHash fence and must reconcile chain state before retrying.
Ticks arrive three ways: a manual flows automations tick , the install-time tick a wallet deploy runs immediately, and scheduled ticks (the scheduler fires DRYRUN and LIVE installs alike; it does not filter on mode). What a tick does depends on the install kind. For a DEPLOYED custom action, EVERY tick — all three entry points, DRYRUN and LIVE — runs the exact linked proposer in the TEE, then the host classifier and notional derivation, the org policy + grant checks, and the certified seat; a DRYRUN tick signs and broadcasts nothing, and persists the returned nextState to a SEPARATE dryrun state lane (the live lane starts fresh at promotion). For a generic/template install, a DRYRUN tick simulates the gate verdict for the manifest and params only — no proposer runs and no state persists. Either way, treat promotion to live as the first shakedown of the saga against real broadcasts and live spend reservations.
Before on-chain steps, check the PKP Base ETH gas balance. A zero-gas PKP should produce a clear held/no-op tick such as GAS_PREFLIGHT_FAILED, not a mid-saga revert.
---
# Primitives
Build automations by importing published primitives instead of hand-rolling security-sensitive code. Current first-party packages include flows-policy for the gate, flows-jcs for canonical JSON signing bytes, flows-defi-validator for swap and transaction validation, lit-venues for exchange connectors, flows-verify for signed envelope verification, flows-client for invoking flows, and flows-clmm for concentrated-liquidity math, calldata codecs, LP validation, and pricing.
Pin exact package versions in action source. A deployed action imports ESM packages through jsDelivr. The version pin is part of the action source and therefore part of the action CID identity. Edit a dependency pin and you have a new CID.
## Contract calls
The contract-call verb is for generic, certified on-chain calldata. A manifest must declare a contractCall block. This snippet is structural only: replace every placeholder with verified Base addresses and raw 4-byte selectors before dry-run. Selector entries are 0x plus 8 hex characters; do not put function names or signatures in selectorAllowlist. Resolve selectors from the verified ABI.
SUPPORTED SCOPE TODAY: concentrated-liquidity (CL) only — Uniswap-v3-style / Aerodrome Slipstream. The certified onchain-call executor does not just check the allowlist; it decodes and classifies every call in-TEE and refuses anything it cannot recognize as a known LP operation. Today it understands exactly: the CL NonfungiblePositionManager (mint, increaseLiquidity, decreaseLiquidity, collect, burn) and ERC-20/721 approve under profile univ3 or slipstream, plus the CL gauge (deposit, withdraw, getReward) under profile slipstream only (Uniswap v3 has no gauge; staking a CL position for AERO is Slipstream-specific). Classic constant-product AMMs are NOT supported: e.g. the Aerodrome v2 volatile/stable Router (addLiquidity(tokenA, tokenB, bool stable, ...)), Uniswap v2, or any non-CL LP entrypoint. Allowlisting one PASSES the dry-run (the gate checks allowlist, selector, chainId, and notional structurally, not the classifier) but REFUSES at execution as unclassifiable calldata. If you are managing a full-range or AMM position, this executor is not for it yet — target a CL pool.
{
"notional": { "kind": "param", "param": "notionalUsd" },
"contractCall": {
"chainId": 8453,
"toAllowlist": [
"0x",
"0x",
"0x",
"0x"
],
"selectorAllowlist": [
"0x<8-hex-mint-selector>",
"0x<8-hex-decreaseLiquidity-selector>",
"0x<8-hex-collect-selector>",
"0x<8-hex-gaugeDeposit-selector>",
"0x095ea7b3"
],
"maxValueWei": "0",
"roles": {
"0x": "positionManager",
"0x": "gauge",
"0x": "erc20",
"0x": "erc20"
},
"gaugeAddress": "0x",
"maxDeadlineSec": 1800,
"lpConstraints": {
"pools": [{ "token0": "0x", "token1": "0x", "tickSpacing": 100 }],
"rangeWidthTicks": { "min": 200, "max": 4000 },
"maxTicksFromTwap": 1000
}
}
}
For LP work, roles maps each lowercase target to the closed enum positionManager, gauge, erc20, erc721, pool, or router. The router role declares the profile's SwapRouter for single-hop exactInputSingle swaps — there is no default, so an undeclared router refuses as UNCLASSIFIABLE_CALL even when allowlisted, and any unsupported router ABI stays unclassifiable regardless. Aggregator-routed swaps use trade-spot through the certified dex-swap executor instead, with api.enso.finance in the installed automation's egress.apiHosts. Flows strips egress from the derived no-code grant while retaining it on the executable install. The declared notional is host-derived, compared within 100 bps, and re-derived in-TEE.
The executor refuses unless every transaction target is in toAllowlist, every selector is in selectorAllowlist, the chainId matches, and the calldata is classifiable by a certified validator. Empty or unconstrained allowlists are not a shortcut. Agent-class initiators need contract-call explicitly in their standing-auth verb allowlist; broad trade grants must not silently become arbitrary calldata authority.
The signed call plan is bound to the judged intent by params.callPlanHash = sha256(JCS(callPlan)). The host computes and injects callPlanHash when the proposer omits it — a proposer may supply it but never needs to. The executor recomputes the hash in the TEE, refuses on mismatch, and verifies that the main call equals the judged intent fields. Contract-call pricing is derived in-TEE from decoded calldata, CID-pinned stable and anchor token tables, pool reads, and TWAP deviation checks. Host-supplied pricing context is not trusted. Unpriceable calls fail closed.
Params shape for gate judgment: a contract-call intent's params must ALSO carry the flat main-call fields to, selector, and chainId (and value when nonzero) alongside callPlan / profile / notionalUsd. The gate judges the flat fields — missing params.to or params.selector denies as contractCall.underspecified, and a missing params.chainId denies as contractCall.chainMissing — while the executor and validator consume the callPlan. Dry-run fixtures need both.
## flows-clmm
The @lit-protocol/flows-clmm package is the concentrated-liquidity primitive. It provides tick math, amount/liquidity helpers, calldata encoders and decoders, classifyCall, validateLpPlan, TWAP helpers, and USD pricing for stable-paired pools. Profiles include univ3 and slipstream.
encodeCall(profile, shape, args) takes the target contract address INSIDE args as args.to — there is no separate to parameter, and the call throws (invalid address) without it. A worked two-liner:
const approve = encodeCall('slipstream', 'erc20Approve', { to: token0, spender: positionManager, amount })
const mint = encodeCall('slipstream', 'mint', { to: positionManager, token0, token1, tickSpacing, tickLower, tickUpper, amount0Desired, amount1Desired, amount0Min, amount1Min, recipient: pkpAddress, deadline, sqrtPriceX96 })
Each returns { to, data, value } ready to place in a callPlan. Aerodrome Slipstream uses a parameterized profile; addresses and ABIs must be verified from Aerodrome docs and Basescan, not copied from examples as gospel.
flows-clmm validates LP plans. For CL mint, quote expected consumption, normalize both desired maxima to that token ratio, then calculate each minimum with integer math: min = desired × (10,000 − slippageBps) / 10,000, rounded down. Pricing supports pools with an allowlisted USD-stable side (USDC on Base) directly, and pools with no stable side (for example LITKEY/WETH) through a pinned anchor (WETH on Base): the anchor's USD price is derived from the same factory's anchor/stable reference pool (Slipstream tickSpacing 100, Uniswap v3 fee 500) under the same tuple cross-check and TWAP deviation guard. A pool with neither a stable nor an anchor side refuses UNPRICEABLE_NO_USD_ROUTE inside POOL_DERIVATION_FAILED — fail-closed, not a bug. A successful receipt's legacy approvalVerified: false does not mean failure; inspect approvalVerification for explicit performed and reason semantics.
## Future third-party primitives
First-party certified executors remain first-party because they sign. Future user-published support should arrive as pinned packages and proposer templates, not as an open executor registry. The validator registry is the certification seam: unclassifiable calldata refuses until a reviewed validator package supports that protocol shape.
---
# Actions & Governance
The canonical manifest verbs are read, trade-spot, transfer-internal, withdraw, and contract-call. A deployed automation binds exactly ONE certified executor, so one live install serves one seat: the LP range-keeper's position-management legs (contract-call, onchain-call seat) and its swap legs (trade-spot, dex-swap seat) run as SEPARATE automations. Across those installs the range-keeper normally uses read, trade-spot, transfer-internal, and contract-call; it should not need withdraw unless intentionally sending funds to an external address. Read gathers balances, positions, pool ticks, and venue state. Trade-spot handles exchange spot orders. Transfer-internal handles transfers that stay under the owner-controlled venue or wallet path, such as an attested Coinbase deposit leg. Contract-call handles certified, allowlisted calldata for LP operations.
Policies and manifests declare the maximum envelope. A policy can refuse. A manifest can only restrict what the deployed automation may do. Code alone does not grant power.
## Leash an agent key
To let a coding agent deploy and run a bot without giving it owner authority, create an OAuth agent token with flows login or a dashboard API key, then have that key request a standing-auth grant for itself with grantee.id "self". An owner approves the request in Dashboard -> Sign-offs. For an Aerodrome LP range-keeper, the grant should include verbs [read, trade-spot, transfer-internal, contract-call], allow the relevant symbols/assets, set per-order and daily USD caps, include the exact bounded contractCall surface from the automation manifest, and expire on a planned date. A grant may list multiple verbs and cover several installs, but each deployed automation binds exactly one executor — the range-keeper's dex-swap legs are a separate install covered by the same grant. Who does what: venue connections, policy edits, and approving/applying the grant are owner/admin acts; the agent key itself can request its own grant, author, dry-run, deploy, tick, and read logs.
Two rules the manifest schema enforces on a grant like this. First, name is required (every manifest carries a human-readable name for the permission sheet). Second, a grant that includes the contract-call verb MUST carry a contractCall constraint block — and that block must be byte-identical (JCS-canonical) to the automation manifest's contractCall block: copy the exact contractCall block from your automation manifest into the grant. The TEE executor seat verifies containment against the attested sha256Jcs(contractCall) hash and fail-closes on any difference. A stricter subset grant does not authorize promotion; narrow the automation manifest first, then copy that exact block into the grant. (A grant WITHOUT the contract-call verb must not declare a contractCall block at all.)
Symbols are matched against the full symbol string the move carries: a CEX spot order is judged by its pair string (e.g. "ETH/USDC"), while a transfer or on-chain leg carries a SINGLE asset symbol — an on-chain dex-swap's candidate symbol is params.symbol if present, else its sellToken (compared uppercased), so allowlist "ETH" for a native ETH->WETH wrap, never a pair like "ETH/WETH". A symbolAllowlist must therefore include the CEX pair symbols as well as the individual assets, or every real spot trade holds.
Avoid bounds.venueAllowlist on Base contract-call grants unless your dry-run sample and live intent carry a venue value the gate can verify. A grant dry-run is fail-closed by axis: a constrained venue axis with no venue in the sample move denies. The Base chain and call surface are already pinned more tightly by contractCall.chainId, toAllowlist, selectorAllowlist, and maxValueWei.
Example standing-auth manifest for an agent leash (the contractCall block is structural placeholder shape, abbreviated — in a real grant it is the exact, complete block from your automation manifest, including roles / gaugeAddress / maxDeadlineSec / lpConstraints when the automation declares them):
{
"schema": "flows-capability-manifest-v1",
"name": "LP bot agent key — Aerodrome on Base",
"form": "standing-auth",
"grantee": { "kind": "api-key", "id": "" },
"verbs": ["read", "trade-spot", "transfer-internal", "contract-call"],
"bounds": {
"maxNotionalPerOrderUsd": 1000,
"maxNotionalPerDayUsd": 4000,
"symbolAllowlist": ["ETH/USDC", "ETH", "USDC", "AERO"]
},
"contractCall": {
"chainId": 8453,
"toAllowlist": ["0x", "0x", "0x", "0x"],
"selectorAllowlist": ["0x<8-hex-mint-selector>", "0x<8-hex-decreaseLiquidity-selector>", "0x<8-hex-collect-selector>", "0x<8-hex-gaugeDeposit-selector>", "0x095ea7b3"],
"maxValueWei": "0"
},
"expiry": "2026-08-01T00:00:00Z",
"revocable": true
}
Create it with:
flows deploy --grant @agent-leash.manifest.json
The resulting request appears in Dashboard -> Sign-offs, not flows approvals list. flows approvals list shows operational held moves only; grant requests, policy edits, and action changes are governance sign-offs.
Size daily caps for the whole saga, not just the position notional. One rebalance can consume roughly 3-4x the position notional against daily caps because exit, route, and mint are separately gated. If caps are too low, the saga may strand mid-route and require owner intervention or a safe-park path.
Prove the leash before trusting it: run at least one allowed dry-run and one intentional refusal, such as an off-allowlist target or selector. The refusal receipt is evidence that the agent key cannot exceed its grant.
Effective bounds intersect each axis: effective = min(manifest cap, CLI leash cap) when both exist; otherwise the defined cap applies. Grant matching uses the exact JCS-normalized constraint hash, so a grant may cover sibling installs with byte-identical constraints; CLI output reports origin-install and hash provenance.
---
# Tutorial: Build a custom DeFi automation
Goal: learn the complete ceremony for putting your own fund-moving code on Flows — any DeFi protocol, not a curated template. The worked example is a reward harvester that calls getReward on a concentrated-liquidity gauge on a schedule; the ceremony is identical for staking, unstaking, minting, or multi-step rebalancing.
The steps in order: author a proposer + manifest -> dry-run the gate locally -> deploy onto your wallet with a schedule -> request the DERIVED grant -> owner signs off -> promote -> operate. At every step, one command tells you what is blocking and what to run next: flows automations status . Before promotion, flows automations tick is the high-fidelity preflight: proposer output, production classifier and host notional, org policy, grant match, and certified-seat simulation, with no broadcast or live reservation consumption.
Trust model: your code proposes; it never signs. Each tick returns at most one proposal. The policy gate judges it in the TEE (allow / hold / deny) and only a certified executor signs — within the manifest's allowlists, the grant's caps, and the validator's rules. A refused move produces a receipt, not a loss.
## 1. Verify protocol facts
Never take contract addresses or selectors from examples. Resolve your protocol's contracts from its docs and the chain explorer's verified source; compute raw 4-byte selectors from the verified ABI (cast sig "getReward(uint256)").
Check your calls against the supported surface: the certified onchain-call executor signs only calldata it can CLASSIFY — concentrated-liquidity position management (mint, increaseLiquidity, decreaseLiquidity, collect, burn; univ3 and slipstream profiles), gauge staking (deposit / withdraw / getReward), and bounded ERC-20/721 approvals and transfers. A protocol outside that surface needs an owner-approved CUSTOM executor (flows executor publish -> escrowed source + capability report -> CID-exact owner approval; custom executors carry no Flows guarantees and are labeled custom, not certified).
## 2. Prepare the account
Create the on-chain wallet venue (flows venues connect-onchain --chain base), fund it with the assets the automation touches, and fund gas. Avoid native value: a contract-call moving ETH always holds for a human sign-off — keep maxValueWei "0" and work in wrapped assets (WETH) so no unattended tick blocks on a person.
Who does what: connecting venues and signing grants is owner work (ADMIN seat + passkey). Everything else — author, dry-run, deploy, request the grant, tick, read logs — runs under an agent API key that cannot exceed what the owner granted.
## 3. Author the manifest
The manifest is the automation's complete permission sheet; the gate enforces manifest INTERSECT policy, and anything undeclared is denied. Declare the minimum. The harvester needs one verb and one allowlisted call:
verbs ["contract-call"]; contractCall { chainId, toAllowlist [gauge], selectorAllowlist [getReward selector], maxValueWei "0", roles { gauge: "gauge" }, gaugeAddress, maxDeadlineSec }; egress.apiHosts [your RPC host].
Selectors are raw 4-byte hex from the verified ABI; roles map keys are lowercase addresses that must also appear in toAllowlist; egress.apiHosts declares the RPC host the proposer reads from (undeclared egress refuses). Save the manifest as a sibling file (harvester.manifest.json next to harvester.js) and flows deploy picks it up.
## 4. Author the proposer
The runtime calls a global async function main(params) (plain JS, not export default) with the install params spread flat plus reserved keys: state (your own nextState from the prior tick, max 4 KiB), venue, and mode. Return a no-op with a stable reason string, or exactly one proposal:
return { proposal: { verb: 'contract-call', params: { to, selector, chainId, callPlan: [{ to, data }], profile, notionalUsd } }, nextState }
The flat { to, selector, chainId } fields ride ALONGSIDE the callPlan — the gate judges the flat fields (missing ones deny as contractCall.underspecified); the executor validates and signs the callPlan. nextState is a cursor, never a source of truth — re-read the chain every tick. notionalUsd is a declaration the platform re-derives and cross-checks. For multi-step automations (unstake -> exit -> re-mint -> restake), return one gated move per tick and advance a saga cursor in nextState.
## 5. Prove the gate locally
flows deploy harvester.js --local runs the exact production gate in-process — no login, nothing signed. Prove the allow path AND a refusal (e.g. a target not in toAllowlist -> DENY contractCall.toNotAllowed) before deploying. The --local verdict is judged under a synthetic preview policy: trust a DENY absolutely; treat an ALLOW as provisional.
## 6. Deploy with a schedule
flows deploy harvester.js --wallet --every 3600 --params '{"gauge":"","tokenId":""}'
Wallet deploys always upload the source encrypted and link it automatically (CLI >= 0.4.x): every live tick runs your exact deployed source in the TEE, verified to hash to the deployed CID. --link-source is accepted for compatibility and does nothing. If live ticks deny with "deployed action has no linked source", the install came from an old CLI — upgrade (npm install -g @lit-protocol/flows@latest) and redeploy. --every N / --cron "expr" (UTC) set the schedule. Installs start in DRYRUN: scheduled ticks simulate and move nothing until promoted. The executor auto-resolves from verb + wallet type (contract-call on an EVM wallet -> onchain-call).
## 7. The grant — derived, signed by an owner
To promote, an owner must have issued a standing-auth grant covering this exact automation — for contract-call, with a contractCall block byte-identical (JCS) to the manifest's. Do not hand-copy it; derive it:
flows automations request-grant
The server derives the grant from the install: fund verbs from the resolved executor, the contractCall block copied verbatim from the registered manifest (byte-identical by construction), venue, and caps from the install bounds (--max-order / --max-day raise them; the owner sees and signs the final numbers). An agent key's request opens a pending sign-off; the owner approves in Dashboard -> Sign-offs with a passkey. Then: flows automations promote .
## 8. Promote and operate
flows automations status — the diagnosis: executor, linked source, bounds, grant state for YOUR identity, wallet grant, recent runs, and the exact next command
flows automations tick — run one tick now (dry-run simulates)
flows automations promote — DRYRUN -> LIVE (needs the signed grant)
flows automations logs — run history: verdicts + receipts
Troubleshooting: a registry entry binds one manifest to a source CID, so a manifest-only change needs a new CID. Use flows registry inspect ; conflicts report registered and submitted manifest hashes. Proven pre-commit/no-submit failures release spend reservations; ambiguous post-dispatch failures remain reserved fail-closed until the 00:00 UTC daily reset. "Did not pin this automation's exact contract-call constraints" means request a matching grant again.
## 9. Going further
The full LP range-keeper (unstake -> exit -> size-routed rebalance -> re-mint -> restake, with debounce, idempotency, and crash-resume) is the same ceremony with a saga cursor; the reference implementation lives in the flows repo under examples/lp-range-keeper. Swap legs go through the certified dex-swap executor (trade-spot + api.enso.finance egress), never through your contract-call allowlist. Protocols outside the certified surface use owner-approved custom executors.
---
# Payments
Payment operations are one of the things you deploy on Flows, and they run the same deploy loop as everything else. A form is a typed human-input node: people submit facts, the gate computes what is owed, and a payout runs. It fits contractor payouts, invoices, bounties, reimbursements.
Who holds the pen: it is all you, working WITH a coding agent (an API key, the AGENT role). Your coding agent does the build and operate work — create a form (it lands a DRAFT, inert), add and scope payees, and run pay periods (a run only ever computes and HOLDS; it can never move money). Flows requires you, IN PERSON with a passkey, for the two power-granting steps: publish a form (DRAFT to live, which authorizes its execution seat) and release each held payout. Those two steps have no API/CLI/MCP surface at all.
Lifecycle: build (agent, DRAFT) -> publish (you) -> the contractor authenticates by email and enters their facts plus their own payout address (screened, then approved by you; the payout destination is ALWAYS the attested address record, never a value from the submission facts) -> run the period (agent; every payout holds) -> release and pay (you, passkey): the certified onchain-send executor re-judges the frozen intent in the enclave and signs, USDC on Base with an attested receipt.
Safety: any address change re-runs the full screen-then-approve pipeline; the paying wallet carries the form's per-period cap as seat-enforced bounds; a held payout floors to L2 assurance (the seat verifies the approval plus your bound passkey step-up in-TEE before signing); in-policy auto-release is recorded but not auto-sent — releasing money with no person is deliberately not enabled.
From the CLI / MCP: flows payments (create form, add payee, process a period) and the flows_payments_* MCP tools cover the build side only. There is no publish or release command — those are your sign-offs in the dashboard.
## Drive a payout end to end (REST or MCP, with your API key)
REST base: https://flows.litprotocol.com/api with header Authorization: Bearer . MCP: point your coding agent at https://flows.litprotocol.com/mcp (same bearer). Your API key carries the AGENT role: it performs every build and operate step below and returns 403 on the two human steps (publish, release). A 403 on those two is the human boundary by design — hand that step to a person, do not retry it. Read the field named in each arrow (->) to chain to the next call.
MCP vs REST coverage: steps 1, 2, and 6 (add payee, create form, run period) plus the list/read calls each have a flows_payments_* / flows_wallet_* MCP tool, named inline below. Two things have NO MCP tool and are REST-only: scoping a payee to a form (step 3 — or set payeeScope "ALL_ACTIVE" and skip it entirely) and the public contractor lane (step 5, which uses no key at all). Publish and release have no programmatic surface — they are the human steps.
Noun guide (for talking to your human): the things that hold their money are ACCOUNTS — an on-chain wallet, a Coinbase account. "Venue" is the internal API word for where execution happens (a CEX is both an account and a venue; a wallet is an account whose venues are the chains/protocols it acts on). Ask "which account should fund this?", never "which venue".
0. Discover the paying account (the one value you must carry forward from here).
- GET /api/venues (MCP flows_wallet_venues) -> { connections: [ { id, venueType, status, pkpAddress, chains } ] } — the org's connected accounts. Pick the entry with venueType "onchain" and status "ACTIVE"; its id is your executionVenueId (the API field name for the funding account; pkpAddress is that wallet's address, chains lists where it can pay, e.g. base).
- Optional sanity check: GET /api/portfolio (MCP flows_wallet_portfolio) -> your holdings, each with usdValue — confirm the paying account holds USDC on Base before you go live.
1. Add a payee. POST /api/requests/payees { email, displayName } (both required) -> { payee: { id } }. Every payee must exist here before they can authenticate on any form — payeeScope only controls per-form scoping, not this registration. (MCP flows_payments_add_payee.)
2. Create the form — it lands DRAFT, inert. POST /api/requests/forms with:
{ "slug": "contractor-hours", "title": "Contractor hours",
"paramsSchema": [ { "name": "hours", "type": "number", "required": true } ],
"cadence": { "kind": "none" }, "approvalMode": "ALL", "authLevel": "L1",
"amountMode": "COMPUTED",
"amountConfig": { "rateUsdPerUnit": 0.01, "unitFactKey": "hours", "maxUnitsPerPeriod": 100 },
"executionVenueId": "", "payeeScope": "EXPLICIT_LIST" }
-> { form: { id, status: "DRAFT" } }. rateUsdPerUnit x maxUnitsPerPeriod is the seat's hard spend cap (maxUnitsPerPeriod is your per-period ceiling); unitFactKey must name a number field in paramsSchema. (MCP flows_payments_create_form.)
Field values: amountMode COMPUTED | STATED; approvalMode ALL (every payout holds for you) | EXCEPTIONS (gate decides, out-of-policy still holds); authLevel L0 (email OTP) | L1 (persistent account — use for money) | L2 (L1 + claim-time step-up); payeeScope EXPLICIT_LIST | ALL_ACTIVE (any non-suspended payee of the org — they must still be added in step 1); cadence { kind: "none" } | { kind: "weekly" }. A paramsSchema field's type is a free-form label shown to the payee ("number", "string", ...); only "number" carries semantics — the COMPUTED unitFactKey and the STATED amountUsd field must both be type "number".
STATED variant (the payee writes the amount — invoices, bounties): set "amountMode": "STATED", "amountConfig": { "maxStatedUsdPerPeriod": }, and add an { "name": "amountUsd", "type": "number", "required": true } field to paramsSchema. The payee submits facts.amountUsd (step 5); anything over maxStatedUsdPerPeriod force-holds regardless of approvalMode.
3. Scope the payee to the form. POST /api/requests/forms//payees/ -> 201. (Only needed when payeeScope is EXPLICIT_LIST.)
4. HUMAN — publish. Activating the form (PATCH /api/requests/forms//status { status: "ACTIVE" }) is ADMIN-only with no API-key path: your key 403s here by design. A person publishes it in the dashboard (Payments -> the form -> Publish). Confirm with GET /api/requests/forms/ -> status "ACTIVE". The contractor link is /pay/.
5. The contractor lane — public, NO key (full paths below, prefix /api/requests/f/). The person filling the form does this; to exercise it yourself:
- POST /api/requests/f//email/start { email } -> { emailToken }; a 6-digit OTP emails the payee.
- POST /api/requests/f//email/verify { email, code, emailToken } -> { formToken } (7-day TTL); use formToken as the Bearer for the next two calls.
- POST /api/requests/f//address { address, chain: "base", network: "mainnet" } -> the address parks PENDING_SCREEN then PENDING_APPROVAL; screened at once. You do NOT poll or approve it here — the owner approves this exact address as part of the single release in step 7. Submitting facts and running the period both proceed while it is pending.
- POST /api/requests/f//submit { facts: { hours: 2 } } -> the submission (2 x $0.01 = $0.02). (STATED form: facts: { amountUsd: 500 }.)
6. Run the period. POST /api/requests/forms//runs { periodKey: "open" } ("open" for cadence none; an ISO week like "2026-W27" for weekly) -> { run: { heldCount, outcomes: [ { status: "held", approvalRequestId, amountUsd } ] } }. Every payout HOLDS — nothing your key does can move money. One run processes all SUBMITTED submissions for that period; each held payout carries its own approvalRequestId. Idempotent per (formId, periodKey). (MCP flows_payments_process_period.)
Scheduling: for weekly forms set autoRun: true (at create, or PATCH /api/requests/forms//auto-run { "autoRun": true }) and the platform runs each period itself once its ISO week ends (Mon 00:05 UTC), emailing the owner when payouts hold. Do NOT build client-side cron around /runs — autoRun is the native scheduler. A scheduled run is the same compute-and-HOLD; release stays with the owner.
7. HUMAN — release. POST /api/requests/approvals//approve-and-pay is ADMIN + APPROVER with a passkey step-up and no API-key path: your key 403s. The owner opens Approvals, verifies the amount and the exact destination address, and releases with a passkey. That one act approves the screened address AND hands the frozen intent to the certified onchain-send executor, which re-judges it in the enclave and signs — USDC on Base with an attested receipt.
---
# CLI reference
Install: npm install -g @lit-protocol/flows. Config is stored at ~/.flows/config.json after login. Full per-flag reference: https://raw.githubusercontent.com/LIT-Protocol/flows/main/docs/cli.md
## Zero-account hello world
flows init scaffolds a runnable fund action (writes .js + .manifest.json + .params.json with working sample params), and flows deploy --local runs the exact production policy gate IN-PROCESS on your machine — no login, no backend, nothing signed — and prints the verdict with budget math. Templates (flows init --list): cex-limit (default), dca, rebalance, stop-loss, yield-enter, contract-call.
## Commands
- flows login — opens the browser to sign in; --key for headless/CI (create a key at /dashboard/api-keys, shown once).
- flows init [template] [name] — scaffold an action + manifest + sample params; --list shows templates; --force overwrites.
- flows deploy — fast policy check, install in DRYRUN (with --wallet ), then one high-fidelity dry-run tick. Flags: --manifest <@file>, --params <@file>, --verb, --leash (default 20000), --policy <@file> (overrides --leash), --executor , --every | --cron "<5-field UTC expr>" (cadence, requires --wallet, mutually exclusive), --name, --local (in-process gate, no login, cannot install). Without --policy the preview is judged against a SYNTHETIC default policy, not your org policy; live ticks are judged by the real org policy + grants + install bounds.
- flows deploy --grant <@manifest> — the no-code form: deploy a bounded standing-auth grant (form:"standing-auth", no source file). An agent key can only request for itself (grantee id "self"); owners sign in Dashboard -> Sign-offs.
- flows action check (alias dry-run) — judge one candidate move against a policy/grant without installing; --params, --policy/--leash, --notional, --local.
- flows venues catalog | list | connect | connect-onchain — connect a CEX (Binance, Coinbase; key sealed into the enclave) or use an on-chain wallet. The connection id — a bare CUID like cmrjyyo0i0018pa0dzylst6a8, no prefix — is your --wallet.
- flows portfolio — unified attested portfolio across connected venues.
- flows automations templates | list | install | tick | status | promote | logs — certified automations: install starts in DRYRUN, tick simulates or executes within bounds, promote goes live (admin or covered by a grant). status diagnoses why a promote/tick would fail.
- flows automations request-grant [--max-day ] [--expiry ] — the PAVED PATH for agent promotion: the server derives a standing-auth grant from the install itself (verbs from the resolved executor, contractCall block copied verbatim from the registered manifest, caps from install bounds) and opens the owner sign-off. Re-run after every manifest change; a grantee holds exactly ONE active grant and approval replaces the previous one.
- flows policy show | presets | apply | edit | finalize | preflight — org policy as code; tightenings apply instantly, loosenings hold for a second person.
- flows approvals list | release | decline — held moves. Release is maker-checker: only the ASSIGNED APPROVER, under their own login, can record the decision, and fund-moving holds need the approver's passkey step-up in the dashboard (no email OTP; the CLI only records the decision). An agent can list holds but never release its own.
- flows registry inspect — what code a CID is and what its registered manifest declared.
- flows open [automationId] — open the automation's detail workspace in the browser.
- flows executor publish | list | show | packet [--json] | approve | revoke — custom signing executors, owner-approved by exact CID; packet --json emits the review packet (exact source + capability report) machine-readable for an agent reviewer.
- flows rebalance dry-run --amount-usd --to-address [--source-asset --target-chain --target-asset --target-pool --to-chain --leash|--policy] — preview the two-leg CeFi -> DeFi plan; both legs judged, dry-run only today.
- flows payments — payment-operations build side (forms, payees, periods); publish and release are human-only dashboard acts.
- flows secrets set | list | delete — per-action encrypted secrets.
- flows assets import | report — issuer rail: import a deployed token by address, pull a signed supply report.
- Legacy (pre-pivot marketplace, kept for existing published flows): flows publish, invoke, list, logs, storage.
---
# Manifest, verb, and policy references
The permission model in one line: your action's MANIFEST declares what it may do (it can only restrict, never expand), and the org POLICY is the law it is judged against, per move, in the TEE.
Full field-level references (agent-readable markdown):
- Manifest: https://raw.githubusercontent.com/LIT-Protocol/flows/main/docs/manifest-reference.md
- Verbs: https://raw.githubusercontent.com/LIT-Protocol/flows/main/docs/verb-reference.md
- Policy: https://raw.githubusercontent.com/LIT-Protocol/flows/main/docs/policy-reference.md
## The five declarable verbs
- read — read balances & positions; never moves funds; default allow.
- trade-spot — place spot trades; allow within caps/allowlists, else hold.
- transfer-internal — move funds between your OWN venues (destination must be in the policy's selfWallets); allow within caps, else hold.
- withdraw — send to an external address; ALWAYS holds for a person and is sanctions-screened.
- contract-call — call an allowlisted contract + selector; deny unless allowlisted; native value always holds.
Egress (reaching an off-chain API host) is a capability, not a verb: declare hosts in the manifest's egress.apiHosts and permit them in the policy's allowedApiHosts. policy-edit exists as a gate verb but is NOT declarable in a manifest.
## The manifest (flows-capability-manifest-v1)
Key fields: schema (must be "flows-capability-manifest-v1"), name, form ("automation" ships code and runs itself; "standing-auth" is a bounded no-code grant for a named grantee), verbs (the complete list the action may use — anything undeclared is denied regardless of policy), notional (the dry-run pricing rule, e.g. { kind: "param", param: "notionalUsd" }), bounds (maxNotionalPerOrderUsd, maxNotionalPerDayUsd, symbolAllowlist, venueAllowlist), contractCall (required for contract-call: chainId, toAllowlist, selectorAllowlist of 0x+8-hex selectors, maxValueWei, roles), egress.apiHosts. The manifest is a permission sheet, not a proof; the gate enforces it as a ceiling. The trigger/cadence is NOT a manifest field — it is passed at install time (flows deploy --every/--cron).
## The policy (flows-policy-v1 PolicyDoc)
Key blocks: caps (per-order / per-day USD, optionally per wallet), initiatorClassAllowances (the AGENT LEASH — unattended code and agent keys cannot move funds at all until an allowance exists, and can never exceed it; a person initiating the same move is judged as a person), per-grantee standing-auth grants, allowlists (symbols, venues), allowedApiHosts (egress; fail-closed), circuits (halt switches), compliance (sanctions screening on withdrawals: sanctioned=deny always, high-risk=hold, screen-unavailable=hold), notify. Verdicts: allow, allow-and-notify, hold (a second person releases), deny (absolute). Tightening applies instantly; loosening holds for an approver or time-locks for a solo wallet. Named presets: just-me, me-plus-guardian, team-treasury, agent-on-a-leash.
---
# Supporting docs
Use the CLI to scaffold, dry-run, deploy, and inspect automations, and to run the build side of payment operations. Use the Data docs for price feeds and action inputs, Secrets for per-action credentials and OAuth connections, MCP Setup to point your coding agent at the one Flows MCP server (flows_wallet_*, flows_deploy_*, flows_payments_* tools), and Overview for product concepts. The machine-readable API is the OpenAPI JSON at /api/docs/openapi.json. Keep secrets out of source and never paste private venue keys into public docs or manifests.