Skip to content

GetBindu/Bindu

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1,317 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Bindu — humans and agents, side by side

Bindu

Bindu

The identity, communication, and payments layer for AI agents.


Write your agent in any framework. Wrap it with bindufy(). Ship a signed A2A microservice — identity, OAuth2, and on-chain payments — in ten lines of code.

No infrastructure to write. No framework to rewrite. Works from Python, TypeScript, and Kotlin, and layered on two open protocols: A2A and x402.


What you get

When you wrap a handler with bindufy(config, handler), the process comes up speaking standard protocols, signing every response, and ready to take payment. Grouped by what it does for you:


Protocol — talk to the world

Capability What it means
A2A JSON-RPC endpoint Standard protocol other agents already speak. message/send, tasks/get, message/stream on port 3773.
Push notifications Webhook callbacks on task state change — no polling required.
Language-agnostic Python, TypeScript, and Kotlin SDKs share one gRPC core. Same protocol, same DID, same auth.

Identity & access — prove who's calling

Capability What it means
DID identity (Ed25519) Every returned artifact is signed. Callers verify with a W3C-standard DID — no shared secrets.
OAuth2 via Ory Hydra Scoped tokens (agent:read, agent:write, agent:execute) instead of one all-or-nothing bearer.

Commerce & reach — get paid and be reachable

Capability What it means
x402 payments One flag and the agent charges USDC on Base before processing a request. Payment check runs before your handler.
Public tunnel expose: true opens an FRP tunnel so your local agent is reachable from the public internet.

Install

uv add bindu

For a development checkout with tests:

git clone https://github.com/getbindu/Bindu.git
cd Bindu
uv sync --dev

Requires Python 3.12+ and uv. An API key for at least one LLM provider (OPENROUTER_API_KEY, OPENAI_API_KEY, or MINIMAX_API_KEY) is needed to run the examples.


Hello agent

The whole idea of Bindu shows up clearly in one file — build any agent you like, hand it to bindufy(), and your process comes up as a signed A2A microservice. The block below is complete and runnable.

import os
from bindu.penguin.bindufy import bindufy
from agno.agent import Agent
from agno.models.openai import OpenAIChat
from agno.tools.duckduckgo import DuckDuckGoTools

# 1. Build your agent with whatever framework you prefer. Bindu doesn't
#    care what's inside — it just needs something callable.
agent = Agent(
    instructions="You are a research assistant that finds and summarizes information.",
    model=OpenAIChat(id="gpt-4o"),
    tools=[DuckDuckGoTools()],
)

# 2. Tell Bindu who you are and where the agent lives. `expose: True`
#    opens a public FRP tunnel — drop it for local-only.
config = {
    "author": "you@example.com",
    "name": "research_agent",
    "description": "Research assistant with web search.",
    "deployment": {
        "url": os.getenv("BINDU_DEPLOYMENT_URL", "http://localhost:3773"),
        "expose": True,
    },
    "skills": ["skills/question-answering"],
}

# 3. The handler contract: (messages) -> response. That's it.
def handler(messages: list[dict[str, str]]):
    return agent.run(input=messages)

# 4. bindufy() boots the HTTP server, mints your DID, registers with
#    Hydra (if auth is on), and starts accepting A2A calls.
bindufy(config, handler)

Run it, and the agent is live at the configured URL. Need a different port? Export BINDU_PORT=4000 — no code change.

A bindufied Agno agent running on port 3773

TypeScript equivalent
import { bindufy } from "@bindu/sdk";
import OpenAI from "openai";

const openai = new OpenAI();

bindufy({
  author: "you@example.com",
  name: "research_agent",
  description: "Research assistant.",
  deployment: { url: "http://localhost:3773", expose: true },
  skills: ["skills/question-answering"],
}, async (messages) => {
  const response = await openai.chat.completions.create({
    model: "gpt-4o",
    messages: messages.map(m => ({ role: m.role as "user" | "assistant" | "system", content: m.content })),
  });
  return response.choices[0].message.content || "";
});

