Skip to content

Natural Language Trade CLI

This guide builds an interactive REPL where you type plain English commands like "swap 0.5 ETH to USDC on base" or "show my portfolio" and get formatted responses. It demonstrates the A2A (Agent-to-Agent) protocol's natural language communication.

What the CLI Does

1. Loads your API key from environment variables

2. Starts an interactive REPL with a suwappu> prompt

3. Sends your natural language input to Suwappu via A2A message/send

4. Parses the task response — if completed, pretty-prints artifacts

5. If working or submitted, polls with tasks/get and shows a spinner

6. Supports Ctrl+C to cancel running tasks via tasks/cancel

7. Keeps a local task history accessible with the history command

Python Version

class=class="hl-str">"hl-comment">#!/usr/bin/env python3
class="hl-str">""class="hl-str">"
Suwappu Natural Language Trade CLI — Python

Interactive REPL for communicating with Suwappu via the A2A protocol.

"class="hl-str">""

import os import sys import json import time import signal import requests

A2A_URL = class="hl-str">"https:class="hl-commentclass="hl-str">">//api.suwappu.bot/a2a"

class=class="hl-str">"hl-comment"># Spinner frames for polling animation

SPINNER = [class="hl-str">"⠋", class="hl-str">"⠙", class="hl-str">"⠹", class="hl-str">"⠸", class="hl-str">"⠼", class="hl-str">"⠴", class="hl-str">"⠦", class="hl-str">"⠧", class="hl-str">"⠇", class="hl-str">"⠏"]

class=class="hl-str">"hl-comment"># Track state

request_id = 0

task_history = []

current_task_id = None

def next_id():

class="hl-str">""class="hl-str">"Generate incrementing JSON-RPC request IDs."class="hl-str">""

global request_id

request_id += 1

return request_id

def a2a_request(headers, method, params):

class="hl-str">""class="hl-str">"Send a JSON-RPC 2.0 request to the A2A endpoint."class="hl-str">""

payload = {

class="hl-str">"jsonrpc": class="hl-str">"2.0",

class="hl-str">"id": next_id(),

class="hl-str">"method": method,

class="hl-str">"params": params,

}

response = requests.post(A2A_URL, headers=headers, json=payload)

response.raise_for_status()

data = response.json()

if class="hl-str">"error" in data:

raise Exception(fclass="hl-str">"A2A error {data[class="hl-str">'error'][class="hl-str">'code']}: {data[class="hl-str">'error'][class="hl-str">'message']}")

return data[class="hl-str">"result"]

def send_message(headers, text):

class="hl-str">""class="hl-str">"Send a natural language message via message/send."class="hl-str">""

return a2a_request(headers, class="hl-str">"message/send", {

class="hl-str">"message": {

class="hl-str">"role": class="hl-str">"user",

class="hl-str">"parts": [{class="hl-str">"type": class="hl-str">"text", class="hl-str">"text": text}],

}

})

def get_task(headers, task_id):

class="hl-str">""class="hl-str">"Poll a task by ID via tasks/get."class="hl-str">""

return a2a_request(headers, class="hl-str">"tasks/get", {class="hl-str">"taskId": task_id})

def cancel_task(headers, task_id):

class="hl-str">""class="hl-str">"Cancel a running task via tasks/cancel."class="hl-str">""

return a2a_request(headers, class="hl-str">"tasks/cancel", {class="hl-str">"taskId": task_id})

def format_artifacts(artifacts):

class="hl-str">""class="hl-str">"Pretty-print task artifacts."class="hl-str">""

output = []

for artifact in artifacts:

for part in artifact.get(class="hl-str">"parts", []):

if part[class="hl-str">"type"] == class="hl-str">"text":

output.append(part[class="hl-str">"text"])

elif part[class="hl-str">"type"] == class="hl-str">"data":

output.append(json.dumps(part[class="hl-str">"data"], indent=2))

return class="hl-str">"\n".join(output)

def poll_task(headers, task_id):

class="hl-str">""class="hl-str">"Poll a task until it reaches a terminal state, showing a spinner."class="hl-str">""

global current_task_id

current_task_id = task_id

frame = 0

try:

while True:

result = get_task(headers, task_id)

task = result[class="hl-str">"task"]

