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. onChainCleanup: allOf: - $ref: '#/components/schemas/OnChainRevokeCleanup' description: | Present when status is revoked and the validation entity is still installed on chain. Owner-executable uninstallValidation so clients can finish teardown without re-calling DELETE (#717). SubmitPolicyResponse: allOf: - $ref: '#/components/schemas/SessionPolicy' - type: object required: [supersededPolicyIds] properties: supersededPolicyIds: type: array items: { $ref: '#/components/schemas/Ulid' } description: | Grants revoked to keep this runner's authority a singleton — the previous grants this submit replaced. Their `status` now reads `revoked`; the array distinguishes a replacement the gateway performed from one the user asked for via `DELETE .../policies/{policyId}`. Empty on a first grant. Non-empty means the user's earlier permission is gone, which is worth reflecting in the UI. SessionPolicyList: type: object required: [items] properties: items: type: array items: { $ref: '#/components/schemas/SessionPolicy' } OnChainRevokeCleanup: type: object required: [entityId, target, callData, chainId] description: | Owner-executable call that clears an applied grant's validation entity and hooks from the runner. Production grants are policied: the gateway controller cannot self-uninstall (allowlist blocks uninstallValidation). The owner sends this as a plain transaction to `target` (or a UserOp validated by the owner fallback). Derived from the retained `Grant.InstallCall`, never from live permission structs. properties: entityId: type: integer format: int64 minimum: 1 description: Validation entity this cleanup removes. target: $ref: '#/components/schemas/EthereumAddress' description: The smart wallet (runner) to call. callData: $ref: '#/components/schemas/Hex' description: uninstallValidation calldata (0x-prefixed). chainId: type: integer format: int64 description: Chain the runner lives on. 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. onChainCleanup: allOf: - $ref: '#/components/schemas/OnChainRevokeCleanup' description: | Present when onChainCleanupRequired is true. The wallet client executes this call to finish on-chain teardown (#717). 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. **This REPLACES the runner's previous grants.** A wallet carries at most one usable grant: the send path resolves exactly one and refuses to execute when it finds more, so grants do not stack. Every other usable grant on this runner is revoked as part of this call, and the ones revoked come back in `supersededPolicyIds`. There is no flag to opt out, and clients do not need to revoke the previous grant first. Replacement is scoped to the runner, not to a capability: submitting a grant for one capability revokes the runner's grant for any other. Off-chain only. The superseded grants' validation entities and ERC-20 spend caps stay installed on the account until the owner signs `uninstallValidation` — replacing a grant does not reduce what the account could authorize on chain, only what this gateway will use. 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/SubmitPolicyResponse' } '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: | Outcomes: - `status: deleted` — rare: no InstallCall retained; record removed. - `status: revoked`, `onChainCleanupRequired: false` — pending grant retained so InstallCall survives a late-landing install (orphan path). Not known on chain yet; no cleanup payload. - `status: revoked`, `onChainCleanupRequired: true` — applied grant still believed installed. Response includes `onChainCleanup` (uninstallValidation call for the owner wallet: plain tx to the runner, or owner-fallback UserOp). Once teardown is verified, GET no longer returns cleanup for that policy. 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' }