openapi: 3.0.3 # AVS Aggregator REST API — public partner-facing surface. # # Source of truth for the REST API: Go handler interfaces and SDK types are # generated from this spec via `make rest-gen`. See # avs-infra/API_REST_IMPLEMENTATION_PLAN.md for the full design rationale. # # Conventions: # - All field names are camelCase (request bodies, response bodies, query # parameters, template variables). # - Resources are plural nouns; IDs in path; bodies in JSON. # - Custom actions use colon-suffix routes (Google AIP-136): # POST /workflows/{id}:pause, POST /workflows:simulate, etc. # - Errors follow RFC 7807 (application/problem+json). # - Pagination uses opaque cursors via `?before=` / `?after=` query params # and a uniform `{ data, pageInfo }` envelope. info: title: Ava Protocol AVS API version: '1.0.0' description: | Public REST API for the Ava Protocol AVS aggregator. Exposes workflow creation, execution monitoring, smart-wallet management, and related operations. Authentication is a single credential type — a JWT bearer token — obtained either via the wallet-signing flow (`POST /auth:exchange`) or out-of-band via the operator-run `create-api-key` CLI. Every request must include `Authorization: Bearer `. servers: - url: https://gateway.avaprotocol.org/api/v1 description: Production gateway - url: https://gateway-staging.avaprotocol.org/api/v1 description: Staging gateway - url: http://localhost:8080/api/v1 description: Local dev # Health check lives at root, outside /api/v1/. Documented here so the spec is # self-describing; the actual route is mounted by the server above /api/v1/. tags: - name: Health description: Liveness / readiness probes - name: Auth description: Token issuance and credential management - name: Workflows description: Workflow CRUD and lifecycle actions - name: Executions description: Workflow execution history and status - name: Wallets description: Smart-wallet derivation and operations - name: Secrets description: User/workflow/org secret storage - name: Tokens description: ERC-20 metadata lookup - name: Nodes description: Stand-alone node execution - name: Triggers description: Stand-alone trigger evaluation - name: Operators description: Connected operator status (read-only) security: - bearerAuth: [] components: securitySchemes: bearerAuth: type: http scheme: bearer bearerFormat: JWT description: | JWT bearer token. Obtained via `POST /auth:exchange` (wallet signature flow) or via the operator-run `create-api-key` CLI (long-lived, server-to-server). Send on every request as `Authorization: Bearer `. parameters: PageBefore: name: before in: query description: Cursor — return items immediately before this position (backward pagination). schema: type: string PageAfter: name: after in: query description: Cursor — return items immediately after this position (forward pagination). schema: type: string PageLimit: name: limit in: query description: Max items to return. Default 20; server-enforced ceiling applies. schema: type: integer format: int32 minimum: 1 maximum: 200 default: 20 ChainIdQuery: name: chainId in: query description: | The chain to operate on (a single value). Omit to use the aggregator default (the request's JWT `aud` chain, then the gateway default). schema: type: integer format: int64 schemas: # ------------------------------------------------------------------- # Common scalars # ------------------------------------------------------------------- ChainId: type: integer format: int64 description: | Numeric chain ID (e.g. 11155111 for Sepolia, 8453 for Base). On chain-aware trigger/node configs this is required and must be a configured chain; on query/filter params it is optional. example: 11155111 EthereumAddress: type: string pattern: '^0x[a-fA-F0-9]{40}$' description: Lowercase or checksummed hex EOA / contract address. example: '0x82F2Dd9a552a69f2ceD7Ff2D05c43aB8430158FB' Hex: type: string pattern: '^0x[a-fA-F0-9]*$' description: Arbitrary-length hex-encoded byte string. Ulid: type: string pattern: '^[0-9A-HJKMNP-TV-Z]{26}$' description: ULID identifier (26-char Crockford base32). example: '01JG2FE5MDVKBPHEG0PEYSDKAC' Timestamp: type: string format: date-time description: RFC 3339 timestamp. # ------------------------------------------------------------------- # Pagination envelope (shared by every list endpoint) # ------------------------------------------------------------------- PageInfo: type: object required: - hasNextPage - hasPreviousPage properties: hasNextPage: type: boolean hasPreviousPage: type: boolean startCursor: type: string description: Cursor for the first item in the current page; pass to `before` for the previous page. endCursor: type: string description: Cursor for the last item in the current page; pass to `after` for the next page. # ------------------------------------------------------------------- # RFC 7807 problem+json error shape # ------------------------------------------------------------------- Problem: type: object description: | RFC 7807 problem+json. Returned as `application/problem+json` on any 4xx/5xx response. `type` and `title` describe the error class; `detail` is human-readable; `instance` is a per-request identifier suitable for log correlation. required: - type - title - status properties: type: type: string format: uri description: URI identifying the problem type. example: 'https://docs.avaprotocol.org/errors/workflow-not-found' title: type: string description: Short, human-readable summary. example: 'Workflow not found' status: type: integer format: int32 description: HTTP status code (echoed for clients that surface only the body). example: 404 detail: type: string description: Human-readable explanation specific to this occurrence. example: 'No workflow with id 01JG2FE5MDVKBPHEG0PEYSDKAC for owner 0xabc...' instance: type: string description: URI / opaque ID identifying this specific occurrence (e.g., request id). example: 'req_01JG2FE5MFKTH0754RGF2DMVY7' code: type: string description: | Machine-readable error code. Stable across releases; clients can switch on this for programmatic handling. Mirrors the gRPC-era ErrorCode enum vocabulary. example: 'WORKFLOW_NOT_FOUND' # ------------------------------------------------------------------- # Health # ------------------------------------------------------------------- HealthStatus: type: object required: - status - version properties: status: type: string enum: [ok, degraded, starting] version: type: string description: | Aggregator binary version (e.g., `v3.2.0`). Always set — SDK clients use this to stamp the canonical EIP-191 auth message so the signed `Version` field reflects the gateway the user actually authenticated against. chainId: $ref: '#/components/schemas/ChainId' description: EigenLayer registration chain ID. # ------------------------------------------------------------------- # Auth # ------------------------------------------------------------------- AuthExchangeRequest: type: object required: - ownerAddress - signature - message properties: ownerAddress: $ref: '#/components/schemas/EthereumAddress' signature: $ref: '#/components/schemas/Hex' description: EIP-191 personal_sign signature of `message`. message: type: string description: | The plain-text message that was signed. Must use the canonical format with the EigenLayer registration chain ID (NOT the workflow target chain). SDKs generate this locally. AuthExchangeResponse: type: object required: - token - expiresAt properties: token: type: string description: JWT bearer token. expiresAt: $ref: '#/components/schemas/Timestamp' subject: $ref: '#/components/schemas/EthereumAddress' description: The EOA the token is bound to (echoed for clients). # ------------------------------------------------------------------- # Workflow domain — Workflow envelope + Triggers + Nodes + Edges # # A Workflow is a DAG: one Trigger fires it, Nodes are executed in # topological order following Edges, and the run is recorded as an # Execution (see executions section in a later commit). # # Trigger and Node are discriminated unions, keyed by `type`. Each # variant has its own typed `config` shape. The discriminator lets # SDKs and OpenAPI codegen produce exhaustive switch handling. # ------------------------------------------------------------------- WorkflowStatus: type: string enum: - enabled - disabled - running - completed - failed description: | Lifecycle status. `enabled` means actively monitored; `disabled` is paused. `running`, `completed`, `failed` are terminal-ish states emitted during/after execution. TriggerType: type: string enum: - manual - fixedTime - cron - block - event description: | Discriminator field for the Trigger union. Mirrors the proto `TriggerType` enum but without the `TRIGGER_TYPE_` prefix. NodeType: type: string enum: - ethTransfer - contractWrite - contractRead - graphqlQuery - restApi - customCode - branch - filter - loop - balance - await description: | Discriminator field for the Node union. Mirrors the proto `NodeType` enum but without the `NODE_TYPE_` prefix. Lang: type: string enum: - javascript - json - graphql - handlebars description: | Language/format of an inline payload (e.g., custom code source, manual trigger data). Mirrors the proto `Lang` enum minus the `LANG_` prefix. Wire values are lowercase. # ---- Trigger configs ---- ManualTriggerConfig: type: object description: User-initiated trigger; no chain context. required: [lang] properties: data: description: Arbitrary structured payload returned in the trigger output. # google.protobuf.Value equivalent; OpenAPI allows any JSON. additionalProperties: true headers: type: object additionalProperties: { type: string } description: HTTP headers (for webhook testing). pathParams: type: object additionalProperties: { type: string } description: Path parameters (for webhook testing). lang: $ref: '#/components/schemas/Lang' FixedTimeTriggerConfig: type: object description: Fires at one or more absolute Unix-epoch milliseconds. required: [epochs] properties: epochs: type: array items: type: integer format: int64 minItems: 1 CronTriggerConfig: type: object description: Fires on one or more cron schedules. required: [schedules] properties: schedules: type: array items: type: string description: 'Standard cron expression (e.g., `* * * * *`).' minItems: 1 timezone: type: string description: 'IANA timezone (e.g., `UTC`, `America/New_York`). Default UTC.' BlockTriggerConfig: type: object description: Fires every N blocks on the target chain. required: [interval, chainId] properties: interval: type: integer format: int64 minimum: 1 description: Fire every N blocks. chainId: $ref: '#/components/schemas/ChainId' description: Chain to watch blocks on. Required — a workflow carries no chain to inherit. EventTriggerQuery: type: object description: Single ethereum.FilterQuery — one subscription per query. properties: addresses: type: array items: { $ref: '#/components/schemas/EthereumAddress' } description: Contract addresses to filter events from. Empty matches any contract. topics: type: array items: type: string nullable: true description: | Topic filters (`topics[0]` is the event signature, `topics[1..]` are indexed parameter values). `null` means wildcard at that position. maxEventsPerBlock: type: integer format: int32 description: Safety ceiling per query per block. Exceeded → task cancelled. contractAbi: type: array items: additionalProperties: true description: Contract ABI entries (JSON form) for event decoding. conditions: type: array items: { $ref: '#/components/schemas/EventCondition' } description: Filters applied to decoded event data. methodCalls: type: array items: { $ref: '#/components/schemas/EventMethodCall' } description: Method calls used to enrich decoded event data (e.g., `decimals`). EventCondition: type: object description: Predicate evaluated against decoded event data. required: [fieldName, operator, value] properties: fieldName: { type: string } operator: type: string enum: [eq, ne, gt, gte, lt, lte, contains] value: type: string description: | Value to compare against, encoded as a string. The operator parses it according to `fieldType` (e.g. `int256` / `uint256` → big.Int, `address` → checksummed hex, `bool` → "true"/"false"). Matches the proto `EventCondition.value`, which is also a string. fieldType: { type: string } EventMethodCall: type: object required: [methodName] properties: methodName: { type: string } callData: $ref: '#/components/schemas/Hex' applyToFields: type: array items: { type: string } methodParams: type: array items: type: string description: Handlebars template; resolves against decoded event data. EventTriggerConfig: type: object description: Fires when matching on-chain events are observed. required: [queries, chainId] properties: queries: type: array items: { $ref: '#/components/schemas/EventTriggerQuery' } minItems: 1 cooldownSeconds: type: integer format: int32 minimum: 0 description: | Seconds to wait after a fire before allowing the same task to trigger again. Default 300. 0 disables cooldown. chainId: $ref: '#/components/schemas/ChainId' description: Chain to watch events on. Required — a workflow carries no chain to inherit. # ---- Trigger union ---- Trigger: type: object required: [type, name, config] properties: id: { type: string } name: { type: string } type: { $ref: '#/components/schemas/TriggerType' } discriminator: propertyName: type mapping: manual: '#/components/schemas/ManualTrigger' fixedTime: '#/components/schemas/FixedTimeTrigger' cron: '#/components/schemas/CronTrigger' block: '#/components/schemas/BlockTrigger' event: '#/components/schemas/EventTrigger' oneOf: - $ref: '#/components/schemas/ManualTrigger' - $ref: '#/components/schemas/FixedTimeTrigger' - $ref: '#/components/schemas/CronTrigger' - $ref: '#/components/schemas/BlockTrigger' - $ref: '#/components/schemas/EventTrigger' ManualTrigger: allOf: - type: object properties: type: type: string enum: [manual] config: { $ref: '#/components/schemas/ManualTriggerConfig' } FixedTimeTrigger: allOf: - type: object properties: type: type: string enum: [fixedTime] config: { $ref: '#/components/schemas/FixedTimeTriggerConfig' } CronTrigger: allOf: - type: object properties: type: type: string enum: [cron] config: { $ref: '#/components/schemas/CronTriggerConfig' } BlockTrigger: allOf: - type: object properties: type: type: string enum: [block] config: { $ref: '#/components/schemas/BlockTriggerConfig' } EventTrigger: allOf: - type: object properties: type: type: string enum: [event] config: { $ref: '#/components/schemas/EventTriggerConfig' } # ---- Node configs ---- MethodCall: type: object description: One call to a contract method (used by ContractWrite + ContractRead). required: [methodName] properties: methodName: { type: string } callData: $ref: '#/components/schemas/Hex' contractAddress: $ref: '#/components/schemas/EthereumAddress' description: >- Per-call target contract. When set, this call is sent to this address instead of the node-level contractAddress, so a single ContractWrite node can express a heterogeneous atomic batch (e.g. approve on the token + swap on the router). Absent = the node-level contract. applyToFields: type: array items: { type: string } methodParams: type: array items: type: string description: Handlebars template for method args. ETHTransferNodeConfig: type: object required: [destination, amount, chainId] properties: destination: { $ref: '#/components/schemas/EthereumAddress' } amount: type: string description: Amount in wei (decimal string for big-int safety). Special value `max` withdraws the entire balance. chainId: $ref: '#/components/schemas/ChainId' description: Chain to execute on. Required — a workflow carries no chain to inherit. ContractWriteNodeConfig: type: object required: [contractAddress, chainId] properties: contractAddress: { $ref: '#/components/schemas/EthereumAddress' } callData: { $ref: '#/components/schemas/Hex' } contractAbi: type: array items: additionalProperties: true methodCalls: type: array items: { $ref: '#/components/schemas/MethodCall' } isSimulated: type: boolean description: When true, use Tenderly simulation instead of sending a real UserOp. value: type: string description: ETH value to send with the call (wei, decimal string). gasLimit: type: string description: Custom gas limit (decimal string). chainId: $ref: '#/components/schemas/ChainId' description: Chain to execute on. Required — a workflow carries no chain to inherit. ContractReadNodeConfig: type: object required: [contractAddress, chainId] properties: contractAddress: { $ref: '#/components/schemas/EthereumAddress' } contractAbi: type: array items: additionalProperties: true methodCalls: type: array items: { $ref: '#/components/schemas/MethodCall' } chainId: $ref: '#/components/schemas/ChainId' description: Chain to read from. Required — a workflow carries no chain to inherit. GraphQLQueryNodeConfig: type: object required: [url, query] properties: url: type: string format: uri query: { type: string } variables: type: object additionalProperties: { type: string } RestAPINodeConfig: type: object required: [url, method] properties: url: type: string format: uri method: type: string enum: [GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS] headers: type: object additionalProperties: { type: string } body: { type: string } options: type: object description: | Generic options bag for backend features on terminal RestAPI nodes. `summarize: true` opts a SendGrid /v3/mail/send or Telegram /sendMessage node into the aggregator's context-memory summarizer, which composes a subject + HTML body from the workflow's execution context and injects them into the outgoing request. Without this field set, the aggregator falls back to the deterministic summarizer (no LLM polish). properties: summarize: type: boolean description: | When true on a terminal SendGrid or Telegram node, ComposeSummarySmart runs at execution time and fills in the empty content.value / text slot with an AI-generated body. No-op on non-notification URLs. additionalProperties: true CustomCodeNodeConfig: type: object required: [lang, source] properties: lang: { $ref: '#/components/schemas/Lang' } source: { type: string } BranchCondition: type: object required: [id, expression] properties: id: { type: string } type: type: string enum: [if, elseIf, else] expression: type: string description: JavaScript-evaluated boolean expression. BranchNodeConfig: type: object required: [conditions] properties: conditions: type: array items: { $ref: '#/components/schemas/BranchCondition' } minItems: 1 FilterNodeConfig: type: object required: [inputVariable, expression] properties: inputVariable: type: string description: Template path for the source array (e.g., `{{custom_code1.data}}`). expression: type: string description: JavaScript predicate evaluated per item. LoopNodeConfig: type: object description: | Iterates over an input array, running an inner Node per item. The runner node is one of the chain-aware or chain-agnostic node types; a chain-aware runner must specify its own required `chainId` (there is no inheritance from the loop or workflow). required: [inputVariable, runner] properties: inputVariable: type: string description: Template path for the iterable (e.g., `{{settings.addressList}}`). iterVar: type: string default: 'value' description: Name of the per-iteration variable (defaults to `value`). runner: $ref: '#/components/schemas/Node' BalanceNodeConfig: type: object required: [address, chain] properties: address: { $ref: '#/components/schemas/EthereumAddress' } chain: type: string description: 'Chain name or numeric ID (e.g., `ethereum`, `base`, `1`, `8453`).' includeSpam: { type: boolean } includeZeroBalances: { type: boolean } minUsdValueCents: type: integer format: int64 description: Filter out tokens with USD value below this many cents. tokenAddresses: type: array items: { $ref: '#/components/schemas/EthereumAddress' } description: Restrict to these tokens. Empty = fetch all. AwaitNodeConfig: type: object description: | Pauses the workflow until a wake arrives (durable execution). Two mutually exclusive flavors: the external-signal flavor (human approval — set `channel`, e.g. a Telegram approve/reject), or the chain-event flavor (cross-chain — set `chainEvent` to pause until an operator observes that on-chain event, e.g. a bridge arrival on another chain). Exactly one flavor must be configured. properties: channel: type: string enum: [telegram, api] description: 'External-signal flavor — signal channel: `telegram` or `api`.' approvers: type: array items: { type: string } description: >- External-signal flavor — authorized approver identities. Empty = the workflow owner. NOTE (v1): not yet enforced — the signal endpoint authorizes by workflow ownership only, so the owner can always approve regardless of this list. Delegated-approver enforcement (Telegram binding) is a follow-up; do not rely on this field for security yet. prompt: type: string description: External-signal flavor — message shown to the approver. chainEvent: allOf: [{ $ref: '#/components/schemas/EventTriggerConfig' }] description: | Chain-event flavor — the on-chain event to wait for (a mid-workflow EventTrigger). An operator covering `chainEvent.chainId` watches it and resumes the execution when it fires. Mutually exclusive with `channel`. timeoutSeconds: type: integer format: int64 description: Safety bound; 0 = server default (the wait is never unbounded). SignalExecutionRequest: type: object required: [decision] properties: decision: type: string enum: [approve, reject] description: The approver's decision. payload: type: object additionalProperties: true description: Optional structured data delivered as the await step's output. # ---- Node union ---- Node: type: object required: [type, id, config] properties: id: { type: string } name: { type: string } type: { $ref: '#/components/schemas/NodeType' } discriminator: propertyName: type mapping: ethTransfer: '#/components/schemas/ETHTransferNode' contractWrite: '#/components/schemas/ContractWriteNode' contractRead: '#/components/schemas/ContractReadNode' graphqlQuery: '#/components/schemas/GraphQLQueryNode' restApi: '#/components/schemas/RestAPINode' customCode: '#/components/schemas/CustomCodeNode' branch: '#/components/schemas/BranchNode' filter: '#/components/schemas/FilterNode' loop: '#/components/schemas/LoopNode' balance: '#/components/schemas/BalanceNode' await: '#/components/schemas/AwaitNode' oneOf: - $ref: '#/components/schemas/ETHTransferNode' - $ref: '#/components/schemas/ContractWriteNode' - $ref: '#/components/schemas/ContractReadNode' - $ref: '#/components/schemas/GraphQLQueryNode' - $ref: '#/components/schemas/RestAPINode' - $ref: '#/components/schemas/CustomCodeNode' - $ref: '#/components/schemas/BranchNode' - $ref: '#/components/schemas/FilterNode' - $ref: '#/components/schemas/LoopNode' - $ref: '#/components/schemas/BalanceNode' - $ref: '#/components/schemas/AwaitNode' ETHTransferNode: allOf: - type: object properties: type: { type: string, enum: [ethTransfer] } config: { $ref: '#/components/schemas/ETHTransferNodeConfig' } ContractWriteNode: allOf: - type: object properties: type: { type: string, enum: [contractWrite] } config: { $ref: '#/components/schemas/ContractWriteNodeConfig' } ContractReadNode: allOf: - type: object properties: type: { type: string, enum: [contractRead] } config: { $ref: '#/components/schemas/ContractReadNodeConfig' } GraphQLQueryNode: allOf: - type: object properties: type: { type: string, enum: [graphqlQuery] } config: { $ref: '#/components/schemas/GraphQLQueryNodeConfig' } RestAPINode: allOf: - type: object properties: type: { type: string, enum: [restApi] } config: { $ref: '#/components/schemas/RestAPINodeConfig' } CustomCodeNode: allOf: - type: object properties: type: { type: string, enum: [customCode] } config: { $ref: '#/components/schemas/CustomCodeNodeConfig' } BranchNode: allOf: - type: object properties: type: { type: string, enum: [branch] } config: { $ref: '#/components/schemas/BranchNodeConfig' } FilterNode: allOf: - type: object properties: type: { type: string, enum: [filter] } config: { $ref: '#/components/schemas/FilterNodeConfig' } LoopNode: allOf: - type: object properties: type: { type: string, enum: [loop] } config: { $ref: '#/components/schemas/LoopNodeConfig' } BalanceNode: allOf: - type: object properties: type: { type: string, enum: [balance] } config: { $ref: '#/components/schemas/BalanceNodeConfig' } AwaitNode: allOf: - type: object properties: type: { type: string, enum: [await] } config: { $ref: '#/components/schemas/AwaitNodeConfig' } # ---- Workflow envelope ---- Edge: type: object required: [id, source, target] properties: id: { type: string } source: type: string description: Node or trigger ID where this edge starts. target: type: string description: Node ID where this edge ends. InputVariables: type: object additionalProperties: true description: | Free-form key-value bag of values used to resolve `{{variable.path}}` template references inside trigger and node configs. Conventional well-known keys: `settings.runner` (smart wallet address), `settings.chainId` (chain id). camelCase keys; back-compat support for snake_case keys exists during the migration window. Workflow: type: object required: [id, owner, smartWalletAddress, trigger, nodes, status] properties: id: { $ref: '#/components/schemas/Ulid' } name: { type: string } owner: { $ref: '#/components/schemas/EthereumAddress' } smartWalletAddress: { $ref: '#/components/schemas/EthereumAddress' } trigger: { $ref: '#/components/schemas/Trigger' } nodes: type: array items: { $ref: '#/components/schemas/Node' } edges: type: array items: { $ref: '#/components/schemas/Edge' } inputVariables: { $ref: '#/components/schemas/InputVariables' } status: { $ref: '#/components/schemas/WorkflowStatus' } startAt: type: integer format: int64 description: Unix-epoch milliseconds — workflow is inert before this time. expiredAt: type: integer format: int64 description: Unix-epoch milliseconds — workflow is inert after this time. maxExecution: type: integer format: int64 description: >- Cap on how many times this workflow may execute. The workflow reaches status `completed` once `executionCount` hits this value. Present and finite on every workflow created since the server began assigning a default — a create request that omits the field takes that default, and one that sends 0 or a negative is rejected, so "run forever" is not expressible. Absent on workflows created before that change and stored uncapped; those keep running without a limit, and `remainingExecutions` is likewise absent for them. executionCount: type: integer format: int64 description: How many times this workflow has executed so far. remainingExecutions: type: integer format: int64 description: >- Runs left before the workflow completes — `maxExecution` minus `executionCount`, floored at 0. Derived server-side so clients do not have to reproduce the arithmetic (and so an absent `executionCount` on a never-run workflow cannot be misread). Reported as 0 rather than omitted once the budget is spent. Absent only on legacy uncapped workflows, where no finite number exists. completionReason: type: string enum: - TASK_COMPLETION_REASON_UNSPECIFIED - TASK_COMPLETION_REASON_MAX_EXECUTIONS_REACHED - TASK_COMPLETION_REASON_EXPIRED description: >- Why the workflow reached a terminal state. An exhausted execution budget and a passed expiry both produce status `completed`, so the status alone cannot distinguish them. Absent or UNSPECIFIED while the workflow is still runnable, and on workflows that terminated before this field existed. Cancellation is not represented — cancelling deletes the workflow rather than leaving a record. createdAt: type: integer format: int64 description: Unix-epoch milliseconds — when the workflow was first created. completedAt: type: integer format: int64 description: Unix-epoch milliseconds — when the workflow reached a terminal state. # ---- Workflow request/response bodies ---- CreateWorkflowRequest: type: object required: [smartWalletAddress, trigger, nodes] properties: name: { type: string } smartWalletAddress: { $ref: '#/components/schemas/EthereumAddress' } trigger: { $ref: '#/components/schemas/Trigger' } nodes: type: array items: { $ref: '#/components/schemas/Node' } edges: type: array items: { $ref: '#/components/schemas/Edge' } inputVariables: { $ref: '#/components/schemas/InputVariables' } startAt: { type: integer, format: int64 } expiredAt: { type: integer, format: int64 } maxExecution: type: integer format: int64 minimum: 1 description: >- Optional cap on total executions. Omit the field to take the server default. Sending 0 (or a negative) is rejected rather than treated as unlimited: unlimited execution is not offered, because every run spends metered provider quota. WorkflowList: type: object required: [data, pageInfo] properties: data: type: array items: { $ref: '#/components/schemas/Workflow' } pageInfo: { $ref: '#/components/schemas/PageInfo' } WorkflowCount: type: object required: [total] properties: total: type: integer format: int64 TriggerWorkflowRequest: type: object required: [triggerType] properties: triggerType: { $ref: '#/components/schemas/TriggerType' } triggerOutput: description: | Type-specific output payload (BlockTrigger.Output, EventTrigger.Output, etc.) that simulates what the operator would have observed. Defined alongside execution schemas. additionalProperties: true triggerInput: { $ref: '#/components/schemas/InputVariables' } isBlocking: type: boolean description: When true, wait for the execution to complete and return its result. TriggerWorkflowResponse: type: object required: [executionId, status] properties: executionId: { $ref: '#/components/schemas/Ulid' } # status mirrors ExecutionStatus — the response describes the # execution that the trigger kicked off. `pending` is returned # when isBlocking=false (execution hasn't reached terminal # state yet); `success`/`failed`/`error` are returned when # isBlocking=true. status: { $ref: '#/components/schemas/ExecutionStatus' } startAt: { type: integer, format: int64 } endAt: { type: integer, format: int64 } error: { type: string } SimulateWorkflowRequest: type: object required: [trigger, nodes, inputVariables] properties: chainId: { $ref: '#/components/schemas/ChainId' } trigger: { $ref: '#/components/schemas/Trigger' } nodes: type: array items: { $ref: '#/components/schemas/Node' } edges: type: array items: { $ref: '#/components/schemas/Edge' } inputVariables: { $ref: '#/components/schemas/InputVariables' } EstimateFeesRequest: type: object required: [trigger, nodes, createdAt, expireAt, maxExecution] properties: chainId: { $ref: '#/components/schemas/ChainId' } trigger: { $ref: '#/components/schemas/Trigger' } nodes: type: array items: { $ref: '#/components/schemas/Node' } edges: type: array items: { $ref: '#/components/schemas/Edge' } runner: $ref: '#/components/schemas/EthereumAddress' description: Smart wallet address used for gas estimation (overrides settings.runner). createdAt: { type: integer, format: int64 } expireAt: { type: integer, format: int64 } maxExecution: { type: integer, format: int64 } inputVariables: { $ref: '#/components/schemas/InputVariables' } EstimateFeesResponse: type: object required: [chainId, executionFee, cogs, valueFee] properties: chainId: { $ref: '#/components/schemas/ChainId' } nativeToken: { $ref: '#/components/schemas/NativeToken' } executionFee: $ref: '#/components/schemas/Fee' description: Flat per-execution platform fee (typically USD). cogs: type: array items: { $ref: '#/components/schemas/NodeCOGS' } description: Per-node operational costs (gas, external API). valueFee: $ref: '#/components/schemas/ValueFee' description: Workflow-level value-capture fee (PERCENTAGE). discounts: type: array items: { $ref: '#/components/schemas/FeeDiscount' } pricingModel: type: string description: 'Pricing model label (e.g., `tiered_value_capture_v1`).' # ------------------------------------------------------------------- # Execution domain # ------------------------------------------------------------------- ExecutionStatus: type: string enum: - pending - waiting - success - failed - error description: | Outcome of an execution. `pending` is in-flight; `waiting` is suspended mid-workflow at an `await` node, durably parked until a signal arrives (a human approve/reject or an operator-observed chain event) or the wait times out — non-terminal, like `pending`, but distinguishable so a client can show "awaiting approval"; `success` is full success; `failed` is logical failure (e.g., a node returned an error, or a wait timed out); `error` is a system / infrastructure failure (e.g., RPC unreachable). Fee: type: object required: [amount, unit] properties: amount: type: string description: Decimal numeric value, encoded as a string for big-int safety. unit: type: string enum: [USD, WEI, PERCENTAGE] NativeToken: type: object required: [symbol, decimals] properties: symbol: { type: string, example: ETH } decimals: type: integer format: int32 NodeCOGS: type: object required: [nodeId, costType, fee] properties: nodeId: { type: string } costType: type: string enum: [gas, externalApi, walletCreation] fee: { $ref: '#/components/schemas/Fee' } gasUnits: type: string description: Gas units (for `gas` cost type only). ExecutionTier: type: string enum: [unspecified, tier1, tier2, tier3] description: Pricing group for value-capture fees. ValueFee: type: object required: [fee, tier] properties: fee: { $ref: '#/components/schemas/Fee' } tier: { $ref: '#/components/schemas/ExecutionTier' } valueBase: type: string description: 'What the percentage applies to (e.g., `input_token_value`).' classificationMethod: type: string enum: [ruleBased, llm] confidence: type: number format: float minimum: 0 maximum: 1 reason: { type: string } FeeDiscount: type: object properties: discountType: type: string enum: [newUser, volume, promotional, betaProgram] discountName: { type: string } discount: { $ref: '#/components/schemas/Fee' } expiryDate: { $ref: '#/components/schemas/Timestamp' } terms: { type: string } ExecutionStep: type: object description: | One node (or trigger) invocation within an Execution. The `type` field is the trigger or node type name (e.g., `cron`, `contractWrite`); the corresponding output is in `output`. required: [id, type, success] properties: id: { type: string } type: { type: string } name: { type: string } success: { type: boolean } error: { type: string } errorCode: { type: string } log: { type: string } inputs: type: array items: { type: string } config: additionalProperties: true description: Config of the trigger or node at execution time. metadata: additionalProperties: true description: Optional structured metadata for testing/debugging. executionContext: additionalProperties: true description: Runtime flags and extra info (e.g., `isSimulated`). output: additionalProperties: true description: Type-specific output payload (trigger or node output). gasUsed: type: string description: Gas units consumed (decimal string). Empty if unavailable. gasPrice: type: string description: Gas price in wei per unit (decimal string). totalGasCost: type: string description: gasUsed × gasPrice in wei (decimal string). startAt: { type: integer, format: int64 } endAt: { type: integer, format: int64 } Execution: type: object required: [id, workflowId, status, startAt] properties: id: { $ref: '#/components/schemas/Ulid' } workflowId: { $ref: '#/components/schemas/Ulid' } chainId: { $ref: '#/components/schemas/ChainId' } index: type: integer format: int64 description: 0-based run counter within the workflow. startAt: { type: integer, format: int64 } endAt: { type: integer, format: int64 } status: { $ref: '#/components/schemas/ExecutionStatus' } error: { type: string } steps: type: array items: { $ref: '#/components/schemas/ExecutionStep' } executionFee: $ref: '#/components/schemas/Fee' description: Flat platform fee charged for this execution. cogs: type: array items: { $ref: '#/components/schemas/NodeCOGS' } description: Per-node actual costs (gas, external API). valueFee: $ref: '#/components/schemas/ValueFee' description: Value-capture fee charged (post-paid). ExecutionList: type: object required: [data, pageInfo] properties: data: type: array items: { $ref: '#/components/schemas/Execution' } pageInfo: { $ref: '#/components/schemas/PageInfo' } ExecutionStatusSummary: type: object description: Lightweight execution status (no steps payload). required: [id, status] properties: id: { $ref: '#/components/schemas/Ulid' } workflowId: { $ref: '#/components/schemas/Ulid' } status: { $ref: '#/components/schemas/ExecutionStatus' } startAt: { type: integer, format: int64 } endAt: { type: integer, format: int64 } error: { type: string } ExecutionCount: type: object required: [total] properties: total: type: integer format: int64 ExecutionStats: type: object required: [total, succeeded, failed] properties: total: { type: integer, format: int64 } succeeded: { type: integer, format: int64 } failed: { type: integer, format: int64 } pending: { type: integer, format: int64 } avgExecutionTime: type: number format: double description: Average execution time in milliseconds. # ------------------------------------------------------------------- # Wallet domain # ------------------------------------------------------------------- Wallet: type: object required: [address, salt] properties: address: { $ref: '#/components/schemas/EthereumAddress' } salt: type: string description: Salt used in CREATE2 derivation (decimal string). factoryAddress: $ref: '#/components/schemas/EthereumAddress' description: Factory contract used to derive this address. isHidden: { type: boolean } totalWorkflowCount: { type: integer, format: int64 } enabledWorkflowCount: { type: integer, format: int64 } completedWorkflowCount: { type: integer, format: int64 } failedWorkflowCount: { type: integer, format: int64 } disabledWorkflowCount: { type: integer, format: int64 } WalletList: type: object required: [data] properties: data: type: array items: { $ref: '#/components/schemas/Wallet' } CreateWalletRequest: type: object required: [salt] properties: salt: type: string description: Salt as a decimal string (big-int). Determines the derived address together with the factory. factoryAddress: $ref: '#/components/schemas/EthereumAddress' description: Optional factory override. Defaults to the aggregator's configured factory. chainId: $ref: '#/components/schemas/ChainId' description: | Chain to derive the wallet on. Optional override — when omitted, the gateway defaults to the JWT `aud` chain (i.e. the chain the caller signed the auth message for). Set this when minting a wallet for a chain different from the one the JWT was issued against; the JWT proves EOA ownership, which is chain-independent. UpdateWalletRequest: type: object properties: isHidden: { type: boolean } WithdrawRequest: type: object required: [recipientAddress, amount, token] properties: recipientAddress: { $ref: '#/components/schemas/EthereumAddress' } amount: type: string description: Amount in wei (decimal string) or `max` for the full balance. token: type: string description: '`ETH` for native token, or an ERC-20 contract address.' chainId: $ref: '#/components/schemas/ChainId' description: Chain to execute the withdrawal on. 0 = aggregator default. WithdrawResponse: type: object required: [status] properties: status: type: string # `confirmed` is emitted when the bundler returned a receipt # synchronously. `pending` means the UserOp was accepted but # the receipt hasn't arrived yet. `failed` is a send-time # error (the bundler rejected the op or it reverted before # inclusion). enum: [pending, confirmed, failed] message: { type: string } userOpHash: { $ref: '#/components/schemas/Hex' } transactionHash: { $ref: '#/components/schemas/Hex' } submittedAt: { type: integer, format: int64 } smartWalletAddress: { $ref: '#/components/schemas/EthereumAddress' } recipientAddress: { $ref: '#/components/schemas/EthereumAddress' } amount: { type: string } token: { type: string } NonceResponse: type: object required: [nonce] properties: nonce: type: string description: Smart wallet nonce as a decimal string. # ------------------------------------------------------------------- # Secret domain # ------------------------------------------------------------------- Secret: type: object description: | Stored secret. The `value` is write-only — list/get responses return metadata only, never the secret value. required: [name, scope] properties: name: { type: string } scope: type: string enum: [user, workflow, org] description: | Scope determines visibility — `user` = available to all the user's workflows; `workflow` = scoped to one workflow id; `org` = scoped to an org id. workflowId: { $ref: '#/components/schemas/Ulid' } orgId: { type: string } createdAt: { type: integer, format: int64 } updatedAt: { type: integer, format: int64 } SecretList: type: object required: [data, pageInfo] properties: data: type: array items: { $ref: '#/components/schemas/Secret' } pageInfo: { $ref: '#/components/schemas/PageInfo' } PutSecretRequest: type: object required: [value] properties: value: type: string description: Secret value (write-only, never echoed back). workflowId: { $ref: '#/components/schemas/Ulid' } orgId: { type: string } # ------------------------------------------------------------------- # Token domain # ------------------------------------------------------------------- TokenMetadataResponse: type: object required: [found] properties: found: { type: boolean } address: { $ref: '#/components/schemas/EthereumAddress' } chainId: { $ref: '#/components/schemas/ChainId' } name: { type: string } symbol: { type: string } decimals: { type: integer, format: int32 } source: type: string enum: [whitelist, rpc, cache] # ------------------------------------------------------------------- # Standalone node/trigger execution (for SDK testing flows) # ------------------------------------------------------------------- RunNodeRequest: type: object required: [node] properties: node: { $ref: '#/components/schemas/Node' } inputVariables: { $ref: '#/components/schemas/InputVariables' } chainId: { $ref: '#/components/schemas/ChainId' } erc20Overrides: type: array description: > Optional ERC20 balance/allowance state overrides applied only during this isolated node simulation. Lets callers seed token balances and approvals so contract-write simulations (e.g. Uniswap swaps) don't revert with "transfer amount exceeds allowance/balance" before the approval/funding transactions have been run. Simulation-only: a real-execution request (isSimulated=false) that sets these is rejected with an error, never silently ignored. items: { $ref: '#/components/schemas/ERC20StateOverride' } ERC20StateOverride: type: object required: [tokenAddress, ownerAddress] description: > Seeds a token's balanceOf / allowance storage slots for a single simulation. balanceOf[owner] lives at keccak256(abi.encode(owner, balanceSlot)); allowance[owner][spender] at keccak256(abi.encode(spender, keccak256(abi.encode(owner, allowanceSlot)))). properties: tokenAddress: { $ref: '#/components/schemas/EthereumAddress' } ownerAddress: { $ref: '#/components/schemas/EthereumAddress' } spenderAddress: { $ref: '#/components/schemas/EthereumAddress' } balance: type: string description: Balance override (hex 0x… or decimal string). allowance: type: string description: Allowance override (hex 0x… or decimal string). balanceSlot: type: integer format: int64 minimum: 0 description: 'Storage slot for the balanceOf mapping. Required when balance is set; ERC20 storage layout varies per token (OpenZeppelin 0, USDC FiatToken 9).' allowanceSlot: type: integer format: int64 minimum: 0 description: 'Storage slot for the allowance mapping. Required when allowance is set; ERC20 storage layout varies per token (OpenZeppelin 1, USDC FiatToken 10).' RunNodeResponse: type: object required: [success] properties: success: { type: boolean } error: { type: string } errorCode: { type: string } output: additionalProperties: true metadata: additionalProperties: true executionContext: additionalProperties: true RunTriggerRequest: type: object required: [trigger] properties: trigger: { $ref: '#/components/schemas/Trigger' } triggerInput: { $ref: '#/components/schemas/InputVariables' } RunTriggerResponse: type: object required: [success] properties: success: { type: boolean } error: { type: string } errorCode: { type: string } output: additionalProperties: true metadata: additionalProperties: true # ------------------------------------------------------------------- # Operators (read-only monitoring) # ------------------------------------------------------------------- OperatorCapabilities: type: object properties: eventMonitoring: { type: boolean } blockMonitoring: { type: boolean } timeMonitoring: { type: boolean } OperatorInfo: type: object required: [address, supportedChainIds] properties: address: { $ref: '#/components/schemas/EthereumAddress' } supportedChainIds: type: array items: { $ref: '#/components/schemas/ChainId' } capabilities: { $ref: '#/components/schemas/OperatorCapabilities' } version: { type: string } lastSeen: { $ref: '#/components/schemas/Timestamp' } blockNumber: { type: integer, format: int64 } eventCount: { type: integer, format: int64 } OperatorList: type: object required: [data] properties: data: type: array items: { $ref: '#/components/schemas/OperatorInfo' } # ------------------------------------------------------------------- # Session policies # ------------------------------------------------------------------- AllowedAction: type: object required: [target, selectors] properties: target: $ref: '#/components/schemas/EthereumAddress' selectors: type: array minItems: 1 items: type: string pattern: '^0x[a-fA-F0-9]{8}$' description: 4-byte function selectors permitted on the target. description: One contract the agent may call, scoped to selectors. Erc20SpendCap: type: object required: [token, amount] properties: token: $ref: '#/components/schemas/EthereumAddress' amount: type: string pattern: '^[0-9]+$' description: Total cap in the token's smallest unit (decimal string, no reset). example: '500000000' description: | Cumulative ERC-20 spend cap, enforced on-chain at execution. The token must appear as an `allowedActions` target. PreparePolicyRequest: type: object required: [chainId, agentLabel, allowedActions, erc20SpendCap, expiresInSeconds] properties: chainId: { $ref: '#/components/schemas/ChainId' } agentLabel: type: string minLength: 1 maxLength: 120 justification: type: string maxLength: 500 allowedActions: type: array minItems: 1 items: { $ref: '#/components/schemas/AllowedAction' } erc20SpendCap: { $ref: '#/components/schemas/Erc20SpendCap' } expiresInSeconds: type: integer format: int64 minimum: 60 description: Grant lifetime, relative (skew-proof). Becomes an absolute validUntil. PreparedPolicy: type: object required: [policyId, chainId, entityId, sessionSigner, deadline, validUntil, digest, typedData] properties: policyId: { $ref: '#/components/schemas/Ulid' } chainId: { $ref: '#/components/schemas/ChainId' } entityId: type: integer format: int64 description: The validation entity allocated for this grant (provisional until submit). sessionSigner: $ref: '#/components/schemas/EthereumAddress' deadline: type: integer format: int64 description: Unix seconds; bounds signing → first use, NOT the grant lifetime. validUntil: type: integer format: int64 description: Absolute grant expiry, unix milliseconds. Echo verbatim to submit. digest: type: string pattern: '^0x[a-fA-F0-9]{64}$' description: The EIP-712 hash the typed data produces, for client-side verification. typedData: type: object additionalProperties: true description: The exact eth_signTypedData_v4 payload for the owner's wallet. SubmitPolicyRequest: type: object required: [chainId, policyId, entityId, deadline, validUntil, agentLabel, allowedActions, erc20SpendCap, signature] properties: chainId: { $ref: '#/components/schemas/ChainId' } policyId: { $ref: '#/components/schemas/Ulid' } entityId: type: integer format: int64 deadline: type: integer format: int64 validUntil: type: integer format: int64 description: The ABSOLUTE expiry from prepare. It is baked into the signed calldata; recomputing it would change the digest. agentLabel: type: string minLength: 1 maxLength: 120 justification: type: string maxLength: 500 allowedActions: type: array minItems: 1 items: { $ref: '#/components/schemas/AllowedAction' } erc20SpendCap: { $ref: '#/components/schemas/Erc20SpendCap' } signature: type: string pattern: '^0x[a-fA-F0-9]{130}$' description: The owner's 65-byte signature over the prepared digest. SessionPolicy: type: object required: [id, runner, chainId, status, entityId, sessionSigner, agentLabel, validUntil, createdAt] properties: id: { $ref: '#/components/schemas/Ulid' } runner: { $ref: '#/components/schemas/EthereumAddress' } chainId: { $ref: '#/components/schemas/ChainId' } status: type: string enum: [pending, active, revoked] description: | pending = signed and stored, install not yet on-chain (revocable for free). active = install applied. revoked = grants nothing. entityId: type: integer format: int64 sessionSigner: $ref: '#/components/schemas/EthereumAddress' agentLabel: { type: string } justification: { type: string } allowedActions: type: array items: { $ref: '#/components/schemas/AllowedAction' } erc20SpendCap: { $ref: '#/components/schemas/Erc20SpendCap' } validUntil: type: integer format: int64 description: Unix milliseconds. createdAt: type: integer format: int64 description: Unix milliseconds. SessionPolicyList: type: object required: [items] properties: items: type: array items: { $ref: '#/components/schemas/SessionPolicy' } RevokePolicyResponse: type: object required: [status, onChainCleanupRequired] properties: status: type: string enum: [deleted, revoked] description: deleted = was pending, nothing was ever installed. revoked = retained for audit. onChainCleanupRequired: type: boolean description: | True when the grant's validation is still installed on the account and needs the owner's uninstallValidation to clear. responses: BadRequest: description: Request validation failed. content: application/problem+json: schema: $ref: '#/components/schemas/Problem' Unauthorized: description: Missing or invalid bearer token. content: application/problem+json: schema: $ref: '#/components/schemas/Problem' Forbidden: description: Authenticated but not permitted. content: application/problem+json: schema: $ref: '#/components/schemas/Problem' NotFound: description: Resource not found. content: application/problem+json: schema: $ref: '#/components/schemas/Problem' RateLimited: description: | Rate limit exceeded. Inspect `X-RateLimit-Reset` for the next allowed request time and `Retry-After` for the seconds to wait. headers: X-RateLimit-Limit: schema: type: integer X-RateLimit-Remaining: schema: type: integer X-RateLimit-Reset: schema: type: integer description: Unix timestamp when the bucket refills. Retry-After: schema: type: integer description: Seconds to wait before retrying. content: application/problem+json: schema: $ref: '#/components/schemas/Problem' ServerError: description: Unhandled server error. content: application/problem+json: schema: $ref: '#/components/schemas/Problem' paths: # ===================================================================== # Health (mounted at root, outside /api/v1/) # ===================================================================== # # Documented here for completeness; the server mounts this at `/health`, # not `/api/v1/health`. Health checks aren't versioned with the business # API — monitoring tools expect them at a stable root path. /health: get: tags: [Health] summary: Liveness probe description: | Returns the aggregator's current health. Used by load balancers, k8s liveness/readiness probes, and external uptime monitoring. security: [] responses: '200': description: Aggregator is up. content: application/json: schema: $ref: '#/components/schemas/HealthStatus' '503': description: Aggregator is starting or degraded. content: application/json: schema: $ref: '#/components/schemas/HealthStatus' # ===================================================================== # Auth # ===================================================================== /auth:exchange: post: tags: [Auth] summary: Exchange a wallet signature for a JWT bearer token description: | Verify an EIP-191 `personal_sign` signature and issue a JWT bound to the signer's EOA. The signed message must use the canonical template with the EigenLayer registration chain ID (not the workflow target chain). SDKs construct the message locally — there is no `GetSignatureFormat` endpoint. operationId: authExchange security: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/AuthExchangeRequest' responses: '200': description: Authentication succeeded; token issued. content: application/json: schema: $ref: '#/components/schemas/AuthExchangeResponse' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '429': $ref: '#/components/responses/RateLimited' # ===================================================================== # Workflows # ===================================================================== /workflows: post: tags: [Workflows] summary: Create a workflow description: | Persist a new workflow definition. Each chain-aware trigger and node carries its own required `chainId` (there is no workflow-level chain); the server validates those chains and ensures the smart wallet belongs to the authenticated user. Returns the persisted Workflow with its server-assigned `id` and `createdAt`. operationId: createWorkflow requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/CreateWorkflowRequest' } responses: '201': description: Workflow created. content: application/json: schema: { $ref: '#/components/schemas/Workflow' } '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '429': { $ref: '#/components/responses/RateLimited' } get: tags: [Workflows] summary: List workflows operationId: listWorkflows parameters: - name: smartWalletAddress in: query description: Filter by smart wallet address. Repeat to OR multiple addresses. schema: type: array items: { $ref: '#/components/schemas/EthereumAddress' } - name: status in: query description: Filter by status. Repeat to OR multiple statuses. schema: type: array items: { $ref: '#/components/schemas/WorkflowStatus' } - $ref: '#/components/parameters/PageBefore' - $ref: '#/components/parameters/PageAfter' - $ref: '#/components/parameters/PageLimit' responses: '200': description: Page of workflows. content: application/json: schema: { $ref: '#/components/schemas/WorkflowList' } '401': { $ref: '#/components/responses/Unauthorized' } '429': { $ref: '#/components/responses/RateLimited' } /workflows/{id}: parameters: - name: id in: path required: true schema: { $ref: '#/components/schemas/Ulid' } get: tags: [Workflows] summary: Retrieve a workflow operationId: getWorkflow responses: '200': description: The workflow. content: application/json: schema: { $ref: '#/components/schemas/Workflow' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } delete: tags: [Workflows] summary: Cancel a workflow description: | Permanently cancels and removes the workflow. SDK method is `cancel(id)` to align with Stripe-style vocabulary. operationId: cancelWorkflow responses: '204': description: Workflow cancelled. '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } /workflows/{id}:pause: parameters: - name: id in: path required: true schema: { $ref: '#/components/schemas/Ulid' } post: tags: [Workflows] summary: Pause a workflow description: Transition from `enabled` to `disabled`. Idempotent. operationId: pauseWorkflow responses: '200': description: Workflow paused. content: application/json: schema: { $ref: '#/components/schemas/Workflow' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } '409': description: Workflow is in a terminal state and cannot be paused. content: application/problem+json: schema: { $ref: '#/components/schemas/Problem' } /workflows/{id}:resume: parameters: - name: id in: path required: true schema: { $ref: '#/components/schemas/Ulid' } post: tags: [Workflows] summary: Resume a workflow description: Transition from `disabled` to `enabled`. Idempotent. operationId: resumeWorkflow responses: '200': description: Workflow resumed. content: application/json: schema: { $ref: '#/components/schemas/Workflow' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } '409': description: Workflow is in a terminal state and cannot be resumed. content: application/problem+json: schema: { $ref: '#/components/schemas/Problem' } /workflows/{id}:trigger: parameters: - name: id in: path required: true schema: { $ref: '#/components/schemas/Ulid' } post: tags: [Workflows] summary: Manually trigger a workflow description: | Simulate an operator-detected trigger fire. The server enqueues an execution; when `isBlocking=true`, the response waits for the execution to complete. operationId: triggerWorkflow requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/TriggerWorkflowRequest' } responses: '202': description: Execution enqueued (async). Returned when isBlocking=false. content: application/json: schema: { $ref: '#/components/schemas/TriggerWorkflowResponse' } '200': description: Execution completed (blocking). Returned when isBlocking=true. content: application/json: schema: { $ref: '#/components/schemas/TriggerWorkflowResponse' } '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } '412': description: Workflow is disabled or has reached its max execution count. content: application/problem+json: schema: { $ref: '#/components/schemas/Problem' } /workflows:simulate: post: tags: [Workflows] summary: Simulate a workflow without persisting it description: | Run a workflow definition end-to-end against the engine (Tenderly simulation for chain-writing nodes) and return the full Execution. Nothing is persisted. operationId: simulateWorkflow requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/SimulateWorkflowRequest' } responses: '200': description: Simulated execution. content: application/json: # Execution schema arrives in a later commit; reserved here. schema: type: object additionalProperties: true '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } /workflows:estimateFees: post: tags: [Workflows] summary: Estimate per-execution fees for a workflow definition description: | Returns the platform execution fee, per-node COGS (gas, external API costs), and the workflow-level value-capture fee tier. operationId: estimateWorkflowFees requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/EstimateFeesRequest' } responses: '200': description: Fee estimate. content: application/json: schema: { $ref: '#/components/schemas/EstimateFeesResponse' } '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } /workflows:count: get: tags: [Workflows] summary: Count workflows description: Cheap aggregation over the same filter set as `GET /workflows`. operationId: countWorkflows parameters: - name: smartWalletAddress in: query schema: type: array items: { $ref: '#/components/schemas/EthereumAddress' } - name: status in: query schema: type: array items: { $ref: '#/components/schemas/WorkflowStatus' } responses: '200': description: Workflow count for the filter set. content: application/json: schema: { $ref: '#/components/schemas/WorkflowCount' } '401': { $ref: '#/components/responses/Unauthorized' } # ===================================================================== # Executions # ===================================================================== /executions: get: tags: [Executions] summary: List executions operationId: listExecutions parameters: - name: workflowId in: query description: Filter by workflow ID. Repeat to OR multiple workflows. schema: type: array items: { $ref: '#/components/schemas/Ulid' } - $ref: '#/components/parameters/PageBefore' - $ref: '#/components/parameters/PageAfter' - $ref: '#/components/parameters/PageLimit' responses: '200': description: Page of executions. content: application/json: schema: { $ref: '#/components/schemas/ExecutionList' } '401': { $ref: '#/components/responses/Unauthorized' } /workflows/{id}/executions: parameters: - name: id in: path required: true schema: { $ref: '#/components/schemas/Ulid' } get: tags: [Executions] summary: List executions for a workflow (convenience nested route) description: Same handler as `GET /executions?workflowId={id}`; offered as a nested route for clients that prefer it. operationId: listExecutionsForWorkflow parameters: - $ref: '#/components/parameters/PageBefore' - $ref: '#/components/parameters/PageAfter' - $ref: '#/components/parameters/PageLimit' responses: '200': description: Page of executions for the workflow. content: application/json: schema: { $ref: '#/components/schemas/ExecutionList' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } /executions/{id}: parameters: - name: id in: path required: true schema: { $ref: '#/components/schemas/Ulid' } - name: workflowId in: query required: true description: | Workflow id the execution belongs to. Required because executions are scoped to their parent workflow in the engine's storage (`t:::` keys) — there is no global execution index today. Use `GET /workflows/{id}/executions` if you only know the workflow. schema: { $ref: '#/components/schemas/Ulid' } get: tags: [Executions] summary: Retrieve an execution (full payload with steps) operationId: getExecution responses: '200': description: The execution. content: application/json: schema: { $ref: '#/components/schemas/Execution' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } /executions/{id}:getStatus: parameters: - name: id in: path required: true schema: { $ref: '#/components/schemas/Ulid' } - name: workflowId in: query required: true description: Workflow id the execution belongs to. See `getExecution`. schema: { $ref: '#/components/schemas/Ulid' } get: tags: [Executions] summary: Get execution status (lightweight, no steps) operationId: getExecutionStatus responses: '200': description: Status summary. content: application/json: schema: { $ref: '#/components/schemas/ExecutionStatusSummary' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } /executions/{id}:signal: parameters: - name: id in: path required: true schema: { $ref: '#/components/schemas/Ulid' } - name: workflowId in: query required: true description: Workflow id the execution belongs to. See `getExecution`. schema: { $ref: '#/components/schemas/Ulid' } post: tags: [Executions] summary: Deliver an approval/external signal to a waiting execution description: | Resumes a WAITING execution (durable execution). The caller must own the workflow; the signal only counts against an actual pending wait whose kind it matches and which has not timed out. v1 is the external-signal (human approval) flavor — e.g. a Telegram approve/reject for a money-moving step. operationId: signalExecution requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/SignalExecutionRequest' } responses: '200': description: The resumed (terminal or still-waiting) execution. content: application/json: schema: { $ref: '#/components/schemas/Execution' } '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } /executions/{id}:stream: parameters: - name: id in: path required: true schema: { $ref: '#/components/schemas/Ulid' } - name: workflowId in: query required: true description: Workflow id the execution belongs to. See `getExecution`. schema: { $ref: '#/components/schemas/Ulid' } get: tags: [Executions] summary: Stream execution status changes (Server-Sent Events) description: | Emits one SSE event per status change. Each event payload is an `ExecutionStatusSummary`. The stream closes when the execution reaches a terminal status (`success` / `failed` / `error`). Implementation polls the DB on a short interval (default 1s); override with `?interval=` (max enforced server-side). operationId: streamExecution parameters: - name: interval in: query schema: type: string description: 'Poll interval as a Go-style duration (e.g., `500ms`, `2s`). Default `1s`.' responses: '200': description: SSE stream of execution status updates. content: text/event-stream: schema: type: string description: 'Standard SSE framing; each `data:` payload is a JSON-encoded `ExecutionStatusSummary`.' '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } /executions:count: get: tags: [Executions] summary: Count executions operationId: countExecutions parameters: - name: workflowId in: query schema: type: array items: { $ref: '#/components/schemas/Ulid' } responses: '200': description: Execution count. content: application/json: schema: { $ref: '#/components/schemas/ExecutionCount' } '401': { $ref: '#/components/responses/Unauthorized' } /executions:stats: get: tags: [Executions] summary: Execution statistics (totals + averages) operationId: executionStats parameters: - name: workflowId in: query schema: type: array items: { $ref: '#/components/schemas/Ulid' } responses: '200': description: Execution stats. content: application/json: schema: { $ref: '#/components/schemas/ExecutionStats' } '401': { $ref: '#/components/responses/Unauthorized' } # ===================================================================== # Wallets # ===================================================================== /wallets: get: tags: [Wallets] summary: List the authenticated user's smart wallets operationId: listWallets responses: '200': description: All wallets for the authenticated user. content: application/json: schema: { $ref: '#/components/schemas/WalletList' } '401': { $ref: '#/components/responses/Unauthorized' } post: tags: [Wallets] summary: Derive (and persist) a smart wallet address from (owner, salt, factory) description: | Idempotent "ensure exists". Returns the deterministic CREATE2-derived address. POST (not GET) because it persists a wallet record server-side as a side effect of the derivation. operationId: createWallet requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/CreateWalletRequest' } responses: '200': description: Wallet derived (existing record returned if already present). content: application/json: schema: { $ref: '#/components/schemas/Wallet' } '201': description: Wallet derived (new record persisted). content: application/json: schema: { $ref: '#/components/schemas/Wallet' } '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } /wallets/{address}: parameters: - name: address in: path required: true schema: { $ref: '#/components/schemas/EthereumAddress' } patch: tags: [Wallets] summary: Update wallet properties description: 'Partial update; only fields present in the body are changed (e.g., `isHidden`).' operationId: updateWallet requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/UpdateWalletRequest' } responses: '200': description: Wallet updated. content: application/json: schema: { $ref: '#/components/schemas/Wallet' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } /wallets/{address}:withdraw: parameters: - name: address in: path required: true schema: { $ref: '#/components/schemas/EthereumAddress' } post: tags: [Wallets] summary: Withdraw funds from a smart wallet operationId: withdrawWallet requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/WithdrawRequest' } responses: '200': description: Withdrawal submitted (UserOp may still be in flight). content: application/json: schema: { $ref: '#/components/schemas/WithdrawResponse' } '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } '412': description: Insufficient balance or other on-chain precondition failure. content: application/problem+json: schema: { $ref: '#/components/schemas/Problem' } /wallets/{address}:getNonce: parameters: - name: address in: path required: true schema: { $ref: '#/components/schemas/EthereumAddress' } - $ref: '#/components/parameters/ChainIdQuery' get: tags: [Wallets] summary: Get the smart wallet's current nonce operationId: getWalletNonce responses: '200': description: Wallet nonce. content: application/json: schema: { $ref: '#/components/schemas/NonceResponse' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } # ===================================================================== # Session policies — grants of execution authority on a wallet # ===================================================================== # A policy is the record behind the grant screen: which agent may act on a # wallet, bounded by allowed actions, an ERC-20 spend cap, and an expiry. # Granting is prepare → sign → submit: the wallet needs the EIP-712 payload # before the owner can sign it, and nothing is stored until the signature # comes back. Fund authority — never reachable through a partner assertion. /wallets/{address}/policies:prepare: parameters: - name: address in: path required: true schema: { $ref: '#/components/schemas/EthereumAddress' } post: tags: [Policies] summary: Allocate a grant and return the EIP-712 payload the owner signs description: | Allocates the policy id, validation entity, and session signer, builds the exact `installValidation` calldata the signature will commit to, and returns the typed data for `eth_signTypedData_v4`. Stores nothing: a prepare that is never submitted leaves no state behind. The echoed fields must be passed back verbatim to `:submit` — the gateway recomputes everything from them, so tampering only produces a signature that no longer verifies. operationId: prepareWalletPolicy requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/PreparePolicyRequest' } responses: '200': description: Payload to sign, plus the allocations submit must echo. content: application/json: schema: { $ref: '#/components/schemas/PreparedPolicy' } '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } /wallets/{address}/policies:submit: parameters: - name: address in: path required: true schema: { $ref: '#/components/schemas/EthereumAddress' } post: tags: [Policies] summary: Submit the owner's signature and persist the grant description: | Recomputes the grant from the echoed prepare fields, verifies the signature recovers to the authenticated owner, re-checks the entity allocation inside the write, and stores the policy as `pending`. The install itself rides the first workflow operation on this wallet — nothing reaches the chain here. operationId: submitWalletPolicy requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/SubmitPolicyRequest' } responses: '201': description: Grant stored; the gateway may now execute within it. content: application/json: schema: { $ref: '#/components/schemas/SessionPolicy' } '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } '409': description: | The validation entity was taken by another grant while this one was being signed. Prepare again. content: application/problem+json: schema: { $ref: '#/components/schemas/Problem' } /wallets/{address}/policies: parameters: - name: address in: path required: true schema: { $ref: '#/components/schemas/EthereumAddress' } - $ref: '#/components/parameters/ChainIdQuery' get: tags: [Policies] summary: List the wallet's session policies description: Grant material (calldata, signatures) is never echoed. operationId: listWalletPolicies responses: '200': description: Policies for this wallet, newest first. content: application/json: schema: { $ref: '#/components/schemas/SessionPolicyList' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } /wallets/{address}/policies/{policyId}: parameters: - name: address in: path required: true schema: { $ref: '#/components/schemas/EthereumAddress' } - name: policyId in: path required: true schema: { $ref: '#/components/schemas/Ulid' } - $ref: '#/components/parameters/ChainIdQuery' get: tags: [Policies] summary: Get one session policy operationId: getWalletPolicy responses: '200': description: The policy. content: application/json: schema: { $ref: '#/components/schemas/SessionPolicy' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } delete: tags: [Policies] summary: Revoke a session policy description: | A `pending` grant (never used on-chain) is deleted outright — nothing was installed, so nothing remains. An `active` grant stops authorizing immediately, but its on-chain validation still exists until the owner executes `uninstallValidation`; the response says which case applied. operationId: revokeWalletPolicy responses: '200': description: Revocation outcome. content: application/json: schema: { $ref: '#/components/schemas/RevokePolicyResponse' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } # ===================================================================== # Secrets # ===================================================================== /secrets: get: tags: [Secrets] summary: List secrets (metadata only — values are never echoed) operationId: listSecrets parameters: - name: workflowId in: query schema: { $ref: '#/components/schemas/Ulid' } - name: orgId in: query schema: { type: string } - $ref: '#/components/parameters/PageBefore' - $ref: '#/components/parameters/PageAfter' - $ref: '#/components/parameters/PageLimit' responses: '200': description: Page of secret metadata. content: application/json: schema: { $ref: '#/components/schemas/SecretList' } '401': { $ref: '#/components/responses/Unauthorized' } /secrets/{name}: parameters: - name: name in: path required: true schema: type: string pattern: '^[a-zA-Z0-9_]+$' put: tags: [Secrets] summary: Create or replace a secret (idempotent) operationId: putSecret requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/PutSecretRequest' } responses: '201': description: Secret created. '204': description: Secret replaced (already existed). '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } delete: tags: [Secrets] summary: Delete a secret operationId: deleteSecret parameters: - name: workflowId in: query schema: { $ref: '#/components/schemas/Ulid' } - name: orgId in: query schema: { type: string } responses: '204': description: Secret deleted. '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } # ===================================================================== # Tokens # ===================================================================== /tokens/{address}: parameters: - name: address in: path required: true schema: { $ref: '#/components/schemas/EthereumAddress' } get: tags: [Tokens] summary: Retrieve ERC-20 token metadata operationId: getToken parameters: - $ref: '#/components/parameters/ChainIdQuery' responses: '200': description: 'Token metadata (`found: false` if unknown).' content: application/json: schema: { $ref: '#/components/schemas/TokenMetadataResponse' } '401': { $ref: '#/components/responses/Unauthorized' } # ===================================================================== # Standalone node / trigger execution (testing flows) # ===================================================================== /nodes:run: post: tags: [Nodes] summary: Execute a single node with inline input variables description: | Useful for SDK testing flows — run a node definition against provided input variables without persisting a workflow. Honors `node.config.chainId` (overrides body `chainId`). operationId: runNode requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/RunNodeRequest' } responses: '200': description: Node execution result. content: application/json: schema: { $ref: '#/components/schemas/RunNodeResponse' } '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } /triggers:run: post: tags: [Triggers] summary: Evaluate a trigger definition with inline input operationId: runTrigger requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/RunTriggerRequest' } responses: '200': description: Trigger evaluation result. content: application/json: schema: { $ref: '#/components/schemas/RunTriggerResponse' } '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } # ===================================================================== # Operators (read-only monitoring) # ===================================================================== /operators: get: tags: [Operators] summary: List connected operators description: | Read-only monitoring endpoint. Returns each operator's `supportedChainIds`, `capabilities`, `lastSeen`, and `version`. Useful for dashboards and debugging multi-chain task routing. operationId: listOperators responses: '200': description: Connected operators. content: application/json: schema: { $ref: '#/components/schemas/OperatorList' } '401': { $ref: '#/components/responses/Unauthorized' }