state = task[class="hl-str">"status"][class="hl-str">"state"]

if state == class="hl-str">"completed":

class=class="hl-str">"hl-comment"># Clear spinner line

sys.stdout.write(class="hl-str">"\r" + class="hl-str">" " * 40 + class="hl-str">"\r")

if task.get(class="hl-str">"artifacts"):

print(format_artifacts(task[class="hl-str">"artifacts"]))

else:

print(task[class="hl-str">"status"].get(class="hl-str">"message", class="hl-str">"Done."))

return task

elif state in (class="hl-str">"failed", class="hl-str">"canceled"):

sys.stdout.write(class="hl-str">"\r" + class="hl-str">" " * 40 + class="hl-str">"\r")

message = task[class="hl-str">"status"].get(class="hl-str">"message", state.capitalize())

print(fclass="hl-str">"Task {state}: {message}")

return task

class=class="hl-str">"hl-comment"># Show spinner

sys.stdout.write(fclass="hl-str">"\r {SPINNER[frame % len(SPINNER)]} Processing...")

sys.stdout.flush()

frame += 1

time.sleep(1)

finally:

current_task_id = None

def handle_response(headers, result):

class="hl-str">""class="hl-str">"Handle a message/send response — print immediately or poll."class="hl-str">""

task = result[class="hl-str">"task"]

state = task[class="hl-str">"status"][class="hl-str">"state"]

task_id = task[class="hl-str">"id"]

class=class="hl-str">"hl-comment"># Save to history

task_history.append({

class="hl-str">"id": task_id,

class="hl-str">"state": state,

class="hl-str">"timestamp": task[class="hl-str">"status"].get(class="hl-str">"timestamp", class="hl-str">""),

})

if state == class="hl-str">"completed":

if task.get(class="hl-str">"artifacts"):

print(format_artifacts(task[class="hl-str">"artifacts"]))

else:

print(task[class="hl-str">"status"].get(class="hl-str">"message", class="hl-str">"Done."))

elif state in (class="hl-str">"submitted", class="hl-str">"working"):

poll_task(headers, task_id)

elif state == class="hl-str">"failed":

message = task[class="hl-str">"status"].get(class="hl-str">"message", class="hl-str">"Unknown error")

print(fclass="hl-str">"Failed: {message}")

else:

print(fclass="hl-str">"Unexpected state: {state}")

def print_history():

class="hl-str">""class="hl-str">"Print local task history."class="hl-str">""

if not task_history:

print(class="hl-str">"No task history yet.")

return

print(fclass="hl-str">"\n {class="hl-str">'class="hl-commentclass="hl-str">">#':<4} {class="hl-str">'Task ID':<40} {class="hl-str">'State':<12} {class="hl-str">'Time'}")

print(fclass="hl-str">" {class="hl-str">'-' * 70}")

for i, entry in enumerate(task_history, 1):

print(fclass="hl-str">" {i:<4} {entry[class="hl-str">'id']:<40} {entry[class="hl-str">'state']:<12} {entry[class="hl-str">'timestamp']}")

print()

def print_help():

class="hl-str">""class="hl-str">"Print help text."class="hl-str">""

