# auth.md

> Agent authentication and registration for Diamond Hands — a self-custodial Bitcoin
> line of credit. Lock native BTC in an isolated L1 vault, mint UCD against it, keep
> the Bitcoin.

This document is for **autonomous agents** that operate a Bitcoin-collateralized UCD
position on behalf of the person who owns the wallet. It is generated from this
deployment's live configuration, so the endpoints and the chain id below are the ones
actually being served.

## Agent service

| | |
|---|---|
| Protocol | Model Context Protocol (MCP), streamable HTTP |
| Endpoint | `https://mcp.diamondhands.credit/mcp` |
| Network | Ethereum mainnet (chain id 1) — real BTC, real UCD |
| Protected resource metadata | https://mcp.diamondhands.credit/.well-known/oauth-protected-resource |
| Authorization server metadata | https://mcp.diamondhands.credit/.well-known/oauth-authorization-server |

```json
{
  "mcpServers": {
    "diamond-hands": {
      "type": "http",
      "url": "https://mcp.diamondhands.credit/mcp"
    }
  }
}
```

## agent_auth

```json
{
  "agent_auth": {
    "skill": "operate-position",
    "register_uri": "https://mcp.diamondhands.credit/register",
    "authorization_servers": [
      "https://mcp.diamondhands.credit/"
    ],
    "resource": "https://mcp.diamondhands.credit/mcp",
    "methods": [
      {
        "type": "oauth2",
        "registration": "rfc7591-dynamic-client-registration",
        "register_uri": "https://mcp.diamondhands.credit/register",
        "authorization_uri": "https://mcp.diamondhands.credit/authorize",
        "token_uri": "https://mcp.diamondhands.credit/token",
        "revocation_uri": "https://mcp.diamondhands.credit/revoke",
        "grant_types_supported": [
          "authorization_code",
          "refresh_token"
        ],
        "response_types_supported": [
          "code"
        ],
        "code_challenge_methods_supported": [
          "S256"
        ],
        "token_endpoint_auth_methods_supported": [
          "client_secret_post",
          "none"
        ],
        "bearer_methods_supported": [
          "header"
        ],
        "identity_types_supported": [
          "wallet_signature"
        ],
        "wallet_signature": {
          "standard": "EIP-4361",
          "chain_id": 1,
          "credential_types_supported": [
            "access_token",
            "refresh_token"
          ]
        }
      }
    ]
  }
}
```

### The registration flow, end to end

1. **Register.** `POST https://mcp.diamondhands.credit/register` — RFC 7591 dynamic client registration,
   open, no pre-shared credential. You receive a `client_id`.
2. **Authorize.** Send the user to `https://mcp.diamondhands.credit/authorize` with `response_type=code`,
   your `client_id`, `redirect_uri`, and a PKCE `code_challenge` (`S256` —
   required).
3. **Sign in.** One Sign-In-with-Ethereum signature (EIP-4361) over a message this
   server minted for that flow. Nothing you supply is parsed back — the server verifies
   against its own canonical text, and pins the chain id itself. A person signs this in
   a browser wallet extension; an agent holding its own key signs the same message
   through the flow's JSON endpoints. **Mobile WalletConnect sign-in is not enabled on this deployment** — browser extension or own-key only.
4. **Exchange.** `POST https://mcp.diamondhands.credit/token` with the code and your `code_verifier`. You
   get an access token and a refresh token.
5. **Call.** Send the access token to `https://mcp.diamondhands.credit/mcp` as
   `Authorization: Bearer <token>`. Unauthenticated calls answer `401` with a
   JSON-RPC error, not a redirect.

### What the credential is, and is not

The **wallet address is the account.** There are no passwords, no email addresses, and
no user database. Registration creates an OAuth client, not a Diamond Hands account —
there is nothing to delete, and no personal data is collected.

The access token proves **session identity only**. It can read what that address could
already read, and it can *stage* a fund-moving operation — arm a mint, a repayment, a
withdrawal — but it can never complete one: each of those terminates in a wallet
signature over that specific operation, which the token cannot produce. So it is not a
read-only credential, and it is not a spending one either. A stolen token cannot spend.

- Access token: ~1 hour. Refresh token: rotating, expires after inactivity.
- All authorization state is held **in memory**. A service restart ends every session
  and the user signs in again. Nothing is persisted at rest.
- `scopes_supported` is empty and tokens are minted with no scopes — this
  authorization server does not implement scoped access. Do not request a scope; you
  will not receive one.

### Who holds the wallet — read this before you rely on the approval step

This protocol authenticates **addresses, not people**. It cannot tell whether a
signature came from a person clicking a wallet extension or from software holding a key,
and it does not try to. Two arrangements are supported, and they have **materially
different guarantees**. Be clear which one you are.

**1. You act for a person, on their wallet.** The human owns the position and signs each
operation themselves. This is the arrangement the approval page is designed for: every
fund-moving step raises a card the person reads and approves, and you cannot proceed
without them. Your token gives you reads and the ability to *stage* a ceremony — never
to complete one.

**2. You hold your own key and own your own position.** Nothing here treats you
differently: you sign in with your key, you create and fund your own vault, you borrow
against your own BTC. But then **you are the approver.** The approval card's "Approve
ONLY if you initiated this exact BTC withdrawal" is addressed to a human reader, and
there is no human in this arrangement. Do not present that step to your operator as
independent oversight — it is not; you are satisfying it yourself.

What actually bounds arrangement 2 is **on-chain, not in this service**: a BTC
withdrawal can only go to an address already on the position's on-chain allowlist, and
adding one takes a separate, fully decodable transaction plus the registry's delay —
read live from the contract at execution (24 hours on mainnet today).
Treat that allowlist as your real safety boundary, and put an address on it
deliberately.

**3. Acting on someone else's position with your own key** — a delegated agent key — is
**not offered on this endpoint yet.** If you need it, do not attempt to approximate it
by holding a person's key; contact us.

If your operator has asked you to run unattended, you are in arrangement 2. Say so
plainly to them, and do not describe the approval step as human review.

### Flows this service does not offer

Stated plainly so a scanner does not infer otherwise: there is **no** ID-JAG
(`urn:ietf:params:oauth:token-type:id-jag`) identity-assertion flow, **no**
verified-email flow, and **no** anonymous flow. The only identity type is a wallet
signature.

## Public data needs no credential

Protocol data — live loans, terms, the peg stability module — is public. Start at
<https://app.diamondhands.credit/llms.txt>, or request any app page with
`Accept: text/markdown`.

## Local alternative

An agent on the user's own machine can skip this service and talk to the protocol
directly through the published MCP server, which carries the same tools:

- `npx -y @gvnrdao/dh-mcp` (requires Node ≥ 18)
- `curl -fsSL https://assets.gvnr.xyz/dh-mcp/install.sh | sh` (self-contained binary)
- Claude Desktop bundle and checksums: <https://assets.gvnr.xyz/dh-mcp/>

## Contact

Security contact and disclosure policy:
<https://app.diamondhands.credit/.well-known/security.txt>

Do not probe `POST /register` or `/authorize` during passive scans. This document,
the protected-resource metadata, and the authorization-server metadata are the safe
source of truth.
