Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,14 @@ jobs:
- name: Install dependencies
run: pnpm install

- name: Install dependencies
run: pnpm build

- name: Add other env secrets
run: |
echo "PRIVATE_KEY=${{ secrets.PRIVATE_KEY }}" >> .env

- name: Run e2e tests
run: pnpm test:e2e
env:
PRIVATE_KEY: ${{ secrets.PRIVATE_KEY }}
15 changes: 7 additions & 8 deletions coinfello/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,9 @@ openclaw get_account
Authenticates with CoinFello using Sign-In with Ethereum (SIWE) and your smart account. Saves the session token to local config.

```bash
openclaw sign_in [--base-url <url>]
openclaw sign_in
```

- `--base-url <url>` — Auth server base URL (default: `https://app.coinfello.com/api/auth`)
- Signs in using the private key stored in config
- Saves the session token to `~/.clawdbot/skills/coinfello/config.json`
- The session token is loaded automatically for subsequent `send_prompt` calls
Expand Down Expand Up @@ -102,15 +101,15 @@ openclaw send_prompt "<prompt>" [--use-redelegation]

**What happens internally:**

1. Sends the prompt to CoinFello's conversation endpoint
2. If the server returns a read-only response (no transaction needed) → prints the response text and exits
3. If the server returns a `txn_id` directly → prints it and exits
4. If the server sends an `ask_for_delegation` tool call with a `chainId` and `scope`:
1. Fetches available agents from `/api/v1/automation/coinfello-agents` and sends the prompt to CoinFello's conversation endpoint
2. If the server returns a read-only response (no `clientToolCalls` and no `txn_id`) → prints the response text and exits
3. If the server returns a `txn_id` directly with no tool calls → prints it and exits
4. If the server sends an `ask_for_delegation` client tool call with a `chainId` and `scope`:
- Fetches CoinFello's delegate address
- Rebuilds the smart account using the chain ID from the tool call
- Parses the server-provided scope (supports ERC-20, native token, ERC-721, and function call scopes)
- Creates and signs a subdelegation
- Sends the signed delegation back to the conversation endpoint
- Creates and signs a subdelegation (wraps with ERC-6492 signature if the smart account is not yet deployed on-chain)
- Sends the signed delegation back as a `clientToolCallResponse` along with the `chatId` and `callId` from the initial response
- Returns a `txn_id` for tracking

### get_transaction_status
Expand Down
61 changes: 38 additions & 23 deletions coinfello/references/REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,11 @@ No parameters. Prints the stored smart account address from config. Exits with a
openclaw sign_in [--base-url <url>]
```

| Parameter | Type | Required | Default | Description |
| ------------ | -------- | -------- | ------------------------------------ | -------------------- |
| `--base-url` | `string` | No | `https://app.coinfello.com/api/auth` | Auth server base URL |
| Parameter | Type | Required | Default | Description |
| ------------ | -------- | -------- | ------------------------------- | -------------------- |
| `--base-url` | `string` | No | `${COINFELLO_BASE_URL}api/auth` | Auth server base URL |

The default resolves using the `COINFELLO_BASE_URL` environment variable (defaults to `http://localhost:3000/`).

Performs a Sign-In with Ethereum (SIWE) flow using the private key from config. Saves the `session_token` to config on success. The session token is automatically injected as a cookie for subsequent API calls.

Expand All @@ -86,7 +88,9 @@ openclaw send_prompt <prompt> [--use-redelegation]
| `prompt` | `string` | Yes | — | Natural language prompt to send to CoinFello |
| `--use-redelegation` | `boolean` | No | `false` | Use stored parent delegation to create a redelegation chain |

The server determines whether a delegation is needed and, if so, what scope and chain to use. The client creates and signs the subdelegation based on the server's `ask_for_delegation` tool call response.
The server determines whether a delegation is needed and, if so, what scope and chain to use. The client creates and signs the subdelegation based on the server's `ask_for_delegation` client tool call response.

