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://api.avaprotocol.org/api/v1 description: Production 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`. token: { $ref: '#/components/schemas/EthereumAddress' } spender: { $ref: '#/components/schemas/EthereumAddress' } neededWei: { type: string, pattern: '^[0-9]+$' } availableWei: { type: string, pattern: '^[0-9]+$' } 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. recipient: $ref: '#/components/schemas/EthereumAddress' description: | Native recipient a policies:prepare or policies:submit refused for its code. Set on `400 SESSION_POLICY_RECIPIENT_NOT_EOA`, `400 SESSION_POLICY_RECIPIENT_NOT_ACKNOWLEDGED`, and `400 SESSION_POLICY_RECIPIENT_DELEGATE_CHANGED`. It can be an enabled task's recipient rather than one the request added. delegate: $ref: '#/components/schemas/EthereumAddress' description: | Delegate the recipient's EIP-7702 designation names now. Set on `SESSION_POLICY_RECIPIENT_NOT_ACKNOWLEDGED` and `SESSION_POLICY_RECIPIENT_DELEGATE_CHANGED`. acknowledgedDelegate: $ref: '#/components/schemas/EthereumAddress' description: | Delegate the grant acknowledged for `recipient`. Set on `SESSION_POLICY_RECIPIENT_DELEGATE_CHANGED`. # ------------------------------------------------------------------- # 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. reserveGas: type: boolean default: false description: Owner preparation only. Treat amount as a captured balance cap and deduct the signed maximum gas cost; sponsored operations reserve zero. This node must be terminal. 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. PrepareWorkflowRequest: type: object additionalProperties: false required: [chainId, trigger, nodes, inputVariables] description: | A Manual definition with read/check nodes and exactly one terminal ContractWrite. The initial owner path supports one configured chain, MA v2 derived wallets with an EOA fallback owner, and zero native value. No persistent/Auto workflow or session grant is created. Requires gateway owner_execution_enabled: true (default false). properties: chainId: { $ref: '#/components/schemas/ChainId' } trigger: { $ref: '#/components/schemas/Trigger' } nodes: type: array minItems: 1 maxItems: 64 items: { $ref: '#/components/schemas/Node' } edges: type: array maxItems: 128 items: { $ref: '#/components/schemas/Edge' } inputVariables: { $ref: '#/components/schemas/InputVariables' } OwnerUserOperationV07: type: object additionalProperties: false description: | Finalized ERC-4337 v0.7 bundler-wire operation. Quantity fields are hex strings, never JSON numbers. Signature is empty (0x) until continuation; no operation field can be supplied or changed by submitExecution. required: [sender, nonce, callData, callGasLimit, verificationGasLimit, preVerificationGas, maxFeePerGas, maxPriorityFeePerGas, signature] properties: sender: { $ref: '#/components/schemas/EthereumAddress' } nonce: { $ref: '#/components/schemas/Hex' } callData: { $ref: '#/components/schemas/Hex' } callGasLimit: { $ref: '#/components/schemas/Hex' } verificationGasLimit: { $ref: '#/components/schemas/Hex' } preVerificationGas: { $ref: '#/components/schemas/Hex' } maxFeePerGas: { $ref: '#/components/schemas/Hex' } maxPriorityFeePerGas: { $ref: '#/components/schemas/Hex' } factory: { $ref: '#/components/schemas/EthereumAddress' } factoryData: { $ref: '#/components/schemas/Hex' } paymaster: { $ref: '#/components/schemas/EthereumAddress' } paymasterData: { $ref: '#/components/schemas/Hex' } paymasterVerificationGasLimit: { $ref: '#/components/schemas/Hex' } paymasterPostOpGasLimit: { $ref: '#/components/schemas/Hex' } signature: { $ref: '#/components/schemas/Hex' } PrepareWorkflowResponse: type: object required: [execution, userOperation, userOpHash, owner, chainId, entryPoint, signingMethod, expiresAt, writeNodeId, calls, gasFunding, maxGasCostWei] properties: execution: allOf: - $ref: '#/components/schemas/Execution' description: | Transient execution in waiting status. Steps contain completed reads/checks only; the terminal write is in calls, not reported as completed. No deployed workflow/history record exists for this run. userOperation: { $ref: '#/components/schemas/OwnerUserOperationV07' } userOpHash: { $ref: '#/components/schemas/Bytes32' } owner: { $ref: '#/components/schemas/EthereumAddress' } chainId: { $ref: '#/components/schemas/ChainId' } entryPoint: { $ref: '#/components/schemas/EthereumAddress' } signingMethod: type: string enum: [personal_sign] description: | Sign the 32 raw bytes of userOpHash, with the EIP-191 prefix applied once; ethers signMessage(getBytes(userOpHash)). Offered only for the verified alchemy.sma-bytecode.1.0.0 implementation. expiresAt: type: integer format: int64 description: | Gateway continuation deadline, Unix milliseconds (10 minutes). This is not an on-chain signature expiry or revocation mechanism. writeNodeId: { type: string } calls: type: array items: { $ref: '#/components/schemas/UserOpInnerCall' } gasFunding: type: string enum: [source, sponsored] description: Final funding choice; sponsorship refusal never silently charges the source. maxGasCostWei: type: string description: Signed maximum gas cost in decimal wei, before any paymaster sponsorship. SubmitExecutionRequest: type: object additionalProperties: false required: [signature] properties: signature: type: string pattern: '^0x[a-fA-F0-9]{130}$' description: 65-byte EOA personal signature of the prepared hash. Recovery byte 0/1 or 27/28; gateway verifies and frames it for MA v2. SubmitExecutionResponse: type: object required: [executionId, userOpHash, chainId, submissionState] properties: executionId: { $ref: '#/components/schemas/Ulid' } userOpHash: { $ref: '#/components/schemas/Bytes32' } chainId: { $ref: '#/components/schemas/ChainId' } submissionState: type: string enum: [submitting, submitted, unknown] description: | Relay progress, not transaction success. Poll GET /userops/{userOpHash} with chainId for the existing receipt status. Unknown/submitting can survive a transport failure or restart; repeating submit never changes the operation or starts another broadcast. 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 } errorDetails: type: object properties: token: { $ref: '#/components/schemas/EthereumAddress' } needed: { type: string } remaining: { type: string } validUntil: { type: integer, format: int64 } additionalProperties: true description: Structured refusal fields. BUDGET_USED_UP has token/needed/remaining; BUDGET_ENDED has validUntil. 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: | The full 256-bit nonce, `key << 64 | sequence`, as a decimal string. Use it as `userOp.nonce` unchanged. # ------------------------------------------------------------------- # 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' DelegatedNativeRecipient: type: object required: [recipient, delegate] description: | The owner acknowledged that `recipient` is an EIP-7702 delegated EOA and that its designation named `delegate`. The recipient must also be in `nativeRecipients`. One entry per recipient. Listing an address here does not authorize a plain contract, and it does not set `allowContractRecipient`. Trusting the delegate is the client's job; this record only checks that the designation still names the acknowledged delegate. The ETH send stays an empty-calldata transfer. properties: recipient: { $ref: '#/components/schemas/EthereumAddress' } delegate: { $ref: '#/components/schemas/EthereumAddress' } 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' } missingTokens: type: array description: Budget mode only. Spending tokens that need a new budget row. items: { $ref: '#/components/schemas/EthereumAddress' } needsUntil: type: integer format: int64 description: Budget mode only. The recurring workflow end that exceeds the budget end, in Unix milliseconds. required: $ref: '#/components/schemas/SessionPolicyNeed' detail: type: string SessionPolicyNeed: type: object description: | Permissions one workflow still needs. Workflow mode sizes the remaining runs. Budget mode reports one run and never sizes the wallet budget from this need. Cap amounts are in the token's smallest unit. properties: allowedActions: type: array items: { $ref: '#/components/schemas/AllowedAction' } erc20SpendCaps: type: array items: { $ref: '#/components/schemas/Erc20SpendCap' } nativeRecipients: type: array maxItems: 20 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: 20 items: { $ref: '#/components/schemas/EthereumAddress' } delegatedNativeRecipients: type: array minItems: 1 maxItems: 20 items: { $ref: '#/components/schemas/DelegatedNativeRecipient' } description: | Per-recipient EIP-7702 acknowledgements for native ETH recipients in this addition. Carried per recipient when grants merge; when the current grant and this addition name different delegates for the same recipient, the gateway records this addition's delegate. The owner signs the install call. The ETH row in that call is not limited to the delegate, and the acknowledgement is not part of the signature. The gateway checks it again when it sends. A present empty array is 400. Does not change `allowContractRecipient`, and does not authorize a plain contract. 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] properties: approvalDiscovery: $ref: '#/components/schemas/BudgetApprovalDiscovery' budget: $ref: '#/components/schemas/WalletBudgetInput' description: | Owner totals, independent of workflow run counts. Budget signing requires the stored owner-operation handoff, with separate preparation and signing flags. It never produces a legacy deferred-install signature. Signing is Sepolia-only, self-funded, and subject to the configurable maximum fee (default 0.005 ETH). 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: 20 # 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 20 (MaxNativeRecipients; a 20-row replace with four teardowns mines, measured on Sepolia). delegatedNativeRecipients: type: array minItems: 1 maxItems: 20 items: { $ref: '#/components/schemas/DelegatedNativeRecipient' } description: | Owner acknowledgements of EIP-7702 delegations on native ETH recipients. A recipient with code is allowed only when that code is a designation (`0xef0100` plus a 20-byte delegate), this list names that recipient, and the designation still points at the acknowledged delegate. A plain contract stays refused, even if it is listed. Empty code stays allowed. A present empty array is 400. This does not change `allowContractRecipient`, and the send stays an empty-calldata transfer. nativeSpendCap: { $ref: '#/components/schemas/NativeSpendCap' } allowContractRecipient: type: boolean default: false description: | When true, every native recipient may be a contract (any function on that address, ERC-20 uncapped). Default false: each recipient must have empty code, or be an acknowledged EIP-7702 delegation (`delegatedNativeRecipients`). Logged when true. An acknowledgement is not this flag. expiresInSeconds: type: integer format: int64 x-go-type-skip-optional-pointer: true 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. BudgetChangeState: type: string enum: [prepared, submitting, submitted, active, reverted, rejected, unsent, superseded] description: | prepared expires after ten minutes; that window limits only the first relay. submitting, submitted and rejected remain recoverable without expiry and keep Ava runs paused. A bundler rejection is not final. active, reverted and superseded are settled. unsent is possible only before a submitting marker. Successful inclusion stays submitted until reset verification passes; activation and hold release are atomic. PreparedPolicy: type: object required: [policyId, chainId, entityId, sessionSigner, deadline, validUntil] properties: state: { $ref: '#/components/schemas/BudgetChangeState' } reason: { type: string } transactionHash: { $ref: '#/components/schemas/Hex' } inclusionBlock: { type: integer, format: int64 } finalReadBlock: { type: integer, format: int64 } isCancel: type: boolean description: A successful cancel settles as active with isCancel true; it consumes the nonce and does not activate a new permission. preparationId: { $ref: '#/components/schemas/Ulid' } signing: { $ref: '#/components/schemas/PolicySigning' } budget: { $ref: '#/components/schemas/WalletBudget' } approvalResets: type: array items: { $ref: '#/components/schemas/BudgetApprovalReset' } unmeteredPermissions: type: array items: { $ref: '#/components/schemas/UnmeteredPermission' } 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 x-go-type-skip-optional-pointer: true pattern: '^0x[a-fA-F0-9]{64}$' description: The EIP-712 hash the typed data produces, for client-side verification. typedData: type: object x-go-type-skip-optional-pointer: true 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' } delegatedNativeRecipients: type: array minItems: 1 maxItems: 20 items: { $ref: '#/components/schemas/DelegatedNativeRecipient' } description: | Merged EIP-7702 acknowledgements to echo to submit. Present when `add` was sent and the merged grant has any. See DelegatedNativeRecipient. nativeSpendCap: { $ref: '#/components/schemas/NativeSpendCap' } allowContractRecipient: type: boolean SubmitPolicyRequest: type: object required: [chainId, policyId, entityId, deadline, validUntil, agentLabel, signature] properties: budget: $ref: '#/components/schemas/WalletBudgetInput' description: | Not accepted by this stateless submit route. Presence returns 409 BUDGET_HANDOFF_REQUIRED. Budget signatures must continue a stored preparation on the signature-only owner-operation route. 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: 20 # keep in sync with MaxNativeRecipients in session_permissions.go items: { $ref: '#/components/schemas/EthereumAddress' } delegatedNativeRecipients: type: array minItems: 1 maxItems: 20 items: { $ref: '#/components/schemas/DelegatedNativeRecipient' } description: | Echo prepare's `delegatedNativeRecipients`. A recipient with code is allowed only when its designation still names the acknowledged delegate. A plain contract stays refused. A present empty array is 400. Does not change `allowContractRecipient`. 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 } sizing: allOf: - $ref: '#/components/schemas/BudgetSizing' description: Absent means workflow sizing. Budget fields are additive and not written yet. budgetStartedAt: { type: integer, format: int64 } budgetStartAmounts: type: array items: { $ref: '#/components/schemas/Erc20SpendCap' } budgetStartNative: { $ref: '#/components/schemas/NativeSpendCap' } 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' } delegatedNativeRecipients: type: array minItems: 1 maxItems: 20 items: { $ref: '#/components/schemas/DelegatedNativeRecipient' } description: | EIP-7702 acknowledgements stored with this grant. A native send to one of these recipients is refused once the designation names a different delegate. A plain contract is not authorized by being listed here. Does not change `allowContractRecipient`. 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). BudgetApprovalPair: type: object required: [token, spender] properties: token: { $ref: '#/components/schemas/EthereumAddress' } spender: { $ref: '#/components/schemas/EthereumAddress' } BudgetApprovalDiscovery: type: object additionalProperties: false required: [owner, wallet, chainId, candidates, allowedContracts, source, fetchedAt, complete, signature] description: | Trusted backend attestation of the full unfiltered approval inventory (every page), plus catalog-token/allowed-contract pairs. No caller's unsigned list is trusted. The gateway verifies the configured service signer and freshness (two minutes), then reads allowances on chain. Signature is EIP-712 ApprovalDiscovery, domain name Ava Wallet Budget Approval Discovery, version 1, chainId, verifyingContract=wallet. The message binds owner, wallet, candidatesHash, allowedContractsHash, source, fetchedAt and complete. candidatesHash is keccak256 of sorted unique token/spender address pairs concatenated as two left-padded 32-byte words per pair. properties: owner: { $ref: '#/components/schemas/EthereumAddress' } wallet: { $ref: '#/components/schemas/EthereumAddress' } chainId: { $ref: '#/components/schemas/ChainId' } candidates: type: array maxItems: 10000 items: { $ref: '#/components/schemas/BudgetApprovalPair' } allowedContracts: type: array maxItems: 10000 description: Complete resulting callable target set; its sorted unique address words are hashed as allowedContractsHash. items: { $ref: '#/components/schemas/EthereumAddress' } source: { type: string, enum: [moralis] } fetchedAt: { type: integer, format: int64 } complete: { type: boolean } signature: { $ref: '#/components/schemas/Hex' } BudgetApprovalReset: type: object required: [token, spender, amount] properties: token: { $ref: '#/components/schemas/EthereumAddress' } spender: { $ref: '#/components/schemas/EthereumAddress' } amount: { type: string, pattern: '^[0-9]+$' } PolicySigning: type: object required: [kind] description: | Budget preparations use owner_operation and omit legacy digest and typedData. Preparation and first submission have separate feature flags, off by default. With signing enabled, signature-only first submission relays the exact stored owner operation on Sepolia. The owner personal_signs the 32 raw bytes of userOpHash. Submission is self-funded and fee capped; repeat submissions reconcile without rebroadcasting. properties: kind: { type: string, enum: [deferred_action, owner_operation] } preparationId: { $ref: '#/components/schemas/Ulid' } userOp: { $ref: '#/components/schemas/OwnerUserOperationV07' } userOpHash: { $ref: '#/components/schemas/Bytes32' } maxFeeWei: { type: string, pattern: '^[0-9]+$' } expiresAt: { type: integer, format: int64 } signingMethod: { type: string, enum: [personal_sign] } entryPoint: { $ref: '#/components/schemas/EthereumAddress' } WalletBudgetInput: type: object required: [validUntil] properties: erc20: type: array items: { $ref: '#/components/schemas/Erc20SpendCap' } description: | Owner totals, not the on-chain remainder. Tokens may appear only once. Plain raises/extensions carry omitted totals unchanged; startNewTotals requires every existing capped token total. native: $ref: '#/components/schemas/NativeSpendCap' description: | Combined ETH total, including earlier gas additions. A plain raise/extension may omit it; startNewTotals must include it when the current permission has a native cap. validUntil: type: integer format: int64 minimum: 1 startNewTotals: type: boolean default: false description: | Owner Change/Renew budget. Requires an explicit total for every existing capped token and the native cap; partial resets are refused. Starts usage counting again when activation succeeds. removeActions: type: array items: { $ref: '#/components/schemas/AllowedAction' } description: Owner-only removal of named selectors. Other rows and totals are retained. BudgetSizing: type: string enum: [workflow, budget] BudgetTokenRow: type: object required: [token, amount, remaining, used, unlimited] properties: token: { $ref: '#/components/schemas/EthereumAddress' } amount: { type: string } remaining: { type: string } used: { type: string } unlimited: { type: boolean } BudgetNativeRow: type: object required: [amount, remaining, used, unlimited] properties: amount: { type: string } remaining: { type: string } used: { type: string } unlimited: { type: boolean } gasWei: type: string description: Recorded gas reserve inside amount. Omitted when the stored breakdown is unknown; never reconstructed. UnmeteredPermission: type: object required: [kind, target, selectors, added] properties: kind: type: string enum: [aave_withdraw, aave_borrow, wrapped_native, other] target: { $ref: '#/components/schemas/EthereumAddress' } selectors: type: array items: { type: string } token: { $ref: '#/components/schemas/EthereumAddress' } added: { type: boolean } BudgetRetirement: type: object required: [entityId, state] properties: entityId: { type: integer, format: int64 } policyId: { $ref: '#/components/schemas/Ulid' } state: type: string enum: [registered, residual, unverified] erc20: type: array items: { $ref: '#/components/schemas/Erc20SpendCap' } description: Remaining limits of an entity confirmed registered; absent otherwise. native: { $ref: '#/components/schemas/NativeSpendCap' } LiveApproval: type: object required: [token, spender, amount] properties: token: { $ref: '#/components/schemas/EthereumAddress' } spender: { $ref: '#/components/schemas/EthereumAddress' } amount: { type: string } WalletBudget: type: object required: [chainId, policyId, status, sizing, validUntil, erc20, unmeteredPermissions, readAt] properties: chainId: { $ref: '#/components/schemas/ChainId' } policyId: { $ref: '#/components/schemas/Ulid' } status: type: string enum: [pending, active] sizing: { $ref: '#/components/schemas/BudgetSizing' } validUntil: { type: integer, format: int64 } startedAt: { type: integer, format: int64 } erc20: type: array items: { $ref: '#/components/schemas/BudgetTokenRow' } native: { $ref: '#/components/schemas/BudgetNativeRow' } unmeteredPermissions: type: array items: { $ref: '#/components/schemas/UnmeteredPermission' } retirements: type: array items: { $ref: '#/components/schemas/BudgetRetirement' } liveApprovals: type: array items: { $ref: '#/components/schemas/LiveApproval' } description: Reserved for approval discovery; omitted in this slice, not an assertion that no approvals exist. pendingChange: type: object required: [policyId, state] properties: preparationId: { $ref: '#/components/schemas/Ulid' } policyId: { $ref: '#/components/schemas/Ulid' } state: type: string enum: [prepared, submitting, submitted, reverted, rejected, unsent, superseded] reason: { type: string } description: Latest durable budget change. A rejected or reset-unverified change explains why runs remain paused. readAt: { type: integer, format: int64 } 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:prepare: post: tags: [Workflows] summary: Prepare a Manual workflow for one owner-signed batch description: | Resolve existing read/check nodes, capture the terminal ContractWrite calls, and build/price one MA v2 batch without signing or broadcasting. Requires the concrete owner's Bearer JWT and a registered supported source wallet. Current on-chain owner, implementation and gas funding are checked independently of the stored wallet association. Derived wallets require the verified factory personal_sign build 0x000000000000c5A9089039570Dd36455b5C07383. An EOA source must already be delegated to the pinned SMA-7702 1.0.0 implementation, registered as kind=eoa_7702, and enabled by eoa_7702_execute on Sepolia or Base. EOA operations use sender=owner and omit factory deployment fields. Delegation is a separate explicit wallet action; prepare never installs delegation or a session grant. Both account kinds use personal_sign of the 32 raw userOpHash bytes, owner nonce entity 0/global option 1. A canonical delegated EOA can also sign for its derived smart wallet. Up to 32 calls; no native value, write Loop or post-write nodes. Unsigned preparations are capped at 64 per owner. Submitted, uncertain, rejected, and unsent records do not consume that cap. A gateway with owner execution disabled returns 403 OWNER_EXECUTION_DISABLED; preparing again does not enable it. Other precondition failures return 412 and ask for a fresh prepare. Ordinary Auto/session and simulate behavior is unchanged. operationId: prepareWorkflow security: - bearerAuth: [] requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/PrepareWorkflowRequest' } responses: '200': description: Priced operation awaiting an owner signature; no transaction has been submitted. content: application/json: schema: { $ref: '#/components/schemas/PrepareWorkflowResponse' } '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '412': description: Checks, source compatibility, funding or pricing failed; refresh the preparation. content: application/problem+json: schema: { $ref: '#/components/schemas/Problem' } '429': { $ref: '#/components/responses/RateLimited' } /executions/{id}:submit: post: tags: [Executions] summary: Continue a prepared execution with its owner's transaction signature description: | Submit the exact priced operation from prepareWorkflow. Accepts only the signature, not caller-supplied UserOperation fields. Rechecks the current owner/account identity, nonce and funding before first relay. Uses SendUserOpV07 without a server signing key or gas/nonce retry. Material changes or bundler rejection require fresh prepare/review/signing. Repeat calls still require a valid signature and reconcile the existing hash without rebroadcasting. Status polling requires the JWT only. A durable submitting marker precedes the send. The relay after that marker keeps running when the client disconnects. A crash before the RPC leaves the id submitting. A retry looks the hash up at the bundler: a hit is stored as submitted; an unknown hash stays submitting until the ten-minute signing window ends, then becomes unsent and requires a new prepare. That prepare uses the same sender and owner nonce key (entity 0, global), read from EntryPoint.getNonce with no session nonce cache, so an unmined operation and its replacement cannot both be included. A lookup error leaves the marker submitting. A post-send storage failure also preserves the marker. Poll the prepared hash. An empty bundler result during the window is unresolved. Uncertain and submitted records are retained for 24 hours and do not count toward the 64 prepared-record cap. A response is not a receipt; reuse GET /userops/{userOpHash} for confirmation. operationId: submitExecution security: - bearerAuth: [] parameters: - name: id in: path required: true schema: { $ref: '#/components/schemas/Ulid' } requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/SubmitExecutionRequest' } responses: '202': description: Relay accepted or uncertain; use the existing UserOp status route. content: application/json: schema: { $ref: '#/components/schemas/SubmitExecutionResponse' } '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } '412': description: Preparation is stale or rejected; prepare, review and sign again. content: application/problem+json: schema: { $ref: '#/components/schemas/Problem' } /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 description: | The nonce for the next operation the owner signs. It is read from EntryPoint v0.7 at the owner's validation key (`0x1`: entity 0, global validation). Modular Account v2 selects the validation from the nonce key, so a nonce read at key 0 is not one the account accepts from its owner. Reflects mined state only. 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/preparations/{id}: parameters: - name: address in: path required: true schema: { $ref: '#/components/schemas/EthereumAddress' } - name: id in: path required: true schema: { $ref: '#/components/schemas/Ulid' } get: tags: [Policies] operationId: getWalletBudgetPreparation summary: Recover an owned budget preparation or durable change parameters: - $ref: '#/components/parameters/ChainIdQuery' responses: '200': description: Exact stored review and priced owner operation; no usable grant is created. content: application/json: schema: { $ref: '#/components/schemas/PreparedPolicy' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } '409': description: Budget preparation is disabled. content: application/problem+json: schema: { $ref: '#/components/schemas/Problem' } /wallets/{address}/policies/preparations/{id}:submit: parameters: - name: address in: path required: true schema: { $ref: '#/components/schemas/EthereumAddress' } - name: id in: path required: true schema: { $ref: '#/components/schemas/Ulid' } post: tags: [Policies] operationId: submitWalletBudgetPreparation summary: Relay one stored owner-signed budget operation description: | Feature off by default, Sepolia only. First submit requires an unexpired preparation. Submit accepts only a personal_sign signature of the raw userOpHash. A durable hold and submitting marker precede relay of the exact operation. Repeat calls reconcile the hash without rebroadcasting. Rejected or unknown sends keep the hold until the nonce is consumed. Successful inclusion stays submitted until reset proof passes; activation and hold release are atomic. Signing is self-funded and fee capped. parameters: - $ref: '#/components/parameters/ChainIdQuery' requestBody: required: true content: application/json: schema: type: object additionalProperties: false required: [signature] properties: signature: { $ref: '#/components/schemas/Hex' } responses: '200': description: Stored change and its current state; active only after reset proof. content: application/json: schema: { $ref: '#/components/schemas/PreparedPolicy' } '404': { $ref: '#/components/responses/NotFound' } '503': description: Required chain or storage evidence is unavailable; no unsafe hold release. content: application/problem+json: schema: { $ref: '#/components/schemas/Problem' } '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '409': description: Budget submission is not enabled. content: application/problem+json: schema: { $ref: '#/components/schemas/Problem' } /wallets/{address}/policies/preparations/{id}:cancel: parameters: - name: address in: path required: true schema: { $ref: '#/components/schemas/EthereumAddress' } - name: id in: path required: true schema: { $ref: '#/components/schemas/Ulid' } post: tags: [Policies] operationId: prepareWalletBudgetCancel summary: Prepare a self-funded owner cancellation on the held nonce description: Returns a fresh signing review containing only an owner no-op. Submit its id and signature through the budget submit route. Never broadcasts automatically. parameters: - $ref: '#/components/parameters/ChainIdQuery' responses: '200': description: Exact priced cancellation operation to review and sign. content: application/json: schema: { $ref: '#/components/schemas/PreparedPolicy' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } '409': description: No includable held nonce, unsupported chain or signing disabled. content: application/problem+json: schema: { $ref: '#/components/schemas/Problem' } /wallets/{address}/policies:prepare: parameters: - name: address in: path required: true schema: { $ref: '#/components/schemas/EthereumAddress' } post: tags: [Policies] summary: Prepare a legacy typed grant or a stored unsigned budget operation description: | With budget or approvalDiscovery, the experimental off-by-default wallet_budget_preparation_enabled path stores a priced unsigned owner operation and the exact intent/read/derived review. It requires a fresh signed approvalDiscovery, explicitly self-funds, and returns signing.kind owner_operation (personal_sign of raw userOpHash). It creates no usable policy or hold. Budget submit needs the separate off-by-default wallet_budget_signing_enabled flag and is limited to Sepolia. GET /wallets/{address}/policies/preparations/{id} recovers the same review. Missing/stale discovery returns 409 BUDGET_DISCOVERY_REQUIRED; incomplete discovery or failed reads return 503; 64 live unsigned records per owner returns 429. Only tested token deployments may be reset automatically: v1 Sepolia USDC. Other live allowances requiring reset return 409 BUDGET_RESET_UNSUPPORTED_TOKEN with token/spender; insufficient ETH returns 409 BUDGET_FEE_INSUFFICIENT_ETH with neededWei/availableWei. A quote above wallet_budget_max_fee_wei (default 0.005 ETH) returns 409 BUDGET_FEE_CEILING_EXCEEDED. Unsupported Permit2, NFT/operator or unaudited spending targets return 409 BUDGET_ROUTE_UNSUPPORTED. A live unknown validation authority returns BUDGET_AUTHORITY_UNKNOWN; remove it in the wallet and check again. Ava's session operations are refused with BUDGET_CHANGING while the wallet/chain hold is open; scheduled runs wait without consuming an execution. A known, unexpired legacy deferred signature returns 409 BUDGET_LEGACY_GRANT_PENDING with its latest outstanding deadline. The default legacy signing window lasts up to 30 days. Revoking a permission off chain does not invalidate its signature; migration must wait until that window closes. This route offers no nonce-cancellation remedy and an owner nonce change does not cancel a session-entity nonce. Legacy path (no budget/discovery, non-budget wallet): 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. `delegatedNativeRecipients` is carried per recipient; when both grants name a delegate for the same recipient, the gateway records the addition's delegate. The owner signs the install call; that call's ETH row is not limited to the delegate. The acknowledgement is gateway state, checked again when the gateway sends. 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`. Acknowledgements for those running recipients come from the wallet's last stored grant, including one that was revoked. The chain read still applies, and this addition's delegate wins when both name one. 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 twenty native recipients. Finished automations keep their addresses until revoke. `SESSION_POLICY_RECIPIENT_NOT_EOA` — a native recipient is a contract. `SESSION_POLICY_RECIPIENT_NOT_ACKNOWLEDGED` — a native recipient is a 7702-delegated EOA that `delegatedNativeRecipients` does not acknowledge. `SESSION_POLICY_RECIPIENT_DELEGATE_CHANGED` — an acknowledged recipient's designation now names a different delegate. All three set `recipient`, which can be another enabled task's recipient: the merged grant carries every running automation's. The last two also set `delegate`, and a changed delegate sets `acknowledgedDelegate`. 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. `BUDGET_LEGACY_GRANT_PENDING` — an earlier deferred signature can still install, even after off-chain revocation. The detail names the latest deadline; the default window can last 30 days. `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' } '429': { $ref: '#/components/responses/RateLimited' } '503': description: A required balance, allowance or complete discovery read is unavailable. 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 twenty native recipients. Finished automations keep their addresses until revoke. `SESSION_POLICY_RECIPIENT_NOT_EOA` — a native recipient is a contract. `SESSION_POLICY_RECIPIENT_NOT_ACKNOWLEDGED` — a native recipient is a 7702-delegated EOA that `delegatedNativeRecipients` does not acknowledge. `SESSION_POLICY_RECIPIENT_DELEGATE_CHANGED` — an acknowledged recipient's designation now names a different delegate. All three set `recipient`, which can be another enabled task's recipient: the merged grant carries every running automation's. The last two also set `delegate`, and a changed delegate sets `acknowledgedDelegate`. 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}/budget: get: tags: [Wallets] summary: Read the current permission's remaining token and native limits operationId: getWalletBudget description: | User JWT only; partner assertions are refused. Works for workflow-sized permissions too. Pending deferred installs report stored totals with status pending. Active limits are read on chain; a failed remainder read is 503, never a stored-value fallback. Retirement read failures appear as unverified entries in an otherwise successful response. parameters: - name: address in: path required: true schema: { $ref: '#/components/schemas/EthereumAddress' } - $ref: '#/components/parameters/ChainIdQuery' responses: '200': description: Current limits and retirement health content: application/json: schema: { $ref: '#/components/schemas/WalletBudget' } '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': description: No current permission for this wallet and chain content: application/problem+json: schema: { $ref: '#/components/schemas/Problem' } '409': description: More than one current permission; authority is ambiguous content: application/problem+json: schema: { $ref: '#/components/schemas/Problem' } '503': description: BUDGET_READ_UNAVAILABLE — a remainder could not be read 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' }