print(class="hl-str">""class="hl-str">"

Suwappu Natural Language CLI

────────────────────────────

Type any natural language command. Examples:

swap 0.5 ETH to USDC on base

price of ETH

prices for ETH, BTC, SOL

show my portfolio on ethereum

list supported chains

quote 100 USDC to WBTC

Special commands:

help Show this help message

history Show task history

quit Exit the CLI

Press Ctrl+C during a running task to cancel it.

"class="hl-str">"")

def setup_signal_handler(headers):

class="hl-str">""class="hl-str">"Set up Ctrl+C handler to cancel running tasks."class="hl-str">""

def handler(sig, frame):

global current_task_id

if current_task_id:

sys.stdout.write(class="hl-str">"\r" + class="hl-str">" " * 40 + class="hl-str">"\r")

print(class="hl-str">"Canceling task...")

try:

cancel_task(headers, current_task_id)

print(class="hl-str">"Task canceled.")

except Exception as e:

print(fclass="hl-str">"Cancel failed: {e}")

current_task_id = None

else:

print(class="hl-str">"\nGoodbye!")

sys.exit(0)

signal.signal(signal.SIGINT, handler)

def main():

api_key = os.environ.get(class="hl-str">"SUWAPPU_API_KEY")

if not api_key:

print(class="hl-str">"Error: Set SUWAPPU_API_KEY environment variable.")

print(class="hl-str">" export SUWAPPU_API_KEY=suwappu_sk_your_api_key")

sys.exit(1)

headers = {

class="hl-str">"Authorization": fclass="hl-str">"Bearer {api_key}",

class="hl-str">"Content-Type": class="hl-str">"application/json",

}

setup_signal_handler(headers)

print_help()

while True:

try:

user_input = input(class="hl-str">"suwappu> ").strip()

except EOFError:

print(class="hl-str">"\nGoodbye!")

break

if not user_input:

continue

class=class="hl-str">"hl-comment"># Handle special commands

lower = user_input.lower()

if lower == class="hl-str">"quit" or lower == class="hl-str">"exit":

print(class="hl-str">"Goodbye!")

break

elif lower == class="hl-str">"help":

print_help()

continue

elif lower == class="hl-str">"history":

print_history()

continue

class=class="hl-str">"hl-comment"># Send to A2A

try:

result = send_message(headers, user_input)

handle_response(headers, result)

except requests.exceptions.HTTPError as e:

if e.response.status_code == 429:

print(class="hl-str">"Rate limited. Wait a moment and try again.")

else:

print(fclass="hl-str">"HTTP error: {e}")

except Exception as e:

print(fclass="hl-str">"Error: {e}")

print() class=class="hl-str">"hl-comment"># Blank line between responses

if __name__ == class="hl-str">"__main__":

main()

Running the Python Version

-str">"hl-comment"># Install dependencies
-kw">pip install requests

-str">"hl-comment"># Set your API key
-kw">export SUWAPPU_API_KEY=suwappu_sk_your_api_key

-str">"hl-comment"># Start the CLI

python natural_language_cli.py

Example session:

suwappu> price of ETH

ETH: $3,500.42 (+2.5% 24h)

suwappu> swap 0.5 ETH to USDC on base

Quote ready: 0.5 ETH -> 1,247.50 USDC on Base

{

class="hl-str">"quote_id": class="hl-str">"q_abc123",

class="hl-str">"from_token": class="hl-str">"ETH",

class="hl-str">"to_token": class="hl-str">"USDC",

class="hl-str">"from_amount": class="hl-str">"0.5",

class="hl-str">"to_amount": class="hl-str">"1247.50",

class="hl-str">"chain": class="hl-str">"base"

}

suwappu> list supported chains Suwappu supports: ethereum, base, arbitrum, optimism, polygon, bsc, solana suwappu> history

class=class="hl-str">"hl-comment"># Task ID State Time

----------------------------------------------------------------------

1 a1b2c3d4-e5f6-7890-abcd-ef1234567890 completed 2026-03-08T12:00:00Z

2 b2c3d4e5-f6a7-8901-bcde-f12345678901 completed 2026-03-08T12:00:05Z

3 c3d4e5f6-a7b8-9012-cdef-123456789012 completed 2026-03-08T12:00:10Z

suwappu> quit

Goodbye!

---

TypeScript Version

class=class="hl-str">"hl-comment">#!/usr/bin/env npx tsx

/**

* Suwappu Natural Language Trade CLI — TypeScript

* Interactive REPL for communicating with Suwappu via the A2A protocol.

*/

import * as readline from class="hl-str">"readline"; const A2A_URL = class="hl-str">"https:class="hl-commentclass="hl-str">">//api.suwappu.bot/a2a"; const SPINNER = [class="hl-str">"⠋", class="hl-str">"⠙", class="hl-str">"⠹", class="hl-str">"⠸", class="hl-str">"⠼", class="hl-str">"⠴", class="hl-str">"⠦", class="hl-str">"⠧", class="hl-str">"⠇", class="hl-str">"⠏"]; let requestId = 0; const taskHistory: Array<{ id: string; state: string; timestamp: string }> = []; let currentTaskId: string | null = null; const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); function nextId(): number {

return ++requestId;

}