**ERC-6492 signature wrapping**: If the smart account has not yet been deployed on-chain, the CLI wraps the delegation signature using ERC-6492 (`serializeErc6492Signature`) with the account's factory address and factory data. This allows the delegation to be verified even before the account contract exists.

### openclaw get_transaction_status

Expand Down Expand Up @@ -115,38 +119,41 @@ Any chain exported by `viem/chains`. Common examples:

## API Endpoints

Base URL: `https://app.coinfello.com`
Base URL: Configured via the `COINFELLO_BASE_URL` environment variable (defaults to `http://localhost:3000/`).

| Endpoint | Method | Description |
| ---------------------------------------- | ------ | ------------------------------------------------- |
| `/api/v1/automation/coinfello-address` | GET | Returns CoinFello's delegate address |
| `/api/v1/automation/coinfello-agents` | GET | Returns available CoinFello agents |
| `/api/conversation` | POST | Submits prompt (and optionally signed delegation) |
| `/api/v1/transaction_status?txn_id=<id>` | GET | Returns transaction status |

### POST /api/conversation body
| Endpoint | Method | Description |
| ---------------------------------------- | ------ | ---------------------------------------------------- |
| `/api/v1/automation/coinfello-address` | GET | Returns CoinFello's delegate address |
| `/api/v1/automation/coinfello-agents` | GET | Returns available CoinFello agents (id, name) |
| `/api/conversation` | POST | Submits prompt (and optionally client tool response) |
| `/api/v1/transaction_status?txn_id=<id>` | GET | Returns transaction status |

Initial request (prompt only):
### GET /api/v1/automation/coinfello-agents response

```json
{
"inputMessage": "send 5 USDC to 0xRecipient...",
"agentId": 1,
"stream": false
"availableAgents": [{ "id": 1, "name": "CoinFello Agent" }]
}
```

Follow-up request (with signed delegation):
The `send_prompt` command fetches this list and uses the first agent's `id` as `agentId` in conversation requests.

### POST /api/conversation body

Initial request (prompt only):

```json
{
"inputMessage": "send 5 USDC to 0xRecipient...",
"agentId": 1,
"stream": false,
"signed_subdelegation": { "...delegation object with signature..." }
"stream": false
}
```

`agentId` is dynamically resolved from the `/api/v1/automation/coinfello-agents` endpoint (not hardcoded).

The follow-up request (sending the signed delegation back) is handled internally by `send_prompt` — no manual construction is needed.

### POST /api/conversation response

Read-only response:
Expand All @@ -161,14 +168,15 @@ Delegation request (server asks client to sign):

```json
{
"toolCalls": [
"clientToolCalls": [
{
"type": "function_call",
"name": "ask_for_delegation",
"callId": "...",
"callId": "call_abc123...",
"arguments": "{\"chainId\": 8453, \"scope\": {\"type\": \"erc20TransferAmount\", \"tokenAddress\": \"0x...\", \"maxAmount\": \"5000000\"}}"
}
]
],
"chatId": "chat_abc123..."
}
```

