> ## 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.

# Identity & Signed Calls

> Give the gateway a DID - every outbound call gets a cryptographic signature that agents can verify.

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.

<Warning>
  **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.
</Warning>

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).

<Info>
  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.
</Info>

<Tip>
  Read [Decentralized Identifiers (DIDs)](/bindu/learn/did/overview) for the full intuition. The milk-truck analogy there is the gentlest on-ramp we know.
</Tip>

***

## The three env vars

Generate a private key seed (once, keep it secret):

```bash theme={null}
python3 -c 'import os, base64; print(base64.b64encode(os.urandom(32)).decode())'
```

Add to `gateway/.env.local`:

```bash gateway/.env.local theme={null}
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](https://www.ory.sh/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.

<Info>
  **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`.
</Info>

Two more env vars, pointing at your Hydra:

```bash gateway/.env.local theme={null}
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"
```

<Note>
  **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`.
</Note>

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:

<Steps>
  <Step title="The gateway derived a DID and public key from your seed.">
    Deterministic - same seed always produces the same DID.
  </Step>

  <Step title="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.
  </Step>

  <Step title="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.
  </Step>
</Steps>

The gateway also published its own DID document at `http://localhost:3774/.well-known/did.json`. Curl it:

```bash theme={null}
curl http://localhost:3774/.well-known/did.json
```

```json theme={null}
{
  "@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..."
    }
  ]
}
```

<Tip>
  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.
</Tip>

***

## Flipping a peer to signed mode

Change the `/plan` request:

```json theme={null}
{
  "name": "research",
  "endpoint": "https://research.example.com",
  "auth": { "type": "did_signed" },
  "trust": { "verifyDID": true, "pinnedDID": "did:bindu:..." },
  "skills": [{ "id": "web_research" }]
}
```

<Info>
  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.
</Info>

Re-fire. On the wire, three things change:

<CardGroup cols={3}>
  <Card title="Body signed" icon="signature">
    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`](https://github.com/GetBindu/Bindu/blob/main/gateway/src/bindu/identity/local.ts)). Those exact UTF-8 bytes are then signed with the gateway's Ed25519 private key.
  </Card>

  <Card title="Three signing headers attached" icon="file-shield">
    `X-DID` (your gateway's DID), `X-DID-Signature` (base58 Ed25519 signature), and `X-DID-Timestamp` (unix seconds) go on every signed call.
  </Card>

  <Card title="OAuth token attached" icon="key">
    `Authorization: Bearer <token>` — the cached client-credentials token from Hydra. Auto-refreshed 30s before expiry.
  </Card>
</CardGroup>

On the receiving side, the agent:

<Steps>
  <Step title="Resolves the caller's DID to a public key.">
    Via the gateway's `/.well-known/did.json` or a cached DID→key mapping.
  </Step>

  <Step title="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).
  </Step>

  <Step title="Introspects the bearer token against Hydra.">
    Confirms it's real and unexpired.
  </Step>

  <Step title="Only then processes the request.">
    Otherwise returns HTTP 401.
  </Step>
</Steps>

<Tip>
  **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.
</Tip>

<Warning>
  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.
</Warning>

***

## Two modes: auto vs manual

<Tabs>
  <Tab title="Auto mode (default)">
    One Hydra, shared by the gateway and its peers, handles all the registration and token exchange.

    ```bash gateway/.env.local theme={null}
    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:

    ```json theme={null}
    "auth": { "type": "did_signed" }
    ```

    Use this unless you have a specific reason not to.
  </Tab>

  <Tab title="Manual mode (federated)">
    For federated setups where different peers trust different Hydra instances.

    **Config** - set only the DID env vars (seed, author, name), not the Hydra URLs. For each peer, pre-register your gateway's DID with *that peer's* Hydra (out of band) and obtain an access token. Store the tokens in env vars per peer.

    **Request side** - tell the gateway which env var to read with `tokenEnvVar`:

    ```json theme={null}
    "auth": {
      "type": "did_signed",
      "tokenEnvVar": "PEER_A_TOKEN"
    }
    ```

    A peer-scoped `tokenEnvVar` wins over the auto provider, so you can run a hybrid setup: most peers on auto, a few on per-peer manual tokens.
  </Tab>
</Tabs>

***

## Chapter takeaway

<CardGroup cols={2}>
  <Card title="Local dev" icon="laptop-code">
    Keep `auth.type: "none"`. No cryptography needed.
  </Card>

  <Card title="Anything across a network you don't control" icon="earth-americas">
    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.
  </Card>
</CardGroup>

<Tip>
  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.
</Tip>

Last stop: **[Going to production →](/bindu/gateway/production)**

<span className="brand-quote">
  <img src="https://mintcdn.com/pebbling/x2BFCGEbWywg69kQ/logo/light.svg?fit=max&auto=format&n=x2BFCGEbWywg69kQ&q=85&s=a69e734bb925e661b3c2ca2a20a050a9" alt="Sunflower Logo" width="32" className="clean-icon" data-path="logo/light.svg" />

  <span className="brand-quote-text">
    To maintain trust between agents -{" "}
    <span className="brand-quote-highlight">every request is signed</span>.
  </span>
</span>