async function a2aRequest(apiKey: string, method: string, params: Record<string, unknown>) {

const response = await fetch(A2A_URL, {

method: class="hl-str">"POST",

headers: {

class="hl-str">"Content-Type": class="hl-str">"application/json",

Authorization: Bearer ${apiKey},

},

body: JSON.stringify({

jsonrpc: class="hl-str">"2.0",

id: nextId(),

method,

params,

}),

});

if (!response.ok) {

throw new Error(HTTP ${response.status}: ${await response.text()});

}

const data = await response.json();

if (data.error) {

throw new Error(A2A error ${data.error.code}: ${data.error.message});

}

return data.result;

}

async function sendMessage(apiKey: string, text: string) {

return a2aRequest(apiKey, class="hl-str">"message/send", {

message: {

role: class="hl-str">"user",

parts: [{ type: class="hl-str">"text", text }],

},

});

}

async function getTask(apiKey: string, taskId: string) {

return a2aRequest(apiKey, class="hl-str">"tasks/get", { taskId });

}

async function cancelTask(apiKey: string, taskId: string) {

return a2aRequest(apiKey, class="hl-str">"tasks/cancel", { taskId });

}

function formatArtifacts(artifacts: Array<{ parts: Array<{ type: string; text?: string; data?: unknown }> }>): string {

const output: string[] = [];

for (const artifact of artifacts) {

for (const part of artifact.parts ?? []) {

if (part.type === class="hl-str">"text" && part.text) {

output.push(part.text);

} else if (part.type === class="hl-str">"data" && part.data) {

output.push(JSON.stringify(part.data, null, 2));

}

}

}

return output.join(class="hl-str">"\n");

}

async function pollTask(apiKey: string, taskId: string) {

currentTaskId = taskId;

let frame = 0;

try {

while (true) {

const result = await getTask(apiKey, taskId);

const task = result.task;

const state = task.status.state;

if (state === class="hl-str">"completed") {

process.stdout.write(class="hl-str">"\r" + class="hl-str">" ".repeat(40) + class="hl-str">"\r");

if (task.artifacts?.length) {

console.log(formatArtifacts(task.artifacts));

} else {

console.log(task.status.message ?? class="hl-str">"Done.");

}

return task;

}

if (state === class="hl-str">"failed" || state === class="hl-str">"canceled") {

process.stdout.write(class="hl-str">"\r" + class="hl-str">" ".repeat(40) + class="hl-str">"\r");

console.log(Task ${state}: ${task.status.message ?? state});

return task;

}

process.stdout.write(\r ${SPINNER[frame % SPINNER.length]} Processing...);

frame++;

await sleep(1000);

}

} finally {

currentTaskId = null;

}

}

function handleResponse(apiKey: string, result: { task: any }) {

const task = result.task;

const state = task.status.state;

taskHistory.push({

id: task.id,

state,

timestamp: task.status.timestamp ?? class="hl-str">"",

});

if (state === class="hl-str">"completed") {

if (task.artifacts?.length) {

console.log(formatArtifacts(task.artifacts));

} else {

console.log(task.status.message ?? class="hl-str">"Done.");

}

return Promise.resolve();

}

if (state === class="hl-str">"submitted" || state === class="hl-str">"working") {

return pollTask(apiKey, task.id);

}

if (state === class="hl-str">"failed") {

console.log(Failed: ${task.status.message ?? class="hl-str">"Unknown error"});

} else {

console.log(Unexpected state: ${state});

}

return Promise.resolve();

}

function printHistory() {

if (taskHistory.length === 0) {

console.log(class="hl-str">"No task history yet.");

return;

}

console.log(\n ${class="hl-str">"class="hl-commentclass="hl-str">">#".padEnd(4)} ${class="hl-str">"Task ID".padEnd(40)} ${class="hl-str">"State".padEnd(12)} Time);

console.log( ${class="hl-str">"-".repeat(70)});

taskHistory.forEach((entry, i) => {

console.log(

${String(i + 1).padEnd(4)} ${entry.id.padEnd(40)} ${entry.state.padEnd(12)} ${entry.timestamp}

);

});

console.log();

}

function printHelp() {

console.log(

Suwappu Natural Language CLI

────────────────────────────

Type any natural language command. Examples:

swap 0.5 ETH to USDC on base

price of ETH

prices for ETH, BTC, SOL

show my portfolio on ethereum

list supported chains

quote 100 USDC to WBTC

Special commands:

help Show this help message

history Show task history

quit Exit the CLI

Press Ctrl+C during a running task to cancel it.

);

}

