caddie-mcp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,152 @@
1
+ # B3OS MCP Server
2
+
3
+ MCP server that connects Claude to [B3OS](https://b3os.org), a workflow automation platform for blockchain operations. Build, run, and debug automated workflows through natural conversation.
4
+
5
+ **37 tools** | **562+ actions** | Works with Claude Code, Claude Desktop, and co-work sessions
6
+
7
+ ## Quick Start
8
+
9
+ ### 1. Get an API key
10
+
11
+ Create one at [b3os.org/organizations/settings?tab=api-keys](https://b3os.org/organizations/settings?tab=api-keys) (starts with `b3sk_`).
12
+
13
+ ### 2. Install
14
+
15
+ ```bash
16
+ npm install -g caddie-mcp
17
+ ```
18
+
19
+ <details>
20
+ <summary>Install from source</summary>
21
+
22
+ ```bash
23
+ cd packages/b3os-mcp
24
+ pnpm install && pnpm run build
25
+ ```
26
+ </details>
27
+
28
+ ### 3. Connect to Claude
29
+
30
+ **Interactive setup (recommended):**
31
+
32
+ ```bash
33
+ b3os-mcp-setup
34
+ ```
35
+
36
+ This validates your API key and configures Claude Code (`~/.claude/mcp.json`), project-level `.mcp.json` (if gitignored), and Claude Desktop automatically.
37
+
38
+ #### Reducing permission prompts
39
+
40
+ At the end of setup, you'll be asked whether to pre-approve ~26 read-only b3os-mcp tools (list/get/lookup/debug — no create/update/delete/run). This writes `mcp__b3os-mcp__*` entries to `~/.claude/settings.json` under `permissions.allow`, letting Caddie iterate without constant approval prompts.
41
+
42
+ Write tools (`create_workflow`, `run_action`, `query_database`, etc.) always require approval per call.
43
+
44
+ **To revert:** delete the `mcp__b3os-mcp__*` entries from `permissions.allow` in `~/.claude/settings.json`.
45
+
46
+ <details>
47
+ <summary>Manual setup</summary>
48
+
49
+ **Claude Code** -- add to `.mcp.json` in your project root (or `~/.claude/mcp.json` for global):
50
+
51
+ ```json
52
+ {
53
+ "mcpServers": {
54
+ "b3os": {
55
+ "type": "stdio",
56
+ "command": "b3os-mcp",
57
+ "args": [],
58
+ "env": {
59
+ "B3OS_API_KEY": "b3sk_your_key_here",
60
+ "B3OS_SERVER_URL": "https://api.b3os.org"
61
+ }
62
+ }
63
+ }
64
+ }
65
+ ```
66
+
67
+ **Claude Desktop** -- edit `~/Library/Application Support/Claude/claude_desktop_config.json`:
68
+
69
+ ```json
70
+ {
71
+ "mcpServers": {
72
+ "b3os": {
73
+ "command": "b3os-mcp",
74
+ "env": { "B3OS_API_KEY": "b3sk_your_key_here" }
75
+ }
76
+ }
77
+ }
78
+ ```
79
+
80
+ > **nvm/fnm users:** If `b3os-mcp` isn't resolved, replace `"command": "b3os-mcp"` with the absolute node path. Run `which node` to find it. The interactive setup handles this automatically.
81
+
82
+ </details>
83
+
84
+ ### 4. Restart Claude
85
+
86
+ Restart your Claude Code session or Claude Desktop app.
87
+
88
+ ## What You Can Do
89
+
90
+ **Build workflows** -- Claude delegates to Caddie, the B3OS AI agent with deep domain knowledge:
91
+ ```
92
+ "Build a workflow that monitors ETH price every 5 min and alerts on Slack if it drops below $2000"
93
+ ```
94
+
95
+ **Debug failed runs** -- Caddie analyzes execution state and suggests fixes:
96
+ ```
97
+ "Why did my last workflow run fail?"
98
+ ```
99
+
100
+ **Query on-chain data** -- named lookup tools, no workflow needed:
101
+ ```
102
+ "What's the current price of ETH and BTC?"
103
+ "What tokens does 0xd8dA...6045 hold on Base?"
104
+ "Show me prediction markets about Bitcoin on Polymarket"
105
+ ```
106
+
107
+ **Execute actions** -- run any of the 562+ actions directly:
108
+ ```
109
+ "Debug tx 0xabc... on Base"
110
+ "Get DeFi positions for vitalik.eth"
111
+ ```
112
+
113
+ ## Tools
114
+
115
+ | Group | Tools | Description |
116
+ |-------|-------|-------------|
117
+ | **Caddie** | `build_workflow`, `debug_run` | AI-powered workflow building and run debugging |
118
+ | **Lookups** | `token_lookup`, `price_lookup`, `balance_lookup`, `defi_lookup`, `debug_transaction`, `polymarket_lookup` | Named tools for common data queries |
119
+ | **Catalog** | `search_actions`, `list_actions`, `get_action`, `list_triggers`, `get_trigger` | Browse 562+ actions and triggers |
120
+ | **Execution** | `query_action`, `run_action`, `run_workflow`, `run_ephemeral` | Execute actions and workflows |
121
+ | **Workflows** | `create_workflow`, `get_workflow`, `list_workflows`, `update_workflow`, `delete_workflow`, `publish_workflow`, `pause_workflow`, `resume_workflow`, `validate_workflow` | Full workflow lifecycle |
122
+ | **Runs** | `list_runs`, `get_run`, `cancel_run` | Inspect and manage workflow runs |
123
+ | **Org** | `whoami`, `list_wallets`, `list_connectors`, `list_slack_channels`, `list_telegram_chats` | Organization context and connected services |
124
+ | **Database** | `query_database`, `list_tables`, `get_table_schema` | Query org's Cloudflare D1 database |
125
+
126
+ All tools are prefixed with `b3os_` (e.g. `b3os_build_workflow`).
127
+
128
+ ## Environment Variables
129
+
130
+ | Variable | Required | Default | Description |
131
+ |----------|----------|---------|-------------|
132
+ | `B3OS_API_KEY` | Yes | -- | API key from B3OS dashboard |
133
+ | `B3OS_SERVER_URL` | No | `https://api.b3os.org` | Override for local dev (`http://localhost:8080`) |
134
+
135
+ ## Development
136
+
137
+ ```bash
138
+ pnpm run build # compile TypeScript
139
+ pnpm test # run tests
140
+
141
+ # Dev mode (no build step) -- in .mcp.json use:
142
+ # "command": "pnpm", "args": ["exec", "tsx", "src/index.ts"]
143
+ ```
144
+
145
+ ## Architecture
146
+
147
+ ```
148
+ Claude (Code / Desktop / co-work)
149
+ |-- REST tools ----> B3OS API (workflows, runs, catalog, lookups, database)
150
+ |-- SSE stream ----> Caddie AI agent (build_workflow, debug_run)
151
+ +-- Resources -----> Embedded guide (no API call)
152
+ ```
@@ -0,0 +1,26 @@
1
+ export declare function getApiKey(): string;
2
+ export declare function getServerUrl(): string;
3
+ export declare class ApiError extends Error {
4
+ status: number;
5
+ statusText: string;
6
+ body: string;
7
+ constructor(status: number, statusText: string, body: string);
8
+ }
9
+ interface RequestOptions {
10
+ method?: string;
11
+ body?: unknown;
12
+ params?: Record<string, string>;
13
+ noAuth?: boolean;
14
+ timeout?: number;
15
+ }
16
+ export declare function request<T = unknown>(path: string, options?: RequestOptions): Promise<T>;
17
+ /** Max bytes for a tool response text before truncation. */
18
+ export declare const MAX_RESPONSE_BYTES = 50000;
19
+ /** Truncate a tool response string if it exceeds the byte limit. */
20
+ export declare function truncateResponse(text: string, maxBytes?: number): string;
21
+ /**
22
+ * Parse a JSON string parameter from MCP clients that serialize objects as strings.
23
+ * Returns the value as-is if it's not a string. Throws a descriptive error on invalid JSON.
24
+ */
25
+ export declare function parseJsonParam(value: unknown, paramName: string): unknown;
26
+ export {};
package/dist/client.js ADDED
@@ -0,0 +1,134 @@
1
+ export function getApiKey() {
2
+ const key = process.env.B3OS_API_KEY;
3
+ if (!key) {
4
+ throw new Error("B3OS_API_KEY is required. Set it in your MCP server config or run: pnpm setup");
5
+ }
6
+ return key;
7
+ }
8
+ export function getServerUrl() {
9
+ const url = (process.env.B3OS_SERVER_URL || "https://api.b3os.org").replace(/\/+$/, "");
10
+ if (!/^https:\/\//i.test(url) && process.env.NODE_ENV !== "development") {
11
+ throw new Error("B3OS_SERVER_URL must use HTTPS");
12
+ }
13
+ return url;
14
+ }
15
+ function friendlyHint(status) {
16
+ switch (status) {
17
+ case 401:
18
+ return "Your API key may be invalid or expired. Regenerate it at https://b3os.org/organizations/settings?tab=api-keys";
19
+ case 403:
20
+ return "Insufficient permissions. Check that your API key has read-write scope.";
21
+ case 404:
22
+ return "Resource not found. Verify the ID is correct.";
23
+ case 409:
24
+ return "Conflict — another update may have occurred. Retry with the latest version.";
25
+ case 429:
26
+ return "Rate limited. Wait a moment and try again.";
27
+ default:
28
+ return "";
29
+ }
30
+ }
31
+ const MAX_ERROR_BODY_LENGTH = 500;
32
+ function sanitizeBody(body) {
33
+ if (!body)
34
+ return "";
35
+ // Detect HTML error pages (502/503 Cloudflare, nginx) and replace with
36
+ // a clean message. Uses a single regex to match <!DOCTYPE or <html at
37
+ // the start, avoiding false-positives on XML/SOAP responses.
38
+ if (/^\s*<(!DOCTYPE|html)/i.test(body)) {
39
+ return "(HTML error page — server may be temporarily unavailable)";
40
+ }
41
+ // Truncate long error bodies to avoid leaking internal details
42
+ if (body.length > MAX_ERROR_BODY_LENGTH) {
43
+ return body.slice(0, MAX_ERROR_BODY_LENGTH) + "…(truncated)";
44
+ }
45
+ return body;
46
+ }
47
+ export class ApiError extends Error {
48
+ status;
49
+ statusText;
50
+ body;
51
+ constructor(status, statusText, body) {
52
+ const hint = friendlyHint(status);
53
+ const cleanBody = sanitizeBody(body);
54
+ super(`API error ${status}: ${statusText}${hint ? `\nHint: ${hint}` : ""}${cleanBody ? `\n${cleanBody}` : ""}`);
55
+ this.status = status;
56
+ this.statusText = statusText;
57
+ this.body = body;
58
+ this.name = "ApiError";
59
+ }
60
+ }
61
+ function buildUrl(path, params) {
62
+ const url = new URL(`${getServerUrl()}${path}`);
63
+ if (params) {
64
+ for (const [key, value] of Object.entries(params)) {
65
+ if (value)
66
+ url.searchParams.set(key, value);
67
+ }
68
+ }
69
+ return url.toString();
70
+ }
71
+ function buildHeaders(noAuth) {
72
+ const headers = { "Content-Type": "application/json" };
73
+ if (!noAuth) {
74
+ headers["Authorization"] = `Bearer ${getApiKey()}`;
75
+ }
76
+ return headers;
77
+ }
78
+ export async function request(path, options = {}) {
79
+ const url = buildUrl(path, options.params);
80
+ const controller = new AbortController();
81
+ const timeoutMs = options.timeout ?? 30_000;
82
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
83
+ try {
84
+ const response = await fetch(url, {
85
+ method: options.method || "GET",
86
+ headers: buildHeaders(options.noAuth),
87
+ body: options.body ? JSON.stringify(options.body) : undefined,
88
+ signal: controller.signal,
89
+ });
90
+ if (!response.ok) {
91
+ const body = await response.text();
92
+ throw new ApiError(response.status, response.statusText, body);
93
+ }
94
+ const text = await response.text();
95
+ if (!text)
96
+ return undefined;
97
+ const parsed = JSON.parse(text);
98
+ return parsed.data;
99
+ }
100
+ catch (err) {
101
+ if (err instanceof ApiError)
102
+ throw err;
103
+ if (err instanceof Error && err.name === "AbortError") {
104
+ throw new Error(`Request timed out after ${timeoutMs / 1000} seconds`);
105
+ }
106
+ throw err;
107
+ }
108
+ finally {
109
+ clearTimeout(timeout);
110
+ }
111
+ }
112
+ /** Max bytes for a tool response text before truncation. */
113
+ export const MAX_RESPONSE_BYTES = 50_000;
114
+ /** Truncate a tool response string if it exceeds the byte limit. */
115
+ export function truncateResponse(text, maxBytes = MAX_RESPONSE_BYTES) {
116
+ const buf = Buffer.from(text, "utf8");
117
+ if (buf.length <= maxBytes)
118
+ return text;
119
+ return buf.subarray(0, maxBytes).toString("utf8") + `\n…(truncated, ${buf.length} bytes total)`;
120
+ }
121
+ /**
122
+ * Parse a JSON string parameter from MCP clients that serialize objects as strings.
123
+ * Returns the value as-is if it's not a string. Throws a descriptive error on invalid JSON.
124
+ */
125
+ export function parseJsonParam(value, paramName) {
126
+ if (typeof value !== "string")
127
+ return value;
128
+ try {
129
+ return JSON.parse(value);
130
+ }
131
+ catch (e) {
132
+ throw new Error(`Invalid JSON string for parameter '${paramName}': ${e.message}`);
133
+ }
134
+ }
@@ -0,0 +1,2 @@
1
+ export declare const GUIDE_URI = "b3os://guide";
2
+ export declare const GUIDE_CONTENT = "# B3OS Platform Guide\n\nB3OS is a workflow automation platform for blockchain operations. Users create\nautomated workflows triggered by blockchain events, schedules, webhooks, or\nmanual execution. Workflows combine 560+ actions across DeFi, social, data,\nand on-chain operations.\n\n---\n\n## Workflow Definition Anatomy\n\nA workflow definition is a JSON object with a single `nodes` map. Each key is a\nnode ID, and the value describes that node's type, configuration, and connections.\n\n```json\n{\n \"nodes\": {\n \"root\": {\n \"type\": \"cronjob\",\n \"payload\": { \"rrule\": \"DTSTART:20260101T090000Z\\nRRULE:FREQ=MINUTELY;INTERVAL=5\" },\n \"children\": [\"fetch_price\"]\n },\n \"fetch_price\": {\n \"type\": \"coingecko-get-token-price\",\n \"payload\": { \"coinId\": \"ethereum\" },\n \"children\": [\"check_drop\"]\n },\n \"check_drop\": {\n \"type\": \"if\",\n \"payload\": {\n \"condition\": { \"$and\": [{ \"{{fetch_price.result.price}}\": { \"$lte\": 2000 } }] }\n },\n \"children\": [\"send_alert\", \"do_nothing\"]\n },\n \"send_alert\": {\n \"type\": \"slack-send-message\",\n \"branch\": \"then\",\n \"payload\": {\n \"conversation\": \"{{ask.slack_channel}}\",\n \"text\": \"ETH dropped to ${{fetch_price.result.price}}\"\n },\n \"connector\": { \"type\": \"slack\" },\n \"children\": []\n },\n \"do_nothing\": {\n \"type\": \"log\",\n \"branch\": \"else\",\n \"payload\": { \"message\": \"Price OK: {{fetch_price.result.price}}\" },\n \"children\": []\n }\n }\n}\n```\n\n### Key Structural Rules\n\n- **`root` is always the trigger node.** The workflow engine starts execution from `root`.\n- **`children`** is an ordered array of node IDs that execute after the parent completes.\n- **`branch`** is required on children of `if` nodes (`\"then\"` or `\"else\"`) and `wait` nodes (`\"resumed\"` or `\"timed_out\"`).\n- **`payload`** contains the node's configuration. Supports `{{}}` template expressions.\n- **`connector`** references an OAuth credential: `{ \"type\": \"slack\" }` or `{ \"type\": \"slack\", \"id\": \"conn_abc\" }`. Only needed for actions that interact with external services.\n- **`loopBody`** (on `for-each` nodes) is an array of node IDs that run per iteration \u2014 these go in `loopBody`, not `children`.\n- **`description`** and **`titleOverride`** are optional display labels.\n\n---\n\n## Expression Syntax Reference\n\nTemplate expressions use `{{ }}` syntax and are resolved at runtime.\n\n| Pattern | Example | Resolves To |\n|---------|---------|-------------|\n| Node result | `{{fetch_data.result.items}}` | Output field from an upstream node |\n| Nested path | `{{fetch_data.result.data.price}}` | Nested field access |\n| Node payload | `{{root.payload.chainId}}` | Config value from another node |\n| Trigger input | `{{root.result.body.userId}}` | Webhook body data |\n| User input (ASK) | `{{ask.wallet_address}}` | Creates a UI widget for user input at setup time |\n| Props | `{{$props.chainId}}` | Workflow properties, inlined at creation time |\n| Block inputs | `{{$inputs.coinId}}` | Inputs passed into a reusable block |\n| Loop item | `{{$item.name}}` | Current for-each iteration item |\n| Loop index | `{{$index}}` | Current for-each iteration index (0-based) |\n| Parent loop | `{{$parent_item}}` | Parent for-each item (nested loops) |\n| Ancestor loop | `{{$for.outer_loop.item}}` | Named ancestor for-each (3+ nesting) |\n| Workflow ID | `{{$workflowId}}` | Current workflow's ID |\n| Fallback | `{{node.result.count \\|\\| 0}}` | Use 0 if the variable is nil |\n\n**Important:** Expressions can only reference ancestor nodes \u2014 nodes that have already\nexecuted before the current node in the DAG. You cannot reference sibling or descendant nodes.\n\n---\n\n## Node Types Quick Reference\n\n### Triggers (always the `root` node)\n\n**Core triggers:**\n\n| Type | Description | Key Payload Fields |\n|------|-------------|-------------------|\n| `cronjob` | Schedule-based | `rrule` (two-line DTSTART+RRULE string) |\n| `manual` | User clicks \"Run\" or webhook POST | (none \u2014 also serves as webhook endpoint) |\n| `erc20-receive` | ERC-20 token received | `chainId`, `tokenAddress`, `walletAddress` |\n| `erc20-send` | ERC-20 token sent | `chainId`, `tokenAddress`, `walletAddress` |\n| `eth-receive` | Native ETH received | `chainId`, `walletAddress`, `minAmount` (wei) |\n| `eth-send` | Native ETH sent | `chainId`, `walletAddress` |\n| `evm-log` | Smart contract event | `chainId`, `contractAddress`, `eventName`, `abi` |\n| `token-price-cexes` | Real-time price monitor | `asset`, `condition`, `threshold` |\n| `solana-transaction` | Solana tx event | `address`, `network` |\n\n**Polymarket triggers:**\n\n| Type | Description |\n|------|-------------|\n| `polymarket-user-bet` | A specific user places a bet |\n| `polymarket-market-trade` | Any trade on a market (CLOB WebSocket) |\n| `polymarket-new-market` | New market created |\n| `polymarket-market-close` | Market resolves |\n\n**Social/messaging triggers:**\n\n| Type | Description |\n|------|-------------|\n| `telegram-channel` | New Telegram message |\n| `slack-mentions` | Slack mention detection |\n| `slack-new-message-in-channels` | New message in Slack channel |\n| `farcaster-new-cast` | New Farcaster cast |\n| `x-new-tweet` | New tweet (requires username or keywords filter) |\n| `gmail-new-email-received` | Gmail inbox (OAuth) |\n| `email-new-email-received` | Disposable inbox |\n\n**Other triggers:**\n\n| Type | Description |\n|------|-------------|\n| `exchange-listing` | New trading pair on CEX (Binance, Coinbase, etc.) |\n| `stripe-payment-receive` | Stripe payment received |\n| `shopify-order-created` | Shopify order created |\n| `coinbase-payment-received` | Coinbase Commerce payment |\n\nUse `b3os_list_triggers` to see all available trigger types and their payload schemas.\n\n### Logic Nodes (built-in, no connector needed)\n\n| Type | Description | Key Fields |\n|------|-------------|------------|\n| `if` | Conditional branch | `payload.condition` (object with `$and`/`$or`). Children use `branch: \"then\"/\"else\"` |\n| `for-each` | Loop over array | `payload.array` (expression). Loop nodes go in `loopBody`, not `children` |\n| `filter` | Filter array | `payload.array`, `payload.condition` |\n| `pluck` | Extract fields | `payload.array`, `payload.fields` |\n| `delay` | Wait for duration | `payload.delayMs` (milliseconds) |\n| `log` | Log a message | `payload.message` (supports templates) |\n| `code-transform` | Run JavaScript | `payload.code` (must be `function transform(input) {...}`), `payload.input`, `payload.outputType` |\n| `regex` | Regex extraction | `payload.input`, `payload.pattern` |\n| `wait` | Pause until event | `payload.triggerType`, `payload.triggerConfig`, `payload.timeoutMs`. Children use `branch: \"resumed\"/\"timed_out\"` |\n| `send-webhook` | HTTP POST | `payload.url`, `payload.headers`, `payload.body` |\n\n### Dynamic Actions (560+ from catalog)\n\nUse `b3os_search_actions` to find actions by keyword, then `b3os_get_action` to get\nthe full payload and result schemas. Common examples:\n\n- **Token ops:** `send-erc20-token`, `send-native-token`, `approve-erc20`\n- **Swaps:** `0x-swap`, `cow-swap-gasless`, `relay-swap`, `swapkit-swap`\n- **DeFi:** `morpho-deposit`, `morpho-withdraw`, `v3-add-liquidity`\n- **Data:** `coingecko-get-token-price`, `coinglass-get-funding-rate`\n- **Messaging:** `slack-send-message`, `slack-send-block-kit-message`, `discord-send-message`, `telegram-send-message`\n- **Polymarket:** `polymarket-place-bet`, `polymarket-redeem`\n\n---\n\n## Connector Rules\n\nActions that interact with external services require a `connector` field:\n\n```json\n\"connector\": { \"type\": \"slack\" }\n```\n\nOr with a specific connector ID:\n\n```json\n\"connector\": { \"type\": \"slack\", \"id\": \"conn_abc123\" }\n```\n\n**Connector types:** `slack`, `discord`, `telegram`, `gmail`, `google_sheets`, `wallet` (HSM wallet), `db` (database).\n\n**Logic nodes NEVER need connectors:** `if`, `for-each`, `filter`, `pluck`, `delay`, `log`, `code-transform`, `regex`, `wait`, `send-webhook`.\n\nConnectors must be pre-configured in the B3OS UI before workflows can use them.\nIf the connector ID is unknown at build time, use `{{ask.connector_id}}` or leave\nonly the `type` field \u2014 the user will select the connector in the B3OS UI.\n\n---\n\n## If Condition Format\n\nThe `if` node uses a JSON condition object. The top-level key MUST be `$and` or `$or`:\n\n```json\n{\n \"condition\": {\n \"$and\": [\n { \"{{fetch_price.result.price}}\": { \"$lte\": 2000 } },\n { \"{{fetch_price.result.volume}}\": { \"$gte\": 1000000 } }\n ]\n }\n}\n```\n\n**Operators:** `$eq`, `$ne`, `$gt`, `$gte`, `$lt`, `$lte`, `$contains`, `$notContains`,\n`$startsWith`, `$endsWith`, `$exists`, `$notExists`, `$in`, `$notIn`.\n\n---\n\n## Common Workflow Patterns\n\n**Pattern A \u2014 Monitor + Alert:** `cronjob \u2192 fetch data \u2192 if condition \u2192 send notification`\nMost common pattern. Schedule a check, fetch external data, conditionally alert.\n\n**Pattern B \u2014 Webhook + Transform:** `webhook \u2192 code-transform \u2192 send-webhook / action`\nReceive external data, transform it, take action. Used for API integrations.\n\n**Pattern C \u2014 Trigger + Loop + Action:** `trigger \u2192 fetch list \u2192 for-each \u2192 action per item`\nBatch processing. Trigger fetches a list, loops over items, processes each.\n\n**Pattern D \u2014 Approval Flow:** `trigger \u2192 slack-send-block-kit-message \u2192 wait \u2192 if approve/reject \u2192 action`\nHuman-in-the-loop. Post approval buttons, wait for click, branch on decision.\n\n---\n\n## Numeric Conventions\n\n**On-chain amounts are strings in raw units (wei/atomic):**\n- 1 ETH = `\"1000000000000000000\"` (18 decimals)\n- 1 USDC = `\"1000000\"` (6 decimals)\n- 1 WBTC = `\"100000000\"` (8 decimals)\n\nNEVER pass human-readable decimals to send/swap/approve actions. Always convert\nto the token's raw unit string. Look up token decimals \u2014 never hardcode them.\n\n**Prices and display values are numbers:** `2000.50`, `0.65`, `1500000`\nThese are used in conditions and display, not on-chain transactions.\n\n---\n\n## ASK Widget Patterns\n\n`{{ask.fieldName}}` creates a user-input widget in the B3OS UI. Use it for values\nthe user should provide at setup time:\n\n```json\n\"walletAddress\": \"{{ask.wallet_address}}\",\n\"conversation\": \"{{ask.slack_channel}}\",\n\"threshold\": \"{{ask.price_threshold}}\"\n```\n\n**Rich widget types** are available for common fields. Use the matching field name\npattern and the B3OS UI will render the appropriate widget:\n- `wallet_address` \u2192 Wallet selector\n- `token_address` / `token_addresses` \u2192 Token contract picker (with chain)\n- `chain_id` / `chain_ids` \u2192 Network selector\n- `connector_id` \u2192 Connector account picker\n- `slack_channel` \u2192 Slack channel picker\n- `telegram_chat` \u2192 Telegram chat picker\n- `token_amount` \u2192 Token amount with decimals\n- `recipient_address` \u2192 Recipient wallet address\n- `contract_address` \u2192 Smart contract address\n- `network` / `networks` \u2192 Network selector (alternative)\n- `boolean`, `number`, `select`, `multiselect`, `textarea`, `date`, `color` \u2192 Basic types\n\n---\n\n## How to Use B3OS Tools\n\n### Building a Workflow\nUse `b3os_build_workflow` to delegate to Caddie, the B3OS AI agent. Caddie has\ndeep domain knowledge, address verification, and 30+ specialized tools.\n\n1. Gather prerequisites: `b3os_list_connectors`, `b3os_list_wallets` \u2192 present options\n2. Describe the workflow in natural language to `b3os_build_workflow`\n3. Review the returned definition with the user\n4. Save: `b3os_create_workflow` (new) or `b3os_update_workflow` (existing)\n5. Confirm with user \u2192 deploy: `b3os_publish_workflow`\n\nTo modify an existing workflow, pass its `workflowId` to `b3os_build_workflow`.\n\n### Debugging a Failure\nUse `b3os_debug_run` to delegate diagnosis to Caddie:\n\n1. `b3os_list_runs` (status: \"failure\") \u2192 find failed runs\n2. `b3os_debug_run` (runId) \u2192 Caddie diagnoses + suggests fixes\n3. Apply fix: `b3os_update_workflow` \u2192 `b3os_publish_workflow`\n\nFor manual inspection: `b3os_get_run` with nodeIds to see per-node input, result, and error.\n\n### Data Queries (no workflow needed)\nUse named lookup tools for common queries:\n- Token info: `b3os_token_lookup` (network + address, or coinId)\n- Prices: `b3os_price_lookup` (coinIds)\n- Balances: `b3os_balance_lookup` (address, chainIds, limit)\n- DeFi positions: `b3os_defi_lookup` (address, chainId)\n- Tx debugging: `b3os_debug_transaction` (txHash, chainId)\n- Polymarket: `b3os_polymarket_lookup` (query, slug, or marketUrl)\n- Any other action: `b3os_query_action` (generic fallback \u2014 use `b3os_search_actions` to find action types first)\n\n### One-Shot Execution (no save)\n- `b3os_run_action` for single-action execution\n- `b3os_run_ephemeral` for multi-step definitions (max 20 nodes, 60s timeout)\n\n---\n\n## Key Gotchas\n\n**Workflow lifecycle:**\n- **Workflows start in \"draft\" status** \u2014 must call `b3os_publish_workflow` to make them live\n- **Connectors must be set up in B3OS UI first** \u2014 Slack, Discord, Google Sheets, etc.\n- **On-chain actions require a funded org wallet** with sufficient balance and gas\n\n**Scheduling:**\n- **Cronjob rrule must be two-line format:** `\"DTSTART:YYYYMMDDTHHmmSSZ\\nRRULE:FREQ=...\"`. A bare RRULE without DTSTART will break scheduling. Encode desired time in DTSTART, NOT in BYHOUR/BYMINUTE.\n- **DTSTART must use current UTC timestamp** \u2014 a future DTSTART means the cron won't fire until then.\n\n**Conditions and expressions:**\n- **If condition format:** top-level key MUST be `$and` or `$or` \u2014 a bare comparison won't work.\n- **Regex matches** are a map with string keys: use `{{node.result.matches.1}}` (dot notation), never `{{node.result.matches[1]}}`.\n- **code-transform output** is at `{{nodeId.result.output}}`, not `{{nodeId.result}}`. The function must be named `transform(input)`.\n\n**Integrations:**\n- **Slack Block Kit `blocks` must be a JSON string**, not an object. Stringify the JSON array before setting the field.\n- **Slack action results** are nested under `data.ret`: use `{{node.result.data.ret.ts}}`, NOT `{{node.result.data.ts}}`.\n- **Google Sheets worksheetId** must be an integer, never a sheet name string.\n- **Gmail vs email triggers:** `gmail-new-email-*` polls a real Gmail inbox via OAuth. `email-new-email-*` creates a disposable inbox. If the user wants to watch their inbox, use `gmail-*`.\n- **x-new-tweet requires at least one filter** \u2014 empty payload is invalid. Must include username or keywords.\n\n**On-chain:**\n- **approve-erc20 is for smart contracts only** (DEX routers, DeFi protocols). For sending tokens to a wallet, use `send-erc20-token` directly \u2014 no approval step needed.\n- **Use `amountFormatted` in notifications**, `amount` for raw precision in on-chain calls.\n\n**Wait nodes:**\n- **Wait node timeout:** default 5 min is too short for human approval. Set `timeoutMs` to at least 86400000 (24h). Always include a `timed_out` branch child.\n- **Wait node uses `waitPayloadSchema`**, NOT the trigger's `payloadSchema`. For slack-new-interaction-event, valid fields are actionId, messageTs, userIds.\n\n**Database:**\n- **db-query results are always wrapped in a `rows` array** \u2014 access as `{{node.result.rows.0.field}}`, NEVER `{{node.result.field}}`.\n- **Use COALESCE for first-run safety:** `SELECT COALESCE((SELECT spent FROM budgets WHERE id = ?), 0)` \u2014 prevents row[0] IndexOutOfBounds.\n\n**Polymarket:**\n- **polymarket-redeem: add an if guard** checking `{{redeem.result.redeemedAmount}}` > 0 before downstream actions \u2014 redeemedAmount is ZERO for losing outcomes.\n- **Minimum bet amounts:** $1 for market orders (FAK/FOK), $5 for limit orders (GTC).\n";