The TypeScript SDK launches the Python core automatically. Same protocol, same DID. Full example in examples/typescript-openai-agent/.

Calling the agent with curl
curl -X POST http://localhost:3773/ \
  -H 'Content-Type: application/json' \
  -d '{
    "jsonrpc": "2.0",
    "method": "message/send",
    "id": "<uuid>",
    "params": {
      "message": {
        "role": "user",
        "kind": "message",
        "parts": [{"kind": "text", "text": "Hello"}],
        "messageId": "<uuid>",
        "contextId": "<uuid>",
        "taskId": "<uuid>"
      }
    }
  }'

Poll tasks/get with the same taskId until state is completed. The returned artifact carries a DID signature under metadata["did.message.signature"].


How it fits

So what actually happens when that bindufy() call executes? The handler is the only code you write. Everything else is scaffolding Bindu puts around it:

your handler  ──►  bindufy(config, handler)
                          │
                          ▼
                 ┌────────────────────────────────────┐
                 │  Bindu core (HTTP :3773)           │
                 │    OAuth2 (Hydra)                  │
                 │    DID verification                │
                 │    x402 payment check (optional)   │
                 │    Task manager + scheduler        │
                 └────────────────────────────────────┘
                          │
                          ▼
                 A2A-signed artifact returned to caller

bindufy() is a thin wrapper. Your handler stays pure — (messages) -> response. Bindu owns identity, protocol, auth, payment, storage, and scheduling.


Calling a secured agent

TL;DR — when AUTH__ENABLED=true, every call needs a Hydra bearer token and three X-DID-* headers. Python client: ~25 lines, below. Postman: paste one script. The rest of this section unpacks why and how, and what goes wrong when it goes wrong.

The curl example in Hello agent works because auth is off by default — anyone can POST to your agent. The moment you flip AUTH__ENABLED=true AUTH__PROVIDER=hydra, your agent gets stricter. Every caller now has to answer two questions before the handler runs:

  1. Are you allowed to call me? — show a valid OAuth2 token from Hydra.
  2. Are you really who you say you are? — sign the request with a DID key.

Think of it like boarding a flight: the boarding pass (OAuth token) says "yes, you have a seat on this flight," and the passport (DID signature) says "and you really are the person on that boarding pass." The server checks both.

The full theory lives in docs/AUTHENTICATION.md and docs/DID.md — plain-English, no crypto background assumed. What follows is the practical "I just want to call my agent" version.


The three extra headers

Alongside the usual Authorization: Bearer <hydra-jwt>, every secured request carries:

Header Value
X-DID your DID string, e.g. did:bindu:you_at_example_com:myagent:<uuid>
X-DID-Timestamp current unix seconds (server allows 5 min skew)
X-DID-Signature base58( Ed25519_sign( <signing payload> ) )

The signing payload is reconstructed on the server like this:

json.dumps({"body": <raw-body-string>, "did": <did>, "timestamp": <ts>}, sort_keys=True)

Two gotchas that will bite you until you've felt them:

  • Match Python's JSON spacing. Python's default json.dumps writes ", " and ": " (with spaces). JSON.stringify in JS writes them without. If your payload serializes differently, Ed25519 sees different bytes and the server returns reason="crypto_mismatch".
  • Sign what you send. If you parse the body, modify it, re-serialize, and ship that — you signed the wrong bytes. Build the body string once, sign those exact bytes, send those exact bytes.

Step 1 — get a bearer token from Hydra

The agent prints a ready-to-run curl in its startup banner. The short version:

SECRET=$(jq -r '.[].client_secret' < .bindu/oauth_credentials.json)
curl -X POST https://hydra.getbindu.com/oauth2/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials" \
  -d "client_id=did:bindu:you_at_example_com:myagent:<uuid>" \
  -d "client_secret=$SECRET" \
  -d "scope=openid offline agent:read agent:write"

