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: UserOps description: Status lookup for UserOps this user submitted (not a public AA explorer) - name: Triggers description: Stand-alone trigger evaluation - name: Operators description: Connected operator status (read-only) # Default: end-user JWT. Operations that accept partner assertions (or # require them exclusively) override `security` per path. See # aggregator/rest/permission.go and GATEWAY_CHANGE_REQUIRED_PARTNER_GATED_READS.md. 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 `. partnerAssertion: type: apiKey in: header name: X-Partner-Assertion description: | Short-lived Ed25519-signed partner assertion (JWT). `iss` must match a registered partner id; `scope` must include the required scope for the route (typically `read`); short-lived `exp` is required. For preview wallet list/create, `sub` must be the end-user's non-zero 0x EOA. See partners[] in gateway config. 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. Bytes32: type: string pattern: '^0x[a-fA-F0-9]{64}$' description: 32-byte hex hash (UserOp hash, transaction hash). example: '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' UserOpInnerCall: type: object required: [to, value, selector, data] properties: to: $ref: '#/components/schemas/EthereumAddress' value: type: string description: Native value in wei (decimal string). selector: type: string pattern: '^0x[a-fA-F0-9]{8}$' description: 4-byte function selector of `data` (`0x00000000` for a value-only call). data: $ref: '#/components/schemas/Hex' UserOpStatusResponse: type: object required: [userOpHash, executionStatus] properties: userOpHash: $ref: '#/components/schemas/Bytes32' sender: $ref: '#/components/schemas/EthereumAddress' executionStatus: type: string enum: [pending, confirmed, failed] description: | `pending` means the bundler accepted the op and it is not yet mined. It is not a failure. transactionHash: $ref: '#/components/schemas/Hex' blockNumber: type: string description: Block number as a hex string when mined. success: type: boolean description: Inner UserOp success from the bundler receipt. Absent while pending. calls: type: array items: $ref: '#/components/schemas/UserOpInnerCall' failedCall: $ref: '#/components/schemas/UserOpInnerCall' description: Present when the inner call reverted and there is a single inner call. 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' policyId: type: string description: | Usable session grant involved in this failure, when there is one. Set on `SESSION_POLICY_BASE_CHANGED`, `SESSION_POLICY_NOT_COVERING`, `SESSION_POLICY_TARGET_UNRESOLVED`, `SESSION_POLICY_NATIVE_UNSIZED`, `SESSION_POLICY_EXPIRED`, and `SESSION_POLICY_SIGNER_RETIRED`. affectedTaskIds: type: array items: { type: string } description: | Enabled tasks a grant would leave unable to run. Set on `409 SESSION_POLICY_NOT_COVERING`, `409 SESSION_POLICY_TARGET_UNRESOLVED`, and `409 SESSION_POLICY_NATIVE_UNSIZED` from policies:prepare and policies:submit. missingActions: type: array items: { $ref: '#/components/schemas/AllowedAction' } description: Planned calls the grant does not allow. required: $ref: '#/components/schemas/SessionPolicyNeed' description: | Grant the caller still needs. Set on `409 SESSION_POLICY_NOT_COVERING`, `409 SESSION_POLICY_EXPIRED`, and `409 SESSION_POLICY_SIGNER_RETIRED` from create and resume. # ------------------------------------------------------------------- # 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 # Prefix Go constants (WorkflowStatusEnabled, …). oapi-codegen only # prefixes when names collide; these five don't, so they landed as # Enabled/Completed/Failed and can be confused with ExecutionStatus. x-enum-varnames: - WorkflowStatusEnabled - WorkflowStatusDisabled - WorkflowStatusRunning - WorkflowStatusCompleted - WorkflowStatusFailed 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' } 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 description: | Empty when status is success. On failed, a summary of the form `N of M steps failed: : , …` so the cause (e.g. a bundler `replacement underpriced`) is on this envelope, not only on GET /executions/{id}. On error, the system message. steps: type: array items: { $ref: '#/components/schemas/ExecutionStep' } description: | Execution steps. Populated when isBlocking=true (same as gRPC TriggerTaskResp.steps). The envelope `error` summary is built from each failed step's `error` string. `errorCode` is copied onto the step only; it is not concatenated into `error`. 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' } authorizationMode: type: string enum: [enforce, report] description: | `enforce` when omitted. Grant preflight fails the step, which is what one-shot Auto paths read as `SESSION_POLICY_TARGET_NOT_ALLOWED`. `report` does not fail steps for a missing or short grant, and the execution includes `authorization`. A storage lookup failure still fails the step. maxExecution: type: integer format: int64 description: | Runs this workflow may still make. With a cron trigger and `expiredAt`, the requirement uses whichever allows fewer runs. startAt: type: integer format: int64 description: Unix milliseconds. Schedule window start, same clock as create. expiredAt: type: integer format: int64 description: Unix milliseconds. Schedule window end, same clock as create. 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 } ExecutionSummary: type: object additionalProperties: true description: | Studio's summary-step reply, present once the run has ended. Absent on older runs and while the run is still in progress. properties: subject: type: string description: Report title. body: type: object additionalProperties: true description: | Structured report. Its fields include the summary line, executions, errors, notice, and step counts. 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). summary: { $ref: '#/components/schemas/ExecutionSummary' } authorization: $ref: '#/components/schemas/SessionAuthorization' description: | Present on simulate when `authorizationMode` is `report`. Omitted on stored executions and on enforce-mode simulates. 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). Includes the run summary once the run has ended. 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 } summary: { $ref: '#/components/schemas/ExecutionSummary' } 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). Empty for `kind: eoa_7702` (no factory, no salt). factoryAddress: $ref: '#/components/schemas/EthereumAddress' description: Factory contract used to derive this address. Omitted for eoa_7702. kind: type: string description: | Empty/omitted for derived CREATE2 wallets. `eoa_7702` is the owner's EOA delegated to SemiModularAccount7702. enum: [eoa_7702] delegate: $ref: '#/components/schemas/EthereumAddress' description: SMA-7702 implementation the EOA designates. Set when kind is eoa_7702. 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 } PreparedDelegation: type: object required: [chainId, delegate, nonce, digest] description: | EIP-7702 authorization the owner signs. `digest` is `SetCodeAuthorization.SigHash` (0x05 ‖ rlp([chainId, delegate, nonce])). `chainId` is never 0. Aggregator-broadcast submit uses this nonce as the EOA's nonce (the EOA is the authority, not the type-4 sender). properties: chainId: { $ref: '#/components/schemas/ChainId' } delegate: { $ref: '#/components/schemas/EthereumAddress' } nonce: type: integer format: int64 minimum: 0 description: Authority (EOA) nonce for the 7702 authorization. digest: { $ref: '#/components/schemas/Hex' } SubmitDelegationRequest: type: object required: [chainId, nonce, signature] properties: chainId: { $ref: '#/components/schemas/ChainId' } nonce: type: integer format: int64 minimum: 0 signature: $ref: '#/components/schemas/Hex' description: 65-byte ECDSA over digest (v 0/1 or 27/28). DelegationStatus: type: object required: [status] properties: status: type: string enum: [missing, pending, delegated] delegate: { $ref: '#/components/schemas/EthereumAddress' } codeHash: { $ref: '#/components/schemas/Hex' } chainId: { $ref: '#/components/schemas/ChainId' } 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 deprecated: true description: > Legacy single-node overrides. Nonempty overrides are rejected. Simulate actions as a complete workflow including their prerequisites. 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 for one token, enforced on-chain at execution. The token must appear as an `allowedActions` target. Prefer `erc20SpendCaps` when capping more than one token; this field remains the one-token alias (must match one entry of that array when both are sent). Native ETH is not this list. NativeSpendCap: type: object required: [amount] properties: amount: type: string pattern: '^[0-9]+$' description: | Cumulative native-token cap in wei (decimal string, no reset). Enforced on-chain by NativeTokenLimitModule. Self-funded UserOps also decrement this cap by gas; sponsored UserOps decrement only the ETH value sent. When the chain does not sponsor gas, prepare sizes headroom for those UserOps into this amount: the steady-op ceiling times a per-chain fee assumption, plus one install. That headroom can also be sent as value to the allowed recipients. example: '10000000000000000' SessionAuthorization: type: object required: [status] description: | Opt-in simulate verdict. Returned only when `authorizationMode` is `report`. It does not replace step errors from anything other than grant coverage. properties: status: type: string enum: [covered, no_grant, not_covered, cap_too_low, expires_too_soon, cap_needs_input, target_unresolved] description: | `covered` — the usable grant already allows this workflow, or the workflow moves no funds. `no_grant` — fund-moving steps and no usable grant, including a grant made for a session signer the gateway no longer uses. `not_covered` — a planned call or native recipient is outside the grant. `cap_too_low` — a sized cap or native budget is short. `expires_too_soon` — the grant has already expired, or it ends before this workflow's window. A workflow with no end still reports this when the grant's own expiry has passed. `cap_needs_input` — a spend amount is not a fixed number, so the caller must choose the cap. `target_unresolved` — a fund-moving target could not be read from the stored workflow (a loop over a previous step, not a settings list). Choosing a spend cap does not name that target. One observed iteration does not clear this. Pause the automation, or name it in `dropTaskIds`. policyId: type: string description: The runner's current usable grant, when there is one. missingActions: type: array items: { $ref: '#/components/schemas/AllowedAction' } required: $ref: '#/components/schemas/SessionPolicyNeed' detail: type: string SessionPolicyNeed: type: object description: | Permissions one workflow still needs, sized to the runs it has left. Cap amounts are totals in the token's smallest unit, not per-run amounts. properties: allowedActions: type: array items: { $ref: '#/components/schemas/AllowedAction' } erc20SpendCaps: type: array items: { $ref: '#/components/schemas/Erc20SpendCap' } nativeRecipients: type: array maxItems: 5 items: { $ref: '#/components/schemas/EthereumAddress' } nativeSpendCap: { $ref: '#/components/schemas/NativeSpendCap' } nativeGasWei: type: string pattern: '^[0-9]+$' description: | Wei of `nativeSpendCap` that is self-funded gas rather than ETH this workflow sends. Steady UserOps only. Prepare adds one install for the wallet when the grant is not yet applied. Omitted when this workflow's gas was not priced. validUntil: type: integer format: int64 description: Absolute unix milliseconds the grant must last through. 0 when the window is unknown. SessionPolicyAddition: type: object description: | What the automation being set up needs. Merged, under the runner lock, with what that runner's enabled tasks on this chain still need. Cap amounts are totals for this automation. When the runner has a usable grant, each cap is the greater of what remains on that grant and what enabled automations still need, plus this addition. With no usable grant, caps start at zero and this addition is added to what enabled automations still need. properties: allowedActions: type: array items: { $ref: '#/components/schemas/AllowedAction' } erc20SpendCaps: type: array items: { $ref: '#/components/schemas/Erc20SpendCap' } nativeRecipients: type: array maxItems: 5 items: { $ref: '#/components/schemas/EthereumAddress' } nativeSpendCap: { $ref: '#/components/schemas/NativeSpendCap' } nativeGasWei: type: string pattern: '^[0-9]+$' description: | Wei of this addition's `nativeSpendCap` that is self-funded gas rather than ETH sent. Already included in that cap. Prepare does not add it again. It lets the approval screen split payments from gas. Omit when the cap is value only. allowContractRecipient: type: boolean validUntil: type: integer format: int64 description: | Absolute unix milliseconds this automation needs. Omit to use now + `expiresInSeconds`. The signed expiry is the later of that horizon and every enabled task's end on this chain. SessionPolicyCapChange: type: object required: [token, amount] properties: token: { $ref: '#/components/schemas/EthereumAddress' } amount: type: string description: New total, in the token's smallest unit. previousAmount: type: string description: | What remained on the previous grant, in the token's smallest unit. Once that grant is on chain, this is the on-chain remainder. Before then, it is the stored cap. Omitted when the token is new. removed: type: boolean description: | True when this token's cap was dropped. `amount` is then "0", and `previousAmount` is what remained. Omitted when the cap is still on the grant. SessionPolicyExpiryChange: type: object required: [name, validUntil] properties: taskId: { type: string } name: { type: string } validUntil: type: integer format: int64 previousValidUntil: type: integer format: int64 description: The wallet grant's previous expiry. One expiry covers every permission. SessionPolicyChanges: type: object required: [summary] description: | What the approval screen shows. `summary` is the copy. The structured fields are the same facts. properties: nativeGasWei: type: string pattern: '^[0-9]+$' description: | Wei of the signed native cap that is self-funded gas: enabled automations' steady UserOps, one install when the grant is not yet applied, plus `nativeGasWei` from the addition. Omitted when this prepare did not price gas. The rest of the signed native cap is ETH the automations send. This headroom can also be sent as value to the allowed recipients. summary: type: array items: { type: string } description: | Approval lines. An expiry change reads ": until , was " in UTC when both dates are in the same year, for example "Weekly swap: until Dec 28, was Nov 30". When the years differ, both dates include the year. basePolicyId: type: string description: | Usable grant the merge read. Empty when the runner had none. Echo this to submit, including the empty string. keptActions: type: array items: { $ref: '#/components/schemas/AllowedAction' } addedActions: type: array items: { $ref: '#/components/schemas/AllowedAction' } removedActions: type: array items: { $ref: '#/components/schemas/AllowedAction' } capChanges: type: array items: { $ref: '#/components/schemas/SessionPolicyCapChange' } expiryChanges: type: array items: { $ref: '#/components/schemas/SessionPolicyExpiryChange' } PreparePolicyRequest: type: object required: [chainId, agentLabel, 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' } description: | Selector-scoped contract calls. Native-only grants omit this field. A present empty array is 400. erc20SpendCap: { $ref: '#/components/schemas/Erc20SpendCap' } erc20SpendCaps: type: array minItems: 1 items: { $ref: '#/components/schemas/Erc20SpendCap' } description: | Per-token ERC-20 caps (AllowlistModule HasERC20SpendLimit per target). Source of truth when present. `erc20SpendCap` must match one entry. A capped token's allowedActions may only be transfer and/or approve — deposit/withdraw on the same target reverts on-chain. Native ETH is not this list. nativeRecipients: type: array minItems: 1 maxItems: 5 # keep in sync with MaxNativeRecipients in session_permissions.go items: { $ref: '#/components/schemas/EthereumAddress' } description: | EOAs this grant may send native ETH to (empty-calldata execute). Omit for Uniswap/ERC-20-only and for payable-write-only (nativeValueCap). A present empty array is 400. Max 5 (MaxNativeRecipients; deferred replace of 20-row grants AA23s). nativeSpendCap: { $ref: '#/components/schemas/NativeSpendCap' } allowContractRecipient: type: boolean default: false description: | When true, nativeRecipients may be contracts (any-function on that address, ERC-20 uncapped). Default false: each recipient must have empty code. Logged when true. expiresInSeconds: type: integer format: int64 minimum: 60 maximum: 9223372036 description: | Grant lifetime, relative (skew-proof). Becomes an absolute validUntil. When `add` is set, this is the new automation's horizon if `add.validUntil` is omitted. The signed expiry can be later, because the wallet keeps one expiry and it must cover every enabled automation. Values above 9223372036 overflow the duration conversion and are rejected. basePolicyId: type: string description: | Usable grant this prepare was merged against. Omit on a legacy prepare that sends the full permission set and does not compare. Send an empty string when the client believes there is no usable grant. A mismatch is 409 SESSION_POLICY_BASE_CHANGED. dropTaskIds: type: array items: { type: string } description: | Enabled tasks this prepare may leave out of the merged grant. Honored only when `add` is set. Echo the same ids on submit. When the runner has a usable grant, a task whose target cannot be read does not fail prepare and does not add that target. When there is no usable grant, that task is `409 SESSION_POLICY_TARGET_UNRESOLVED` until it is paused or named here. add: $ref: '#/components/schemas/SessionPolicyAddition' description: | Create or update in one call. When set, the permission fields above are ignored. The response's permission fields are the merged set and must be echoed to submit. 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. basePolicyId: type: string description: | Set when prepare merged an `add`. Empty means there was no usable grant. Echo `changes.basePolicyId`, including the empty string. affectedTaskIds: type: array items: { type: string } description: | Enabled tasks this prepare left out because `dropTaskIds` named them. Echo those ids as submit's `dropTaskIds`. changes: $ref: '#/components/schemas/SessionPolicyChanges' allowedActions: type: array items: { $ref: '#/components/schemas/AllowedAction' } description: Merged actions to echo to submit. Present when `add` was sent. erc20SpendCap: { $ref: '#/components/schemas/Erc20SpendCap' } erc20SpendCaps: type: array items: { $ref: '#/components/schemas/Erc20SpendCap' } nativeRecipients: type: array items: { $ref: '#/components/schemas/EthereumAddress' } nativeSpendCap: { $ref: '#/components/schemas/NativeSpendCap' } allowContractRecipient: type: boolean SubmitPolicyRequest: type: object required: [chainId, policyId, entityId, deadline, validUntil, agentLabel, 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' } description: | Selector-scoped contract calls. Native-only grants omit this field. A present empty array is 400. Echo prepare verbatim. erc20SpendCap: { $ref: '#/components/schemas/Erc20SpendCap' } erc20SpendCaps: type: array minItems: 1 items: { $ref: '#/components/schemas/Erc20SpendCap' } description: Per-token ERC-20 caps. Source of truth when present; erc20SpendCap must match one entry. nativeRecipients: type: array minItems: 1 maxItems: 5 # keep in sync with MaxNativeRecipients in session_permissions.go items: { $ref: '#/components/schemas/EthereumAddress' } nativeSpendCap: { $ref: '#/components/schemas/NativeSpendCap' } allowContractRecipient: type: boolean default: false signature: type: string pattern: '^0x[a-fA-F0-9]{130}$' description: The owner's 65-byte signature over the prepared digest. basePolicyId: type: string description: | Echo prepare's `basePolicyId` / `changes.basePolicyId`, including an empty string when prepare saw no usable grant. Omit only on a legacy submit that did not compare. A mismatch is 409 SESSION_POLICY_BASE_CHANGED. dropTaskIds: type: array items: { type: string } description: | Enabled tasks this grant may leave uncovered. Echo prepare's `affectedTaskIds`. Naming a task does not fail submit. When a grant was carried, submit refuses with `409 SESSION_POLICY_NOT_COVERING` only when the new grant covers less of an enabled task than the current grant did. A task the current grant already does not cover does not freeze the wallet. When there is no current grant, every other enabled task whose fund-moving steps are outside the grant is `409 SESSION_POLICY_NOT_COVERING`, and a task whose target cannot be read is `409 SESSION_POLICY_TARGET_UNRESOLVED` unless it is named here. 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' } erc20SpendCaps: type: array minItems: 1 items: { $ref: '#/components/schemas/Erc20SpendCap' } description: | Per-token ERC-20 caps. Omitted on singular-only grants; present when the client submitted `erc20SpendCaps`. The alias `erc20SpendCap` is the submitted singular when submitted; otherwise the first list entry. nativeRecipients: type: array minItems: 1 items: { $ref: '#/components/schemas/EthereumAddress' } nativeSpendCap: { $ref: '#/components/schemas/NativeSpendCap' } allowContractRecipient: type: boolean 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. affectedTaskIds: type: array items: { type: string } description: | Enabled tasks this grant leaves uncovered, which the request named in `dropTaskIds`. Empty when every enabled task on the runner is still covered. 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. The token expires at the message's `Expire At`, which may be at most 48 hours from now. A message that expires later is refused with `AUTH_EXPIRY_TOO_FAR`. 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`. When the gateway config `session_policy_deploy_check` is on, a workflow whose fund-moving steps the runner's usable grant does not cover is refused with `409 SESSION_POLICY_NOT_COVERING`. `required` is the grant those steps still need. A fund-moving workflow whose single usable grant has already expired is `409 SESSION_POLICY_EXPIRED`, including when the workflow has no end date. A fund-moving step whose target cannot be read stays `409 SESSION_POLICY_TARGET_UNRESOLVED` even when that grant has expired. A grant with no recorded expiry is not expired. A grant made for a session signer the gateway no longer uses is `409 SESSION_POLICY_SIGNER_RETIRED`: it cannot execute however well it covers the workflow. The owner grants again. A workflow that only notifies passes with no grant. The flag defaults to off, so today's callers are unchanged. 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' } '409': description: | `SESSION_POLICY_NOT_COVERING` when `session_policy_deploy_check` is on and the runner's usable grant does not cover this workflow's fund-moving steps. `required`, `policyId`, and `missingActions` describe the gap. `SESSION_POLICY_EXPIRED` when that grant has already expired, including when the workflow has no end date. `SESSION_POLICY_SIGNER_RETIRED` when that grant was made for a session signer the gateway no longer uses. `SESSION_POLICY_TARGET_UNRESOLVED` when a fund-moving target cannot be read, including when the grant has also expired. Notification-only workflows do not hit this. content: application/problem+json: schema: { $ref: '#/components/schemas/Problem' } '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. When `session_policy_deploy_check` is on, enabling a workflow whose fund-moving steps the runner's usable grant does not cover is `409 SESSION_POLICY_NOT_COVERING`. A fund-moving workflow whose grant has already expired is `409 SESSION_POLICY_EXPIRED`, including when the workflow has no end date. A fund-moving step whose target cannot be read stays `409 SESSION_POLICY_TARGET_UNRESOLVED` even when that grant has expired. A grant made for a session signer the gateway no longer uses is `409 SESSION_POLICY_SIGNER_RETIRED`. Disabling is not checked. Notification-only workflows pass with no grant. 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: | The workflow is in a terminal state and cannot be resumed, or `session_policy_deploy_check` is on and enabling it would leave its fund-moving steps outside the runner's usable grant (`SESSION_POLICY_NOT_COVERING`; `required` describes the gap). `SESSION_POLICY_EXPIRED` when the runner's grant has already expired, including when the workflow has no end date. `SESSION_POLICY_SIGNER_RETIRED` when it was made for a session signer the gateway no longer uses. `SESSION_POLICY_TARGET_UNRESOLVED` when a fund-moving target cannot be read, including when the grant has also expired. 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. Requires a user Bearer JWT; partner assertion alone is not sufficient. Grant preflight fails fund-moving steps unless `authorizationMode` is `report`. Report mode returns `authorization` and lets the simulation proceed so a test screen can show "it works" and "needs permission" as two results. `maxExecution`, `startAt`, and `expiredAt` size that verdict to the schedule. They do not change the simulated run. operationId: simulateWorkflow security: - bearerAuth: [] 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; includes the run summary once the run has ended) 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 description: | Preview wallet resolve. Authorize with a user Bearer JWT **or** a partner assertion (`scope: read`) whose `sub` is the owner EOA. Wallets are stored per chain, so the listing is per chain too. The `chainId` query selects which one, on the same precedence as `POST /wallets`: explicit value, then the JWT `aud` chain, then the gateway default. A JWT minted for one chain can therefore list another's wallets without a second signature — it proves EOA ownership, which is chain-independent. operationId: listWallets security: - bearerAuth: [] - partnerAssertion: [] parameters: - $ref: '#/components/parameters/ChainIdQuery' 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. Authorize with a user Bearer JWT **or** a partner assertion (`scope: read`) whose `sub` is the owner EOA. operationId: createWallet security: - bearerAuth: [] - partnerAssertion: [] 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' } /wallets/{address}/delegation:prepare: parameters: - name: address in: path required: true schema: { $ref: '#/components/schemas/EthereumAddress' } description: The owner's EOA (the account that will be 7702-delegated). post: tags: [Wallets] summary: Return the EIP-7702 authorization the owner signs description: | AVS-side consent, first of two approvals (delegate, then session grant). Chain-bound: `chainId=0` is refused. First production chains are Sepolia and Base, and the SMA-7702 pin must be configured. Partner assertions are refused. Does not broadcast and does not install session hooks. operationId: prepareEoaDelegation requestBody: required: false content: application/json: schema: type: object properties: chainId: { $ref: '#/components/schemas/ChainId' } responses: '200': description: Authorization payload to sign. content: application/json: schema: { $ref: '#/components/schemas/PreparedDelegation' } '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } /wallets/{address}/delegation:submit: parameters: - name: address in: path required: true schema: { $ref: '#/components/schemas/EthereumAddress' } post: tags: [Wallets] summary: Broadcast the signed 7702 authorization and persist the EOA runner description: | Reconstructs the authorization, recovers the authority (must be the path EOA), broadcasts a type-4 tx from the aggregator (the EOA is not the sender, so the auth nonce is the EOA nonce, not nonce+1), then asserts K13 (`ef0100`‖delegate + impl hash) — never tx status. Upserts `kind=eoa_7702` wallet record. Does not install session hooks. operationId: submitEoaDelegation requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/SubmitDelegationRequest' } responses: '200': description: EOA is designated to SMA-7702. content: application/json: schema: { $ref: '#/components/schemas/DelegationStatus' } '202': description: | Type-4 was broadcast; K13 is not yet visible. Poll GET `/wallets/{address}/delegation` (GET upserts the eoa_7702 row once code matches). Do not resubmit — the EOA nonce has not changed and a second broadcast spends controller gas again. content: application/json: schema: { $ref: '#/components/schemas/DelegationStatus' } '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '409': description: Code at the EOA is not the pinned SMA-7702 designation. content: application/problem+json: schema: { $ref: '#/components/schemas/Problem' } /wallets/{address}/delegation: parameters: - name: address in: path required: true schema: { $ref: '#/components/schemas/EthereumAddress' } get: tags: [Wallets] summary: Current 7702 designation of the EOA operationId: getEoaDelegation parameters: - $ref: '#/components/parameters/ChainIdQuery' responses: '200': description: | Delegation status from a code read (K13), not tx status. GET returns `missing` or `delegated` only — never `pending`. `pending` is the 202 submit body while the type-4 has been broadcast but K13 is not yet visible. Poll GET until `delegated`; `missing` after submit means not visible yet, not that submit failed. Do not resubmit. content: application/json: schema: { $ref: '#/components/schemas/DelegationStatus' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } # ===================================================================== # 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. When `add` is set and the runner has a usable grant, the merged grant starts from that grant. Actions, native recipients, and `allowContractRecipient` only grow. Each token cap is what is left on that grant (the on-chain remainder once the grant is applied, otherwise the stored cap), or what enabled automations still need when that is higher, plus the addition. A token with nothing left is omitted. A router action stays. A task whose target cannot be read does not fail prepare and does not add that unknown target. When `add` is set and there is no usable grant, the merged grant is the addition plus every enabled task except ids in `dropTaskIds`. A task whose token or recipient cannot be read from the stored workflow (a loop over a previous step) is `409 SESSION_POLICY_TARGET_UNRESOLVED`. That is not an unsized amount. Pause the automation, or name its id in `dropTaskIds`. An unsized token amount on this path is `400 POLICIES_BAD_PERMISSIONS`. Installing a native spend cap while a running payable cannot be sized is `409 SESSION_POLICY_NATIVE_UNSIZED`, including when the runner has no usable grant. The addition or a sized running payable can be what installs that cap. 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': description: | `POLICIES_BAD_PERMISSIONS` — the grant is not signable. When there is no usable grant, an unsized token amount is this code. An unsized native payable that would install a native cap is 409. An amount that was a JSON number past the exact integer range says to send a decimal string. `SESSION_POLICY_TOO_MANY_NATIVE_RECIPIENTS` — more than five native recipients. Finished automations keep their addresses until revoke. `SESSION_POLICY_RECIPIENT_NOT_EOA` — a native recipient is a contract or a 7702-delegated EOA. content: application/problem+json: schema: { $ref: '#/components/schemas/Problem' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } '409': description: | `SESSION_POLICY_BASE_CHANGED` — `basePolicyId` is not the runner's usable grant anymore. Prepare again. `SESSION_POLICY_TARGET_UNRESOLVED` — there is no usable grant, and an enabled task moves funds to a target this prepare cannot read, and that task was not listed in `dropTaskIds`. `affectedTaskIds` names them. Pausing the task, or naming it in `dropTaskIds`, is the way through. When a usable grant exists, an unresolved task does not fail prepare. `SESSION_POLICY_NATIVE_UNSIZED` — `add` would install a native spend cap and a running task's payable value cannot be sized. `affectedTaskIds` names those tasks. Pausing one, or naming it in `dropTaskIds`, is the way through. The native limit module is not installed. If the current grant already has a native cap, this is not an error. content: application/problem+json: schema: { $ref: '#/components/schemas/Problem' } /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. The deferred batch that installs the new grant also uninstalls up to four prior validation entities (#717). The owner signs that batch once, at prepare. Replacing a grant does not leave the old entities installed until a separate `uninstallValidation` signature. `DELETE` of an already-applied grant is different: the controller cannot call `uninstallValidation` itself, so that response still carries `onChainCleanup` for the owner wallet. When `basePolicyId` is sent, including an empty string, the runner's usable grant must still be that value or the call is `409 SESSION_POLICY_BASE_CHANGED`. That code also covers a carried grant whose signed permissions no longer match a fresh merge: the remaining limit moved, the running set changed, or both. The grant id can be unchanged. Prepare again. When a grant was carried, the stored grant is refused with `409 SESSION_POLICY_NOT_COVERING` only when it covers less of an enabled task than the current grant did. Tasks listed in `dropTaskIds` are the ones this grant may leave uncovered. When there is no current grant, the stored grant must still cover every enabled task on this runner except those ids, or the call is `409 SESSION_POLICY_NOT_COVERING` and nothing is stored. A missing prepare snapshot (restart, or the prepare was evicted) uses that same per-task check. 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': description: | `POLICIES_BAD_PERMISSIONS` — the echoed grant is not signable, or, when there is no usable grant, a token amount cannot be sized. An unsized native payable that would install a native cap is `409 SESSION_POLICY_NATIVE_UNSIZED`. `SESSION_POLICY_TOO_MANY_NATIVE_RECIPIENTS` — more than five native recipients. Finished automations keep their addresses until revoke. `SESSION_POLICY_RECIPIENT_NOT_EOA` — a native recipient is a contract or a 7702-delegated EOA. content: application/problem+json: schema: { $ref: '#/components/schemas/Problem' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } '409': description: | `POLICIES_ENTITY_TAKEN` — the validation entity was taken by another grant while this one was being signed. Prepare again. `SESSION_POLICY_BASE_CHANGED` — `basePolicyId` is not the runner's usable grant anymore, or a carried grant's remaining limit or running set moved after prepare. The grant id can be unchanged. Prepare again. `SESSION_POLICY_NOT_COVERING` — when a grant was carried, the new grant covers less of an enabled task than the current grant did. When there is no current grant, an enabled task on the runner would be unable to run. Tasks listed in `dropTaskIds` are excluded. `affectedTaskIds` names the tasks. `SESSION_POLICY_TARGET_UNRESOLVED` — there is no usable grant, and an enabled task moves funds to a target the grant cannot read, and that task was not listed in `dropTaskIds`. `affectedTaskIds` names them. When a usable grant exists, an unresolved task does not fail submit by itself. `SESSION_POLICY_NATIVE_UNSIZED` — the echoed grant would install a native spend cap and a running task's payable value cannot be sized. `affectedTaskIds` names those tasks. Pausing one, or naming it in `dropTaskIds`, is the way through. If the current grant already has a native cap, this is not an error. content: application/problem+json: schema: { $ref: '#/components/schemas/Problem' } '503': description: | `POLICIES_ENTITY_UNVERIFIED` — the signed validation entity could not be read from the chain. Nothing was stored. Retry submit with the same signature before its signing window expires; no new prepare or owner signature is needed unless that window expires. 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 description: | Partner-gated public-ish chain data. Requires a partner assertion with `scope: read` (`X-Partner-Assertion`). A user Bearer JWT alone is not sufficient. operationId: getToken security: - partnerAssertion: [] 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: | Run a non-transaction node against inline variables without persisting a workflow. Contract writes and native transfers, including their loop runners, are rejected. Simulate the complete workflow with its prerequisites using `POST /workflows:simulate`. Honors `node.config.chainId` (overrides body `chainId`). Requires a user Bearer JWT; partner assertion alone is not sufficient. operationId: runNode security: - bearerAuth: [] 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' } /userops/{userOpHash}: get: tags: [UserOps] summary: Look up a UserOp this user submitted description: | Re-poll a UserOp by hash (G3 / N14.a). Same gate as `nodes:run` (user Bearer JWT; partner assertion alone is not sufficient). The sender must be one of the caller's smart wallets — this is **not** a public AA explorer. Unknown hashes and hashes that belong to another user both return 404. Inner `calls` are unpacked from the UserOp `callData` via the gateway's existing `UnpackExecuteCalldata` (execute / executeBatch / MA v2). Pending is not failed. operationId: getUserOp security: - bearerAuth: [] parameters: - name: userOpHash in: path required: true schema: $ref: '#/components/schemas/Bytes32' - $ref: '#/components/parameters/ChainIdQuery' responses: '200': description: Typed UserOp status and inner calls. content: application/json: schema: { $ref: '#/components/schemas/UserOpStatusResponse' } '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } /triggers:run: post: tags: [Triggers] summary: Evaluate a trigger definition with inline input description: | Requires a user Bearer JWT; partner assertion alone is not sufficient. operationId: runTrigger security: - bearerAuth: [] 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. Currently unauthenticated (public monitoring). operationId: listOperators security: [] responses: '200': description: Connected operators. content: application/json: schema: { $ref: '#/components/schemas/OperatorList' }