Skip to main content
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. The Bindu team runs one at hydra-admin.getbindu.com that any Bindu gateway or agent can register with.
How it works: You register once at boot; the registry stores your DID + public key; agents that want to talk to you fetch your public key by DID and verify your signatures with it.
Two more env vars:
gateway/.env.local
BINDU_GATEWAY_HYDRA_ADMIN_URL=https://hydra-admin.getbindu.com
BINDU_GATEWAY_HYDRA_TOKEN_URL=https://hydra.getbindu.com/oauth2/token
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.getbindu.com...
[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
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 exchanged client credentials for an OAuth access token.

That token is now cached in memory and refreshed 30 seconds before expiry.
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:
"auth": { "type": "did_signed" }
No token or envVar — the gateway will use its own Hydra token automatically.
Re-fire. On the wire, three things change:

Body signed

The gateway computes a canonical JSON representation of the body, signs it with its Ed25519 private key, and attaches the signature as X-Bindu-Signature (plus the DID in X-Bindu-DID).

OAuth token attached

Authorization: Bearer <token> — the agent introspects this against Hydra to confirm it’s real and unexpired.

Audit trail recorded

The signing result is written to Supabase on the task row: “at time T, gateway signed body hash H to reach agent DID D.”
On the receiving side, the agent:
1

Fetches the gateway's /.well-known/did.json.

Or uses a cached DID→key mapping from a previous interaction.
2

Verifies the signature matches the body.

Using the gateway’s public key.
3

Introspects the bearer token against Hydra.

Confirms it’s real and unexpired.
4

Only then processes the request.

Otherwise returns HTTP 401.
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.getbindu.com
BINDU_GATEWAY_HYDRA_TOKEN_URL=https://hydra.getbindu.com/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 →