async function main() {

const apiKey = process.env.SUWAPPU_API_KEY;

if (!apiKey) {

console.error(class="hl-str">"Error: Set SUWAPPU_API_KEY environment variable.");

process.exit(1);

}

class=class="hl-str">"hl-comment">// Handle Ctrl+C for task cancellation

process.on(class="hl-str">"SIGINT", async () => {

if (currentTaskId) {

process.stdout.write(class="hl-str">"\r" + class="hl-str">" ".repeat(40) + class="hl-str">"\r");

console.log(class="hl-str">"Canceling task...");

try {

await cancelTask(apiKey, currentTaskId);

console.log(class="hl-str">"Task canceled.");

} catch (e) {

console.error(Cancel failed: ${e});

}

currentTaskId = null;

} else {

console.log(class="hl-str">"\nGoodbye!");

process.exit(0);

}

});

const rl = readline.createInterface({

input: process.stdin,

output: process.stdout,

prompt: class="hl-str">"suwappu> ",

});

printHelp();

rl.prompt();

rl.on(class="hl-str">"line", async (line) => {

const input = line.trim();

if (!input) {

rl.prompt();

return;

}

const lower = input.toLowerCase();

if (lower === class="hl-str">"quit" || lower === class="hl-str">"exit") {

console.log(class="hl-str">"Goodbye!");

rl.close();

process.exit(0);

}

if (lower === class="hl-str">"help") {

printHelp();

rl.prompt();

return;

}

if (lower === class="hl-str">"history") {

printHistory();

rl.prompt();

return;

}

try {

const result = await sendMessage(apiKey, input);

await handleResponse(apiKey, result);

} catch (error: unknown) {

const message = error instanceof Error ? error.message : String(error);

if (message.includes(class="hl-str">"429")) {

console.log(class="hl-str">"Rate limited. Wait a moment and try again.");

} else {

console.error(Error: ${message});

}

}

console.log();

rl.prompt();

});

rl.on(class="hl-str">"close", () => {

console.log(class="hl-str">"\nGoodbye!");

process.exit(0);

});

}

main().catch(console.error);

Running the TypeScript Version

-str">"hl-comment"># Install tsx for running TypeScript directly
-kw">npm install -g tsx

-str">"hl-comment"># Set your API key
-kw">export SUWAPPU_API_KEY=suwappu_sk_your_api_key

-str">"hl-comment"># Start the CLI

npx tsx natural_language_cli.ts

---

Customization Tips

Add Wallet Context

To automatically include your wallet address in portfolio queries, prepend it to every message:

WALLET = os.environ.get(class="hl-str">"WALLET_ADDRESS", class="hl-str">"")

def send_with_context(headers, text):

if WALLET and class="hl-str">"portfolio" in text.lower():

text = fclass="hl-str">"{text} for {WALLET}"

return send_message(headers, text)

Persistent History

Save task history to a JSON file so it persists across sessions:

import json
from pathlib import Path

HISTORY_FILE = Path.home() / class="hl-str">".suwappu_history.json"

def load_history():

if HISTORY_FILE.exists():

return json.loads(HISTORY_FILE.read_text())

return []

def save_history(history):

HISTORY_FILE.write_text(json.dumps(history, indent=2))

Colored Output

Add color codes for different response types:

GREEN = class="hl-str">"\033[92m"

YELLOW = class="hl-str">"\033[93m"

RED = class="hl-str">"\033[91m"

RESET = class="hl-str">"\033[0m"

def format_state(state):

colors = {class="hl-str">"completed": GREEN, class="hl-str">"working": YELLOW, class="hl-str">"failed": RED}

color = colors.get(state, RESET)

return fclass="hl-str">"{color}{state}{RESET}"

Pipe-Friendly Mode

Detect non-interactive input for scripting:

import sys

if not sys.stdin.isatty():

class=class="hl-str">"hl-comment"># Non-interactive: read all lines, process each, exit

for line in sys.stdin:

result = send_message(headers, line.strip())

handle_response(headers, result)

else:

class=class="hl-str">"hl-comment"># Interactive REPL

...