> ## Documentation Index
> Fetch the complete documentation index at: https://docs.b3os.org/llms.txt
> Use this file to discover all available pages before exploring further.

# GET /v1/workflows/{id}/analyze-funds

Analyze the fund requirements of a workflow without executing it. Returns resolved (wallet, chain, token, amount) tuples for every action that moves money, plus a `balance` row per requirement when the wallet-balances client is configured. The endpoint is read-only and does not consume CU.

` GET /v1/workflows/{id}/analyze-funds `

Analyze the fund requirements of a workflow without executing it. Returns resolved (wallet, chain, token, amount) tuples for every action that moves money, plus a `balance` row per requirement when the wallet-balances client is configured. The endpoint is read-only and does not consume CU.

## Parameters

### ` id ` (path, required)

Workflow ID

- ` value `: type ` string `

### ` version ` (query, optional)

Pin analysis to a specific workflow version

- ` value `: type ` integer `

## Responses

### ` 200 ` — OK

Content type: ` application/json `

- ` value `: type ` object `

  - ` code `: type ` integer `

    - example: ` 200 `

  - ` data `: type ` object `

    - ` balancesFetched `: type ` boolean `

    - ` requirements `: type ` array `

      - ` array item `: type ` object `

        - ` amountIsHumanReadable `: type ` boolean ` — AmountIsHumanReadable is AmountUnit == "human", restated for the wire. DERIVED — set only by deriveWireFields, never by a producer. It reports false for "raw" AND for an undeclared unit, which are different things, so anything rescaling by decimals must read AmountUnit instead.

        - ` amountIsPartial `: type ` boolean ` — AmountIsPartial means RequiredAmount understates the true total for a reason OTHER than a runtime expression: an operand was dropped, or the amount is per-iteration inside a loop. Floor semantics apply (the number is a lower bound), but this flag does not claim a source or later discovery point for the remainder.

        - ` amountRuntimeDetermined `: type ` boolean ` — AmountRuntimeDetermined is true when the leg's amount is known only when the workflow runs. Usually that means the amount is wired to a runtime expression ({{...}}) rather than a literal and could not be constant-folded; it can also be declared by action metadata when the action itself reads the complete amount at execution time, such as an all-balance close. The leg's IDENTITY (chain + token) is fully resolved — only the SIZE is unknown until the workflow runs. Distinct from Unresolved, which means the identity itself could not be determined.  When multiple legs sharing the same dedup key (wallet + chain + token) are merged (addOrMerge), this flag is true if ANY contributing leg was runtime-determined — even when another contributing leg supplied a literal amount. It is never cleared just because a literal amount is also present.  FLOOR SEMANTICS: when this is true AND RequiredAmount is non-empty, the two fields are BOTH meaningful together: RequiredAmount is the sum of only the literal contributing legs — a known LOWER BOUND on the true requirement, not the total (the runtime-determined leg's own contribution is never invented). Treat RequiredAmount as "at least X" in this state. isInsufficient MAY still be computed against this floor and MAY be true — a balance below the floor is a genuine shortfall. A balance at or above the floor is UNKNOWN sufficiency, not proven sufficient.

        - ` amountUnit `: type ` string ` — AmountUnit declares the unit of the amount when the analyzer KNOWS it, rather than inferring it from the string's shape. THE definition of the field; every other mention of it should point here rather than restate it.  	"raw"   — smallest units (wei-like). Either the constant-fold said so, having 	          computed the integer itself, or the action's payload schema annotates 	          the field `token-amount` (amount_units.go). 	"human" — whole tokens. The same schema, read the other way: an amount field 	          the action does NOT annotate is whole tokens. 	""      — unknown. Consumers MUST print the value verbatim and MUST NOT rescale 	          it by decimals.  Three things produce the unknown, and a consumer cannot tell them apart:  	1. The field is not in the action's schema at all, so there was nothing to read 	   (schemaAmountUnit, amount_units.go). 	2. The schema declares the field raw, but the literal holds a decimal point and 	   so cannot BE a raw integer. A contradiction on a money path is not resolved 	   in either direction (ResolveLeg, leg_resolver.go). 	3. Two requirements merged whose declared units disagreed, so the sum belongs to 	   neither (mergeRequirement, workflow_resolver.go).  This said "human" would be a guess in a new coat, on the grounds that action payloads carry no unit metadata. For the Solana write actions they do: the schema states the unit and a sibling field holds the raw form. Left as a guess, the shape rule read a whole-token "2" as 2 lamports.

        - ` balance `: type ` object `

          - ` amount `: type ` string ` — raw units (wei)

          - ` amountIsPartial `: type ` boolean ` — AmountIsPartial mirrors funds.Requirement.AmountIsPartial: true when RequiredAmount understates the true total for a reason OTHER than a runtime expression — a dropped operand on a unit mismatch, or a per-iteration amount inside a loop. Same FLOOR semantics as AmountRuntimeDetermined (the amount is a known lower bound, and IsInsufficient=true against it is a genuine shortfall), but unlike that flag it does not claim a known source or later discovery point for the remainder.

          - ` amountRuntimeDetermined `: type ` boolean ` — AmountRuntimeDetermined mirrors funds.Requirement.AmountRuntimeDetermined: true when at least one contributing leg's amount is known only when the workflow runs. Usually that means a runtime expression could not be resolved statically; action metadata may also declare this for operations whose executor reads the complete amount at run time, such as all-balance account closes.  When this is true and the requirement has no known amount at all, IsInsufficient stays false (nothing to compare against — "unknown", not "known and sufficient").  When this is true AND a RequiredAmount IS present (merged with a literal-amount leg), that amount is only a known FLOOR — the literal legs' sum, not the true total — and IsInsufficient IS computed against it. IsInsufficient=true there is a genuine shortfall. But IsInsufficient=false is still NOT proof of sufficiency: it only means the balance clears the known floor, not the (partially unknown) total. Consumers must key off AmountRuntimeDetermined AND AmountIsPartial, not IsInsufficient alone, to know whether "sufficient" can be asserted — both flags carry the identical FLOOR semantics described above.

          - ` amountUsd `: type ` number `

          - ` chainId `: type ` integer `

          - ` decimals `: type ` integer `

          - ` isInsufficient `: type ` boolean ` — amount < requiredAmount

          - ` isLow `: type ` boolean ` — gas only: balance below LowGasThreshold

          - ` symbol `: type ` string `

          - ` tokenAddress `: type ` string `

          - ` walletAddress `: type ` string `

        - ` chainId `: type ` integer `

        - ` chainName `: type ` string `

        - ` decimals `: type ` integer `

        - ` isNative `: type ` boolean `

        - ` kind `: type ` string ` — Kind is the declaring action's `fundsMovement.kind` — how the leg moves funds ("send", "swap", "bridge", …), so consumers can phrase a swap as a spend rather than a transfer. Set on `fundsMovement.sent` legs only; empty on gas / requiredTokens rows and on parse-error legs (kind lives in the JSON that failed to parse, so it is unknowable there). On a dedup merge it follows the node identity above: first leg wins.

          - enum: ` ["send","swap","bridge","disperse","receive"] `

        - ` nodeId `: type ` string `

        - ` nodeName `: type ` string `

        - ` nodeType `: type ` string `

        - ` reason `: type ` string `

        - ` receivedDisplay `: type ` object ` — ReceivedDisplay is DISPLAY-ONLY context describing what the SAME node receives, attached only when this sent leg has no RequiredAmount of its own. See the ReceivedLegDisplay doc for the boundary it must not cross.

          - ` amount `: type ` string ` — Amount is optional. When present it is a resolved literal or exact constant-fold; when absent this carries identity-only display context.

          - ` amountUnit `: type ` string ` — AmountUnit is Requirement.AmountUnit on the wire: "raw" for smallest units, "human" for whole tokens, "" when the analyzer could not vouch for either. Consumers MUST NOT rescale an unknown unit by decimals. Requirement.AmountUnit carries the full rule and the three ways it comes back unknown; this is deliberately a pointer rather than a copy of it.

          - ` chainId `: type ` integer `

          - ` chainName `: type ` string `

          - ` decimals `: type ` integer `

          - ` isHumanReadable `: type ` boolean ` — IsHumanReadable mirrors Requirement.AmountIsHumanReadable for Amount: true for values like "0.5", false for raw smallest-unit integers.

          - ` symbol `: type ` string `

          - ` symbolUnverified `: type ` boolean `

          - ` tokenAddress `: type ` string `

        - ` requiredAmount `: type ` string `

        - ` source `: type ` string `

          - enum: ` ["requiredTokens","fundsMovement.sent","fundsMovement.received","requiresGas"] `

        - ` symbol `: type ` string `

        - ` symbolUnverified `: type ` boolean ` — SymbolUnverified is true only when Symbol came from an unverified on-chain `symbol()` read (AnalyzeFundsService's fallback pass for the UnknownSymbol sentinel), not from a curated registry or tokens.b3.fun. Callers MUST render it alongside the contract address + a ⚠ — an unverified read can return an attacker-chosen string (e.g. a scam token impersonating a known ticker).

        - ` tokenAddress `: type ` string `

        - ` unresolved `: type ` boolean `

        - ` unresolvedReason `: type ` string `

          - enum: ` ["chainId","tokenAddress","parseError","template"] `

        - ` walletAddress `: type ` string `

    - ` unresolved `: type ` array `

      - ` array item `: type ` object `

        - ` amountIsHumanReadable `: type ` boolean ` — AmountIsHumanReadable is AmountUnit == "human", restated for the wire. DERIVED — set only by deriveWireFields, never by a producer. It reports false for "raw" AND for an undeclared unit, which are different things, so anything rescaling by decimals must read AmountUnit instead.

        - ` amountIsPartial `: type ` boolean ` — AmountIsPartial means RequiredAmount understates the true total for a reason OTHER than a runtime expression: an operand was dropped, or the amount is per-iteration inside a loop. Floor semantics apply (the number is a lower bound), but this flag does not claim a source or later discovery point for the remainder.

        - ` amountRuntimeDetermined `: type ` boolean ` — AmountRuntimeDetermined is true when the leg's amount is known only when the workflow runs. Usually that means the amount is wired to a runtime expression ({{...}}) rather than a literal and could not be constant-folded; it can also be declared by action metadata when the action itself reads the complete amount at execution time, such as an all-balance close. The leg's IDENTITY (chain + token) is fully resolved — only the SIZE is unknown until the workflow runs. Distinct from Unresolved, which means the identity itself could not be determined.  When multiple legs sharing the same dedup key (wallet + chain + token) are merged (addOrMerge), this flag is true if ANY contributing leg was runtime-determined — even when another contributing leg supplied a literal amount. It is never cleared just because a literal amount is also present.  FLOOR SEMANTICS: when this is true AND RequiredAmount is non-empty, the two fields are BOTH meaningful together: RequiredAmount is the sum of only the literal contributing legs — a known LOWER BOUND on the true requirement, not the total (the runtime-determined leg's own contribution is never invented). Treat RequiredAmount as "at least X" in this state. isInsufficient MAY still be computed against this floor and MAY be true — a balance below the floor is a genuine shortfall. A balance at or above the floor is UNKNOWN sufficiency, not proven sufficient.

        - ` amountUnit `: type ` string ` — AmountUnit declares the unit of the amount when the analyzer KNOWS it, rather than inferring it from the string's shape. THE definition of the field; every other mention of it should point here rather than restate it.  	"raw"   — smallest units (wei-like). Either the constant-fold said so, having 	          computed the integer itself, or the action's payload schema annotates 	          the field `token-amount` (amount_units.go). 	"human" — whole tokens. The same schema, read the other way: an amount field 	          the action does NOT annotate is whole tokens. 	""      — unknown. Consumers MUST print the value verbatim and MUST NOT rescale 	          it by decimals.  Three things produce the unknown, and a consumer cannot tell them apart:  	1. The field is not in the action's schema at all, so there was nothing to read 	   (schemaAmountUnit, amount_units.go). 	2. The schema declares the field raw, but the literal holds a decimal point and 	   so cannot BE a raw integer. A contradiction on a money path is not resolved 	   in either direction (ResolveLeg, leg_resolver.go). 	3. Two requirements merged whose declared units disagreed, so the sum belongs to 	   neither (mergeRequirement, workflow_resolver.go).  This said "human" would be a guess in a new coat, on the grounds that action payloads carry no unit metadata. For the Solana write actions they do: the schema states the unit and a sibling field holds the raw form. Left as a guess, the shape rule read a whole-token "2" as 2 lamports.

        - ` chainId `: type ` integer `

        - ` chainName `: type ` string `

        - ` decimals `: type ` integer `

        - ` isNative `: type ` boolean `

        - ` kind `: type ` string ` — Kind is the declaring action's `fundsMovement.kind` — how the leg moves funds ("send", "swap", "bridge", …), so consumers can phrase a swap as a spend rather than a transfer. Set on `fundsMovement.sent` legs only; empty on gas / requiredTokens rows and on parse-error legs (kind lives in the JSON that failed to parse, so it is unknowable there). On a dedup merge it follows the node identity above: first leg wins.

          - enum: ` ["send","swap","bridge","disperse","receive"] `

        - ` nodeId `: type ` string `

        - ` nodeName `: type ` string `

        - ` nodeType `: type ` string `

        - ` reason `: type ` string `

        - ` receivedDisplay `: type ` object ` — ReceivedDisplay is DISPLAY-ONLY context describing what the SAME node receives, attached only when this sent leg has no RequiredAmount of its own. See the ReceivedLegDisplay doc for the boundary it must not cross.

          - ` amount `: type ` string ` — Amount is optional. When present it is a resolved literal or exact constant-fold; when absent this carries identity-only display context.

          - ` amountUnit `: type ` string ` — AmountUnit is Requirement.AmountUnit on the wire: "raw" for smallest units, "human" for whole tokens, "" when the analyzer could not vouch for either. Consumers MUST NOT rescale an unknown unit by decimals. Requirement.AmountUnit carries the full rule and the three ways it comes back unknown; this is deliberately a pointer rather than a copy of it.

          - ` chainId `: type ` integer `

          - ` chainName `: type ` string `

          - ` decimals `: type ` integer `

          - ` isHumanReadable `: type ` boolean ` — IsHumanReadable mirrors Requirement.AmountIsHumanReadable for Amount: true for values like "0.5", false for raw smallest-unit integers.

          - ` symbol `: type ` string `

          - ` symbolUnverified `: type ` boolean `

          - ` tokenAddress `: type ` string `

        - ` requiredAmount `: type ` string `

        - ` source `: type ` string `

          - enum: ` ["requiredTokens","fundsMovement.sent","fundsMovement.received","requiresGas"] `

        - ` symbol `: type ` string `

        - ` symbolUnverified `: type ` boolean ` — SymbolUnverified is true only when Symbol came from an unverified on-chain `symbol()` read (AnalyzeFundsService's fallback pass for the UnknownSymbol sentinel), not from a curated registry or tokens.b3.fun. Callers MUST render it alongside the contract address + a ⚠ — an unverified read can return an attacker-chosen string (e.g. a scam token impersonating a known ticker).

        - ` tokenAddress `: type ` string `

        - ` unresolved `: type ` boolean `

        - ` unresolvedReason `: type ` string `

          - enum: ` ["chainId","tokenAddress","parseError","template"] `

        - ` walletAddress `: type ` string `

    - ` workflowId `: type ` string `

    - ` workflowVersion `: type ` integer `

  - ` message `: type ` string `

    - example: ` "success" `

  - ` requestId `: type ` string `

    - example: ` "abc-123" `

### ` 400 ` — Bad request

Content type: ` application/json `

- ` value `: type ` object `

  - ` code `: type ` integer `

  - ` details `: type ` array `

    - ` array item `: type ` unknown `

  - ` message `: type ` string `

  - ` requestId `: type ` string `

### ` 404 ` — Workflow not found

Content type: ` application/json `

- ` value `: type ` object `

  - ` code `: type ` integer `

  - ` details `: type ` array `

    - ` array item `: type ` unknown `

  - ` message `: type ` string `

  - ` requestId `: type ` string `

## Request examples

### cURL

```curl
curl -X GET 'https://api.example.com/v1/workflows/string/analyze-funds' \
  -H 'Authorization: Bearer YOUR_API_TOKEN'
```

### JavaScript

```javascript
const response = await fetch('https://api.example.com/v1/workflows/string/analyze-funds', {
  method: 'GET',
  headers: {
      "Authorization": "Bearer YOUR_API_TOKEN"
  }
});

const data = await response.json();
console.log(data);
```

### Python

```python
import requests

headers = {
    'Authorization': 'Bearer YOUR_API_TOKEN'
}

response = requests.get('https://api.example.com/v1/workflows/string/analyze-funds', headers=headers)
print(response.json())
```

### Go

```go
package main

import (
	"fmt"
	"io"
	"net/http"
)

func main() {
	req, _ := http.NewRequest("GET", "https://api.example.com/v1/workflows/string/analyze-funds", nil)
	req.Header.Set("Authorization", "Bearer YOUR_API_TOKEN")

	resp, _ := http.DefaultClient.Do(req)
	defer resp.Body.Close()
	result, _ := io.ReadAll(resp.Body)
	fmt.Println(string(result))
}
```