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.
Path Parameters
id
string
required
path
Workflow ID
Query Parameters
version
integer
optional
query
Pin analysis to a specific workflow version
Responses
200
OK
application/jsoncode
integer
data
object
balancesFetched
boolean
requirements
object[]
Array of:
amountIsHumanReadable
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
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
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
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
object
amount
string
raw units (wei)
amountIsPartial
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
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
number
chainId
integer
decimals
integer
isInsufficient
boolean
amount < requiredAmount
isLow
boolean
gas only: balance below LowGasThreshold
symbol
string
tokenAddress
string
walletAddress
string
chainId
integer
chainName
string
decimals
integer
isNative
boolean
kind
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, receivenodeId
string
nodeName
string
nodeType
string
reason
string
receivedDisplay
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
string
Amount is optional. When present it is a resolved literal or exact
constant-fold; when absent this carries identity-only display context.
amountUnit
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
integer
chainName
string
decimals
integer
isHumanReadable
boolean
IsHumanReadable mirrors Requirement.AmountIsHumanReadable for Amount:
true for values like "0.5", false for raw smallest-unit integers.
symbol
string
symbolUnverified
boolean
tokenAddress
string
requiredAmount
string
source
string
Enum:
requiredTokens, fundsMovement.sent, fundsMovement.received, requiresGassymbol
string
symbolUnverified
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
string
unresolved
boolean
unresolvedReason
string
Enum:
chainId, tokenAddress, parseError, templatewalletAddress
string
unresolved
object[]
Array of:
amountIsHumanReadable
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
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
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
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
integer
chainName
string
decimals
integer
isNative
boolean
kind
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, receivenodeId
string
nodeName
string
nodeType
string
reason
string
receivedDisplay
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
string
Amount is optional. When present it is a resolved literal or exact
constant-fold; when absent this carries identity-only display context.
amountUnit
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
integer
chainName
string
decimals
integer
isHumanReadable
boolean
IsHumanReadable mirrors Requirement.AmountIsHumanReadable for Amount:
true for values like "0.5", false for raw smallest-unit integers.
symbol
string
symbolUnverified
boolean
tokenAddress
string
requiredAmount
string
source
string
Enum:
requiredTokens, fundsMovement.sent, fundsMovement.received, requiresGassymbol
string
symbolUnverified
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
string
unresolved
boolean
unresolvedReason
string
Enum:
chainId, tokenAddress, parseError, templatewalletAddress
string
workflowId
string
workflowVersion
integer
message
string
requestId
string
400
Bad request
404
Workflow not found
curl -X GET 'https://api.example.com/v1/workflows/string/analyze-funds' \ -H 'Authorization: Bearer YOUR_API_TOKEN'
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);
import requestsheaders = { 'Authorization': 'Bearer YOUR_API_TOKEN'}response = requests.get('https://api.example.com/v1/workflows/string/analyze-funds', headers=headers)print(response.json())
package mainimport ( "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))}
200
Response
{ "code": 200, "data": { "balancesFetched": true, "requirements": [ { "amountIsHumanReadable": true, "amountIsPartial": true, "amountRuntimeDetermined": true, "amountUnit": "<string>", "balance": { "amount": "<string>", "amountIsPartial": true, "amountRuntimeDetermined": true, "amountUsd": 123, "chainId": 123, "decimals": 123, "isInsufficient": true, "isLow": true, "symbol": "<string>", "tokenAddress": "<string>", "walletAddress": "<string>" }, "chainId": 123, "chainName": "<string>", "decimals": 123, "isNative": true, "kind": "send", "nodeId": "<string>", "nodeName": "<string>", "nodeType": "<string>", "reason": "<string>", "receivedDisplay": { "amount": "<string>", "amountUnit": "<string>", "chainId": 123, "chainName": "<string>", "decimals": 123, "isHumanReadable": true, "symbol": "<string>", "symbolUnverified": true, "tokenAddress": "<string>" }, "requiredAmount": "<string>", "source": "requiredTokens", "symbol": "<string>", "symbolUnverified": true, "tokenAddress": "<string>", "unresolved": true, "unresolvedReason": "chainId", "walletAddress": "<string>" } ], "unresolved": [ { "amountIsHumanReadable": true, "amountIsPartial": true, "amountRuntimeDetermined": true, "amountUnit": "<string>", "chainId": 123, "chainName": "<string>", "decimals": 123, "isNative": true, "kind": "send", "nodeId": "<string>", "nodeName": "<string>", "nodeType": "<string>", "reason": "<string>", "receivedDisplay": { "amount": "<string>", "amountUnit": "<string>", "chainId": 123, "chainName": "<string>", "decimals": 123, "isHumanReadable": true, "symbol": "<string>", "symbolUnverified": true, "tokenAddress": "<string>" }, "requiredAmount": "<string>", "source": "requiredTokens", "symbol": "<string>", "symbolUnverified": true, "tokenAddress": "<string>", "unresolved": true, "unresolvedReason": "chainId", "walletAddress": "<string>" } ], "workflowId": "<string>", "workflowVersion": 123 }, "message": "success", "requestId": "abc-123"}
API Playground
Try this endpoint
GET
/v1/workflows/{id}/analyze-funds