The response has an access_token. It's good for about an hour — cache it, refetch when you need it.


Step 2 — pick your client

Python — the shortest working example. Reads the agent's own keys (Bindu writes them to .bindu/ on first boot), signs a request, polls for the result. Self-call works because the agent's keys are a valid caller identity.

import base58, httpx, json, time, uuid
from pathlib import Path
from cryptography.hazmat.primitives import serialization

# 1. Load the keys Bindu wrote on first boot
priv  = serialization.load_pem_private_key(Path(".bindu/private.pem").read_bytes(), password=None)
creds = next(iter(json.loads(Path(".bindu/oauth_credentials.json").read_text()).values()))
did   = creds["client_id"]            # DID doubles as the Hydra client_id

# 2. Exchange credentials for a short-lived JWT
bearer = httpx.post("https://hydra.getbindu.com/oauth2/token", data={
    "grant_type": "client_credentials",
    "client_id": creds["client_id"], "client_secret": creds["client_secret"],
    "scope": "openid offline agent:read agent:write",
}).json()["access_token"]

# 3. Build the body ONCE — these are the bytes we'll sign AND send
tid = str(uuid.uuid4())
body = json.dumps({
    "jsonrpc": "2.0", "method": "message/send", "id": str(uuid.uuid4()),
    "params": {"message": {
        "role": "user", "kind": "message",
        "parts": [{"kind": "text", "text": "Hello!"}],
        "messageId": str(uuid.uuid4()), "contextId": str(uuid.uuid4()), "taskId": tid,
    }},
})

# 4. Sign: base58(Ed25519( json.dumps({body,did,timestamp}, sort_keys=True) ))
ts      = int(time.time())
payload = json.dumps({"body": body, "did": did, "timestamp": ts}, sort_keys=True)
sig     = base58.b58encode(priv.sign(payload.encode())).decode()

# 5. Fire it
r = httpx.post("http://localhost:3773/", content=body, headers={
    "Content-Type":    "application/json",
    "Authorization":   f"Bearer {bearer}",
    "X-DID":           did,
    "X-DID-Timestamp": str(ts),
    "X-DID-Signature": sig,
})
print(r.status_code, r.json())

For a full-featured version with polling and error handling, see examples/hermes_agent/call.py.


Postman — paste one script into your collection.

  1. Open your collection → Pre-request Script tab → paste the contents of docs/postman-did-signing.js.
  2. Set two collection variables: bindu_did (your DID string) and bindu_did_seed (your 32-byte Ed25519 seed, base64-encoded).
  3. Add an Authorization: Bearer {{bindu_bearer}} header and drop your Hydra token into bindu_bearer.
  4. Hit Send. The script signs the exact body bytes Postman is about to send and sets the three X-DID-* headers for you.

Requires Postman Desktop v11+ (needs Ed25519 in crypto.subtle).


Plain curl — technically possible, usually painful. The signature depends on the body bytes you're about to send, so you need a helper script to compute the signature first, then substitute it into the curl call. If you're doing this, you're probably better off using the Python client above.


When signatures fail

The server logs one of three reasons. If your request gets rejected with a 403, ask the operator (or check the server log yourself):

Log says What it means Fix
timestamp_out_of_window Your X-DID-Timestamp is more than 5 min off the server's clock, or you reused an old timestamp Recompute int(time.time()) on every request
malformed_input The base58 decoding of the signature or public key failed Check the X-DID-Signature isn't URL-encoded, truncated, or wrapped in quotes
crypto_mismatch The bytes you signed ≠ the bytes you sent Rebuild the payload with sort_keys=True and Python's default JSON spacing; sign the raw body string once and send the same bytes

One sharper failure mode we hit in testing: if crypto_mismatch persists and you're sure your bytes match, Hydra's stored public key for this DID may be stale from an older registration. Fix: stop the agent, delete .bindu/oauth_credentials.json, restart — Hydra's client record will be refreshed with the current keys.


