-
Notifications
You must be signed in to change notification settings - Fork 390
Add multi-agent portfolio rebalancer with semantic memory #438
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
0ca9d8c
3e549a0
75f47da
368b57b
437da41
9251196
785dd8d
952f1fc
ce9af3f
903ccd7
7727f81
9d683bf
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| AGENT_WALLET_ADDRESS=0x_your_wallet_here | ||
| OPENROUTER_API_KEY=your_openrouter_api_key_here | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,96 @@ | ||
| # Invoice Agent with X402 Payment Flow | ||
| ==================================== | ||
|
|
||
| ## Overview | ||
|
|
||
| This example implements a billing agent that: | ||
|
|
||
| * Generates invoices with structured line items | ||
| * Emits X402-compatible payment requests | ||
| * Verifies payments and updates invoice state | ||
|
|
||
|
|
||
| It demonstrates a complete payment lifecycle: | ||
| ```bash | ||
| create → pay → verify → settled | ||
| ``` | ||
|
|
||
| ## Features | ||
|
|
||
| * Invoice creation with structured payload | ||
| * X402 payment header generation | ||
| * Payment verification (mocked for demo) | ||
| * In-memory (non-persistent) invoice state tracking | ||
|
|
||
| ## Setup | ||
|
|
||
| Install dependencies: | ||
| ```bash | ||
| pip install bindu python-dotenv | ||
| ``` | ||
|
|
||
| Create .env: | ||
| ```bash | ||
| AGENT_WALLET_ADDRESS=0x_your_wallet_here | ||
| OPENROUTER_API_KEY=sk-xxxx #optional | ||
| ``` | ||
|
|
||
| Run the agent: | ||
| ```bash | ||
| python invoice_agent.py | ||
| ``` | ||
|
|
||
| ## Example Input | ||
| ```json | ||
| { | ||
| "type": "generate_invoice", | ||
| "payload": { | ||
| "recipient": "akash@example.com", | ||
| "items": [ | ||
| { "description": "API access", "quantity": 1, "unit_price": 50 }, | ||
| { "description": "Compute", "quantity": 2, "unit_price": 20 } | ||
| ], | ||
| "currency": "USDC" | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| ## Example Output: | ||
|
|
||
| ```json | ||
| { | ||
| "invoice_id": "inv_66ac3b32-6cb4-4588-bd06-f71160ba206c", | ||
| "total": 90, | ||
| "payment_header": "X402 0xE5bC3b8796432A70aC8450E2aaD54055d9e2DBb8:90" | ||
| } | ||
| { | ||
| "invoice": { | ||
| "id": "inv_66ac3b32-6cb4-4588-bd06-f71160ba206c", | ||
| "recipient": "acme@example.com", | ||
| "recipient_wallet": "0xE5bC3b8796432A70aC8450E2aaD54055d9e2DBb8", | ||
| "items": [ | ||
| { "description": "API", "quantity": 1, "unit_price": 50 }, | ||
| { "description": "Compute", "quantity": 2, "unit_price": 20 } | ||
| ], | ||
| "currency": "USDC", | ||
| "total": 90, | ||
| "status": "paid", | ||
| "tx_hash": "0xabc123" | ||
| } | ||
| } | ||
| ``` | ||
|
Comment on lines
+60
to
+81
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Example output doesn't match actual implementation. The documented "payment_header": {
"amount": str(invoice["total"]),
"token": invoice.get("currency", "USDC"),
"network": os.getenv("X402_NETWORK", "base-sepolia"),
"pay_to_address": invoice["recipient_wallet"],
}Update the example to reflect the actual response format. 📝 Proposed fix ## Example Output:
```json
{
"invoice_id": "inv_66ac3b32-6cb4-4588-bd06-f71160ba206c",
"total": 90,
- "payment_header": "X402 0xE5bC3b8796432A70aC8450E2aaD54055d9e2DBb8:90"
+ "payment_header": {
+ "amount": "90",
+ "token": "USDC",
+ "network": "base-sepolia",
+ "pay_to_address": "0xE5bC3b8796432A70aC8450E2aaD54055d9e2DBb8"
+ }
}🤖 Prompt for AI Agents |
||
|
|
||
|
|
||
| ## Skills | ||
|
|
||
| * generate\_invoice – create invoice and emit X402 payment request | ||
| * get\_invoice – fetch invoice by ID | ||
| * list\_invoices – list invoices | ||
| * verify\_payment – verify payment and update invoice state | ||
|
|
||
|
|
||
| ## Notes | ||
|
|
||
| * Payment verification is mocked for demonstration | ||
| * Storage is in-memory and can be replaced with a database | ||
| * Wallet address can be any valid EVM address | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,167 @@ | ||
| from bindu.penguin.bindufy import bindufy | ||
| from dotenv import load_dotenv | ||
| import os | ||
| import logging | ||
| import uuid | ||
| import json | ||
|
|
||
| load_dotenv() | ||
| logger = logging.getLogger(__name__) | ||
| # Simple in-memory storage | ||
|
|
||
| db = {} | ||
|
|
||
|
|
||
| def save_invoice(invoice): | ||
| db[invoice["id"]] = invoice | ||
|
|
||
|
|
||
| def get_invoice_by_id(invoice_id): | ||
| return db.get(invoice_id) | ||
|
|
||
|
|
||
| def list_invoices(): | ||
| return list(db.values()) | ||
|
|
||
|
|
||
| # Core logic | ||
|
|
||
|
|
||
| def create_invoice(payload): | ||
| if not isinstance(payload, dict) or "items" not in payload: | ||
| raise ValueError("Invalid payload") | ||
|
|
||
| if not isinstance(payload["items"], list) or not payload["items"]: | ||
| raise ValueError("items must be a non-empty list") | ||
|
|
||
| total = 0 | ||
| for i, item in enumerate(payload["items"]): | ||
| qty = item.get("quantity") | ||
| price = item.get("unit_price") | ||
|
|
||
| if not isinstance(qty, (int, float)) or not isinstance(price, (int, float)): | ||
| raise ValueError(f"Invalid item at index {i}") | ||
|
|
||
| if qty <= 0 or price < 0: | ||
| raise ValueError(f"Invalid values at index {i}") | ||
|
|
||
| total += qty * price | ||
|
|
||
| recipient_wallet = payload.get("recipient_wallet") or os.getenv( | ||
| "AGENT_WALLET_ADDRESS" | ||
| ) | ||
|
|
||
| if not recipient_wallet: | ||
| raise ValueError("recipient_wallet is required") | ||
| invoice = { | ||
| "id": f"inv_{uuid.uuid4()}", | ||
| "recipient": payload.get("recipient"), | ||
| "recipient_wallet": recipient_wallet, | ||
| "items": payload["items"], | ||
| "currency": payload.get("currency", "USDC"), | ||
| "total": total, | ||
| "status": "pending", | ||
| } | ||
|
|
||
| save_invoice(invoice) | ||
| return invoice | ||
|
|
||
|
|
||
| def verify_payment(invoice_id, tx_hash): | ||
| invoice = get_invoice_by_id(invoice_id) | ||
|
|
||
| if not invoice: | ||
| return {"verified": False, "reason": "Invoice not found"} | ||
|
|
||
| # mock verification | ||
| invoice["status"] = "paid" | ||
| invoice["tx_hash"] = tx_hash | ||
| save_invoice(invoice) | ||
|
|
||
| return { | ||
| "verified": True, | ||
| "settled_amount": invoice["total"], | ||
| "reason": None, | ||
| } | ||
|
|
||
|
|
||
| # Bindu config | ||
|
|
||
| config = { | ||
| "author": "akash", | ||
| "name": "invoice-agent", | ||
| "deployment": {"url": "http://localhost:3773", "expose": True}, | ||
| "description": "Invoice agent with X402 payment flow", | ||
| "version": "1.0.0", | ||
| "capabilities": { | ||
| "payments": ["invoice", "x402"], | ||
| }, | ||
| "skills": ["skills/invoice-agent-skill"], | ||
| "auth": {"enabled": False}, | ||
| "storage": {"type": "memory"}, | ||
| "scheduler": {"type": "memory"}, | ||
| } | ||
|
|
||
| # Handler (Bindu entry point) | ||
|
|
||
|
|
||
| def handler(messages): | ||
| try: | ||
| user_messages = [m for m in messages if m.get("role") == "user"] | ||
|
|
||
| if not user_messages: | ||
| return "No user message found" | ||
|
|
||
| raw = user_messages[-1].get("parts", [{}])[0].get("text", "{}") | ||
|
|
||
| try: | ||
| input_data = json.loads(raw) | ||
| except json.JSONDecodeError: | ||
| return {"error": "bad_request", "message": "Invalid JSON input"} | ||
|
|
||
| if input_data.get("type") == "generate_invoice": | ||
| invoice = create_invoice(input_data.get("payload", {})) | ||
|
|
||
| return { | ||
| "invoice_id": invoice["id"], | ||
| "total": invoice["total"], | ||
| "payment_header": { | ||
| "amount": str(invoice["total"]), | ||
| "token": invoice.get("currency", "USDC"), | ||
| "network": os.getenv("X402_NETWORK", "base-sepolia"), | ||
| "pay_to_address": invoice["recipient_wallet"], | ||
| }, | ||
| } | ||
|
|
||
| if input_data.get("type") == "get_invoice": | ||
| invoice = get_invoice_by_id(input_data.get("invoice_id")) | ||
|
|
||
| if not invoice: | ||
| return { | ||
| "error": "not_found", | ||
| "message": f"Invoice not found: {input_data.get('invoice_id')}", | ||
| } | ||
|
|
||
| return {"invoice": invoice} | ||
|
|
||
| if input_data.get("type") == "list_invoices": | ||
| return {"invoices": list_invoices()} | ||
|
|
||
| if input_data.get("type") == "verify_payment": | ||
| return verify_payment( | ||
| input_data.get("invoice_id"), | ||
| input_data.get("tx_hash"), | ||
| ) | ||
|
|
||
| return "Unknown request type" | ||
|
|
||
| except Exception: | ||
| logger.exception("Unhandled error") | ||
| return {"error": "internal_error", "message": "Internal Server Error"} | ||
|
|
||
|
|
||
| # Run agent | ||
|
|
||
| if __name__ == "__main__": | ||
| print("Invoice Agent running...") | ||
| bindufy(config, handler) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| id: invoice-agent-skill | ||
| name: invoice-agent-skill | ||
| description: Invoice agent for X402 payment flow | ||
| version: 1.0.0 | ||
|
|
||
| tags: | ||
| - billing | ||
| - payments | ||
| - x402 | ||
|
|
||
| input_modes: | ||
| - application/json | ||
|
|
||
| output_modes: | ||
| - application/json |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| COINGECKO_API_KEY=optional | ||
| USE_FAKE_DATA=true | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| REQUEST_TIMEOUT=5 | ||
| REBALANCE_FRACTION=0.25 | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing
X402_NETWORKenvironment variable.The
invoice_agent.pyhandler referencesos.getenv("X402_NETWORK", "base-sepolia")at line 131, but this variable is not documented in the.env.example. While it has a default, users should know it's configurable.📝 Proposed fix
AGENT_WALLET_ADDRESS=0x_your_wallet_here OPENROUTER_API_KEY=your_openrouter_api_key_here +X402_NETWORK=base-sepolia # optional, defaults to base-sepolia📝 Committable suggestion
🤖 Prompt for AI Agents