Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.getbindu.com/llms.txt

Use this file to discover all available pages before exploring further.

Everything so far has been running on localhost. The agents accept unsigned requests because "auth": { "type": "none" } tells the gateway not to sign them. That’s fine for development - there’s no attacker between you and your own laptop.
In production it isn’t. If your gateway calls an agent over the public internet, anyone who can reach that agent’s URL can pretend to be your gateway. They can feed it garbage, steal its output, or (if the agent does anything side-effectful like sending email or moving money) cause real damage.
The fix is: the gateway gets a cryptographic identity and signs every outbound request. Agents verify the signature before processing. If an attacker tries to forge a request, the signature won’t match the gateway’s registered public key, and the agent rejects the call.

What’s a DID?

DID stands for Decentralized Identifier. It’s a string that looks like:
did:bindu:alice_at_example_com:gateway:abc123
It uniquely identifies an agent or a gateway. Paired with it is an Ed25519 key pair - a private key (secret, 32 bytes, lives in an env var) and a public key (safe to share, published at a .well-known URL).
You sign outbound requests with the private key. Recipients verify with the public key. Standard public-key cryptography - what puts the green lock in your browser.
Read Decentralized Identifiers (DIDs) for the full intuition. The milk-truck analogy there is the gentlest on-ramp we know.

The three env vars

Generate a private key seed (once, keep it secret):
python3 -c 'import os, base64; print(base64.b64encode(os.urandom(32)).decode())'
Add to gateway/.env.local:
gateway/.env.local
BINDU_GATEWAY_DID_SEED=<paste the output>
BINDU_GATEWAY_AUTHOR=you@example.com
BINDU_GATEWAY_NAME=gateway
That’s enough for the gateway to have an identity. It won’t be useful yet - we also need to tell the gateway where to publish its public key so agents can fetch it. That’s the next piece.

Hydra - the registration server

Ory Hydra is an open-source OAuth 2.0 / OIDC server. You run your own Hydra (or share one with your peers) — there is no Bindu-hosted public Hydra. The gateway integrates with whichever Hydra you point it at.
How it works: the gateway registers itself as an OAuth client at boot using its DID as the client_id and a secret derived deterministically from the seed. The registry stores your DID + public key in the client’s metadata; agents that want to verify your signatures fetch your /.well-known/did.json.
Two more env vars, pointing at your Hydra:
gateway/.env.local
BINDU_GATEWAY_HYDRA_ADMIN_URL=https://hydra-admin.<your-domain>
BINDU_GATEWAY_HYDRA_TOKEN_URL=https://hydra.<your-domain>/oauth2/token
# Optional, space-separated. Defaults to "openid offline agent:read agent:write"
# BINDU_GATEWAY_HYDRA_SCOPE="openid offline agent:read agent:write"
Partial Hydra config fails fast. Both URLs must be set together, or neither. If you set only one, the gateway aborts boot with the error Partial Hydra config — set both or neither.
Restart npm run dev. You’ll now see:
[bindu-gateway] DID identity loaded: did:bindu:you_at_example_com:gateway:<uuid>
[bindu-gateway] public key (base58): 6MkjQ2r...
[bindu-gateway] registering with Hydra at https://hydra-admin.<your-domain>...
[bindu-gateway] Hydra registration confirmed for did:bindu:...
[bindu-gateway] publishing DID document at /.well-known/did.json
[bindu-gateway] listening on http://0.0.0.0:3774
[bindu-gateway] session mode: stateless
Three things just happened:
1

The gateway derived a DID and public key from your seed.

Deterministic - same seed always produces the same DID.
2

It POSTed to Hydra's admin API to register.

As an OAuth client, with its DID as the client_id and its public key in the metadata. Idempotent - safe to restart as many times as you like.
3

It wired up a client-credentials token provider.

The first call to a did_signed peer triggers an OAuth client_credentials exchange against BINDU_GATEWAY_HYDRA_TOKEN_URL; the resulting access token is cached in memory and proactively refreshed 30 seconds before expiry. Concurrent callers during a refresh share the same in-flight fetch — Hydra is hit at most once per refresh window.
The gateway also published its own DID document at http://localhost:3774/.well-known/did.json. Curl it:
curl http://localhost:3774/.well-known/did.json
{
  "@context": ["https://www.w3.org/ns/did/v1", "https://getbindu.com/ns/v1"],
  "id": "did:bindu:you_at_example_com:gateway:abc123",
  "authentication": [
    {
      "id": "did:bindu:you_at_example_com:gateway:abc123#key-1",
      "type": "Ed25519VerificationKey2020",
      "controller": "did:bindu:you_at_example_com:gateway:abc123",
      "publicKeyBase58": "6MkjQ2r..."
    }
  ]
}
That’s your gateway’s public key, served over HTTP, signed by no one but vouching for itself. Any agent that receives a signed request claiming to be from your DID can fetch this document, extract the public key, and verify the signature.