Gateway — multi-agent orchestration

A single bindufy()-wrapped agent is a microservice. The Bindu Gateway is a task-first orchestrator that sits on top: give it a user question and a catalog of A2A agents, and a planner LLM decomposes the work, calls the right agents over A2A, and streams results back as Server-Sent Events. No DAG engine, no separate orchestrator service — the planner's LLM picks tools per turn.

What you get beyond a single agent:

  • One endpoint: POST /plan — hand it a question and an agent catalog, get streamed steps.
  • Agent catalog per request — external systems pass the list of agents, skills, and endpoints. No fleet hosting in the gateway itself.
  • Session persistence (Supabase) — Postgres-backed with compaction, revert, and multi-turn history.
  • Native TypeScript A2A — no Python subprocess, no @bindu/sdk dependency in the gateway.
  • Optional DID signing + Hydra integration — gateway identity end-to-end.

Minimal quickstart:

cd gateway
npm install
cp .env.example .env.local         # fill SUPABASE_*, GATEWAY_API_KEY, OPENROUTER_API_KEY
npm run dev                        # → http://localhost:3774
curl -sS http://localhost:3774/health

Apply the two Supabase migrations first (gateway/migrations/001_init.sql, 002_compaction_revert.sql). Full walkthrough and operator reference live in gateway/README.md and docs/GATEWAY.md (45-minute end-to-end: clean clone → three chained agents → authoring a recipe → DID signing).

Gateway documentation:

Topic Link
Overview docs.getbindu.com/bindu/gateway/overview
Quickstart docs.getbindu.com/bindu/gateway/quickstart
Multi-agent planning docs.getbindu.com/bindu/gateway/multi-agent
Recipes (progressive-disclosure playbooks) docs.getbindu.com/bindu/gateway/recipes
Identity (DID signing, Hydra) docs.getbindu.com/bindu/gateway/identity
Production deployment docs.getbindu.com/bindu/gateway/production
API reference docs.getbindu.com/api/introduction

For a runnable multi-agent demo, see examples/gateway_test_fleet/ — five small agents on local ports, one gateway, one query.


Supported frameworks and examples

Bring whichever agent framework you already like. You hand Bindu a handler; it gives you a signed A2A microservice. Same flow regardless of what's inside the handler.


Language Frameworks tested in this repo
Python AG2 · Agno · CrewAI · Hermes Agent · LangChain · LangGraph · Notte
TypeScript OpenAI SDK · LangChain.js
Kotlin OpenAI Kotlin SDK
Any other language via the gRPC core — add an SDK in a few hundred lines

Compatible with any LLM provider that speaks the OpenAI or Anthropic API: OpenRouter (100+ models), OpenAI, MiniMax, and others.


A handful of examples to get you started

Five that cover the spectrum of what Bindu can do. All 20+ runnable examples live under examples/.

Example What it shows
Agent Swarm Multi-agent collaboration — a small "society" of Agno agents delegating work to each other.
Premium Advisor x402 payments — caller has to pay USDC on Base before the handler runs.
Hermes via Bindu Third-party framework interop — Nous Research's Hermes agent bindufied in ~90 lines.
Gateway Test Fleet Five small agents + one gateway — the multi-agent orchestration story end-to-end.
TypeScript OpenAI Agent Polyglot proof — a TS agent bindufied with the Bindu TS SDK; no Python to write.

See the full catalog: examples/ — 20+ agents covering CSV analysis, PDF Q&A, speech-to-text, web scraping, cybersecurity newsletters, multi-lingual collab, blog writing, and more.

Missing a framework you use? Open an issue or ask on Discord.


Demo

A built-in chat UI is available at http://localhost:5173 after running cd frontend && npm run dev.

Bindu agent UI


Core features

Everything below is optional and modular — the minimal install is just the A2A server. Each row links to a dedicated guide in docs/.


Identity & access