Expand All @@ -180,6 +188,13 @@ Final response (after delegation submitted):
}
```

| Field | Type | Description |
| ----------------- | --------- | -------------------------------------------------------------- |
| `responseText` | `string?` | Text response for read-only prompts |
| `txn_id` | `string?` | Transaction ID when a transaction has been submitted |
| `clientToolCalls` | `array?` | Server-requested client tool calls (e.g. `ask_for_delegation`) |
| `chatId` | `string?` | Chat session ID, sent back in follow-up requests |

## Delegation Scope Types

The server may request any of the following scope types via `ask_for_delegation`. The CLI parses and creates the appropriate delegation caveat automatically.
Expand Down
24 changes: 9 additions & 15 deletions coinfello/scripts/setup-and-send.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,26 @@
# setup-and-send.sh — End-to-end CoinFello workflow
#
# Usage:
# ./setup-and-send.sh <chain> <token-address> <amount> <prompt> [decimals]
# ./setup-and-send.sh <chain> <prompt>
#
# Example:
# ./setup-and-send.sh sepolia \
# 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 \
# 5 \
# "send 3 USDC to 0xRecipient" \
# 6
# ./setup-and-send.sh sepolia "send 3 USDC to 0xRecipient"

set -euo pipefail

CHAIN="${1:?Usage: $0 <chain> <token-address> <amount> <prompt> [decimals]}"
TOKEN_ADDRESS="${2:?Missing token-address}"
AMOUNT="${3:?Missing amount}"
PROMPT="${4:?Missing prompt}"
DECIMALS="${5:-18}"
CHAIN="${1:?Usage: $0 <chain> <prompt>}"
PROMPT="${2:?Missing prompt}"

echo "==> Creating smart account on ${CHAIN}..."
openclaw create_account "$CHAIN"

echo ""
echo "==> Signing in..."
openclaw sign_in

echo ""
echo "==> Sending prompt..."
OUTPUT=$(openclaw send_prompt "$PROMPT" \
--token-address "$TOKEN_ADDRESS" \
--amount "$AMOUNT" \
--decimals "$DECIMALS")
OUTPUT=$(openclaw send_prompt "$PROMPT")

echo "$OUTPUT"

Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@coinfello/agent-cli",
"version": "0.0.2",
"version": "0.1.0",
"description": "",
"type": "module",
"main": "dist/index.js",
Expand Down Expand Up @@ -34,6 +34,7 @@
},
"devDependencies": {
"@types/node": "^25.2.1",
"dotenv": "^17.3.1",
"eslint": "^10.0.0",
"eslint-config-prettier": "^10.1.8",
"prettier": "^3.8.1",
Expand Down
9 changes: 9 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export function resolveChainById(chainId: number): Chain {
return chain
}

function resolveChainInput(chainInput: string | number): Chain {
export function resolveChainInput(chainInput: string | number): Chain {
if (typeof chainInput === 'number') {
return resolveChainById(chainInput)
}
Expand Down
34 changes: 28 additions & 6 deletions src/api.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { fetchWithCookies } from './cookies.js'
import { SignedSubdelegation } from './types.js'

export const BASE_URL = 'https://app.coinfello.com/'
export const BASE_URL =
process.env.COINFELLO_BASE_URL || 'https://hyp3r-58q8qto10-hyperplay.vercel.app/'
export const BASE_URL_V1 = BASE_URL + 'api/v1'

export async function getCoinFelloAddress(): Promise<string> {
Expand Down Expand Up @@ -43,17 +45,25 @@ export interface ToolCall {
export interface ConversationResponse {
responseText?: string
txn_id?: string
toolCalls?: ToolCall[]
clientToolCalls?: ToolCall[]
chatId?: string | null
}

export interface SendConversationParams {
prompt: string
signedSubdelegation?: unknown
signedSubdelegation?: SignedSubdelegation
chatId?: string | null
/* eslint-disable-next-line */
delegationArguments?: any
callId?: string
}

export async function sendConversation({
prompt,
signedSubdelegation,
chatId,
delegationArguments,
callId,
}: SendConversationParams): Promise<ConversationResponse> {
const agents = await getCoinFelloAgents()
const body: Record<string, unknown> = {
Expand All @@ -64,12 +74,24 @@ export async function sendConversation({
body.agentId = agents[0].id
}
if (signedSubdelegation !== undefined) {
body.signed_subdelegation = signedSubdelegation
body.clientToolCallResponse = {
output: JSON.stringify({
success: true,
delegation: signedSubdelegation,
chainId: delegationArguments ? JSON.parse(delegationArguments).chainId : undefined,
}),
type: 'function_call_output',
callId: callId,
name: 'ask_for_delegation',
arguments: delegationArguments,
}
}
if (chatId) {
body.chatId = chatId
}

const response = await fetchWithCookies(`${BASE_URL}/api/conversation`, {
const response = await fetchWithCookies(`${BASE_URL}api/conversation`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
})

Expand Down
Loading