Flipping a peer to signed mode

Change the /plan request:
{
  "name": "research",
  "endpoint": "https://research.example.com",
  "auth": { "type": "did_signed" },
  "trust": { "verifyDID": true, "pinnedDID": "did:bindu:..." },
  "skills": [{ "id": "web_research" }]
}
No token or envVar on auth — the gateway will use its own Hydra token automatically. The optional trust block is a separate decision: it tells the gateway to verify incoming signatures from this peer (and, if pinnedDID is set, to reject responses signed by any other key). You can sign outbound without verifying inbound, or verify inbound without signing outbound — they’re independent.
Re-fire. On the wire, three things change:

Body signed

The gateway wraps the serialized JSON-RPC body in {"body": <body>, "did": <gateway DID>, "timestamp": <unix seconds>} and serializes that envelope with a Python-compatible sorted-keys serializer (matches the reference agent’s json.dumps(payload, sort_keys=True) byte-for-byte, spaces and all — see gateway/src/bindu/identity/local.ts). Those exact UTF-8 bytes are then signed with the gateway’s Ed25519 private key.

Three signing headers attached

X-DID (your gateway’s DID), X-DID-Signature (base58 Ed25519 signature), and X-DID-Timestamp (unix seconds) go on every signed call.

OAuth token attached

Authorization: Bearer <token> — the cached client-credentials token from Hydra. Auto-refreshed 30s before expiry.
On the receiving side, the agent:
1

Resolves the caller's DID to a public key.

Via the gateway’s /.well-known/did.json or a cached DID→key mapping.
2

Verifies the X-DID-Signature against the request body.

Using the gateway’s public key (Ed25519VerificationKey2020, base58-encoded in the DID document’s authentication block).
3

Introspects the bearer token against Hydra.

Confirms it’s real and unexpired.
4

Only then processes the request.

Otherwise returns HTTP 401.
Audit trail. Each peer call’s verification result is surfaced inline in the SSE stream — the <remote_content> envelope you saw in the quickstart carries a verified attribute with four possible values:
  • "yes" — the peer signed and the signature checked out.
  • "no" — at least one signed artifact failed verification. The task is also marked failed.
  • "unsigned" — verification ran but the peer attached no signature. Treat the body as unverified hearsay.
  • "unknown" — verification didn’t run (no trust.verifyDID, no resolvable DID, or DID-document resolution failed).
auth.type and trust.verifyDID are two separate knobs: auth.type: "did_signed" controls how the gateway authenticates itself on outbound calls, while trust.verifyDID: true (with an optional trust.pinnedDID) is what asks the gateway to verify the peer’s signatures on the way back. Turn both on against a properly-configured peer and you’ll see verified="yes" on every successful call.
If any of those three checks fail - signature mismatch, unknown DID, invalid token - the agent returns HTTP 401 and the gateway surfaces that as event: task.finished with state: failed and a useful error message.

Two modes: auto vs manual

One Hydra, shared by the gateway and its peers, handles all the registration and token exchange.
gateway/.env.local
BINDU_GATEWAY_DID_SEED=<seed>
BINDU_GATEWAY_AUTHOR=you@example.com
BINDU_GATEWAY_NAME=gateway
BINDU_GATEWAY_HYDRA_ADMIN_URL=https://hydra-admin.<your-domain>
BINDU_GATEWAY_HYDRA_TOKEN_URL=https://hydra.<your-domain>/oauth2/token
Request side:
"auth": { "type": "did_signed" }
Use this unless you have a specific reason not to.

Chapter takeaway

Local dev

Keep auth.type: "none". No cryptography needed.

Anything across a network you don't control

Configure the DID identity and flip peers to did_signed. The token and signature are automatic once the env vars are set; you never touch crypto code.
If something in this chapter isn’t working, the most common cause is a missing env var - the gateway logs exactly which one on boot when a partial config is detected.
Last stop: Going to production → Sunflower LogoTo maintain trust between agents - every request is signed.