Feature Guide
Decentralized Identifiers (DIDs) DID.md
Authentication (Ory Hydra OAuth2) AUTHENTICATION.md

Protocol & infrastructure

Feature Guide
Skills system SKILLS.md
Agent negotiation NEGOTIATION.md
Push notifications NOTIFICATIONS.md
PostgreSQL storage STORAGE.md
Redis scheduler SCHEDULER.md
Language-agnostic via gRPC GRPC_LANGUAGE_AGNOSTIC.md

Commerce & reach

Feature Guide
x402 payments (USDC on Base) PAYMENT.md
Tunneling (local dev only) TUNNELING.md

Reliability & operations

Feature Guide
Retry with exponential backoff Retry docs
Observability (OpenTelemetry, Sentry) OBSERVABILITY.md
Health check and metrics HEALTH_METRICS.md

Testing

Bindu targets 70% test coverage (goal: 80%+):

uv run pytest tests/unit/ -v                                    # fast unit tests
uv run pytest tests/integration/grpc/ -v -m e2e                 # gRPC E2E
uv run pytest -n auto --cov=bindu --cov-report=term-missing    # full suite

CI runs unit tests, gRPC E2E, and TypeScript SDK build on every PR. See .github/workflows/ci.yml.


Known issues

If you're running Bindu in production, read bugs/known-issues.md first. It's a per-subsystem catalog with workarounds. Postmortems for fixed bugs live under bugs/core/, bugs/gateway/, bugs/sdk/, and bugs/frontend/.

Current high-severity items:

Subsystem Slug Symptom
Core x402-middleware-fails-open-on-body-parse Malformed JSON body bypasses payment check
Core x402-no-replay-prevention One payment buys unlimited work until validBefore
Core x402-no-signature-verification EIP-3009 signature is never verified
Core x402-balance-check-skipped-on-missing-contract-code Misconfigured RPC silently skips balance check
Gateway context-window-hardcoded Compaction threshold assumes a 200k-token window
Gateway poll-budget-unbounded-wall-clock sendAndPoll can stall 5 minutes per tool call
Gateway no-session-concurrency-guard Two /plan calls on the same session tangle histories

Found a new issue? Open a GitHub Issue referencing the slug (e.g. "Fixes context-window-hardcoded"). Fixed one? Remove its entry from known-issues.md and add a dated postmortem — see bugs/README.md for the template.


Troubleshooting

Common issues
Issue Fix
uv: command not found Restart your shell after installing uv.
Python version not supported Install Python 3.12+ from python.org or via pyenv.
bindu: command not found Activate your virtualenv: source .venv/bin/activate.
Port 3773 already in use Set BINDU_PORT=4000, or override with BINDU_DEPLOYMENT_URL=http://localhost:4000.
ModuleNotFoundError Run uv sync --dev.
Pre-commit fails Run pre-commit run --all-files.
Permission denied (macOS) xattr -cr . to clear extended attributes.

Reset the environment:

rm -rf .venv && uv venv --python 3.12.9 && uv sync --dev

On Windows PowerShell you may need Set-ExecutionPolicy RemoteSigned -Scope CurrentUser.


Contributing

Clone, set up, and run the pre-commit hooks:

git clone https://github.com/getbindu/Bindu.git
cd Bindu
uv venv --python 3.12.9 && source .venv/bin/activate
uv sync --dev
pre-commit run --all-files

Discussion and help happen on Discord. See .github/contributing.md for the full guide. There's an open list of agents we'd like to see bindufied — contribute one.


Maintainers

Raahul Dutta
Raahul Dutta
Paras Chamoli
Paras Chamoli
Chandan
Chandan

Acknowledgements

Bindu stands on the shoulders of:

FastA2A · A2A · x402 · Hugging Face chat-ui · 12 Factor Agents · OpenCode · OpenMoji · ASCII Space Art


License

Apache 2.0. See LICENSE.md.

Star history

Crafted between Amsterdam and India · open source under Apache 2.0 · getbindu.com