> ## Documentation Index
> Fetch the complete documentation index at: https://docs.pylonsync.com/llms.txt
> Use this file to discover all available pages before exploring further.

# SIWE (Sign-In With Ethereum)

> EIP-4361 sign-in keyed on wallet addresses — secp256k1 ECDSA + Keccak-256 recovery, no third-party wallet service.

Pylon ships a complete [EIP-4361](https://eips.ethereum.org/EIPS/eip-4361) (Sign-In With Ethereum) implementation in the binary. Users prove ownership of their Ethereum address by signing a structured message; Pylon recovers the address from the signature and mints a session. No Magic.link, no WalletConnect server, no third-party SIWE service.

## What it does

1. Frontend asks Pylon for a fresh nonce for the user's address.
2. Frontend builds an EIP-4361 message (`Sign in to acme.com\n\nAddress: 0x...\n...nonce: ...\n...`) and asks the user's wallet to sign it via `personal_sign`.
3. Pylon recovers the address from the signature using secp256k1 ECDSA + Keccak-256 (Ethereum's variant), validates the message's domain + nonce + expiry, and mints a session keyed on the wallet.

## Endpoints

| Endpoint                | Method | Auth | Purpose                                      |
| ----------------------- | ------ | ---- | -------------------------------------------- |
| `/api/auth/siwe/nonce`  | GET    | none | Mint a single-use nonce for `?address=0x...` |
| `/api/auth/siwe/verify` | POST   | none | Verify the signed message; mint session      |

## Schema

The user entity needs a `walletAddress` field:

```typescript theme={null}
import { entity, field } from "@pylonsync/sdk";

export const User = entity("User", {
  email: field.string().optional(),
  walletAddress: field.string().optional().unique(),  // 0x + 40 hex chars, lowercase
  displayName: field.string(),
  createdAt: field.string(),
});
```

`unique` so two accounts can't claim the same address.

## Sign-in flow

### 1. Request a nonce

```bash theme={null}
curl 'https://your-app/api/auth/siwe/nonce?address=0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0'
```

Response:

```json theme={null}
{ "nonce": "abc123def456..." }
```

The nonce is bound to the address — using a nonce minted for one address against a different address fails verify. Single-use, short-lived.

Errors:

| Status | Code              | Reason                          |
| ------ | ----------------- | ------------------------------- |
| 400    | `INVALID_ADDRESS` | Not `0x` + exactly 40 hex chars |

### 2. Build + sign the message

The frontend constructs an EIP-4361 message and asks the wallet to sign it (MetaMask, WalletConnect, Coinbase Wallet, etc.):

```javascript theme={null}
import { SiweMessage } from "siwe";  // npm install siwe

const { nonce } = await fetch(
  `/api/auth/siwe/nonce?address=${address}`,
).then((r) => r.json());

const message = new SiweMessage({
  domain: window.location.host,        // must match Pylon's Host header
  address,
  statement: "Sign in to Acme",
  uri: window.location.origin,
  version: "1",
  chainId: 1,
  nonce,
  issuedAt: new Date().toISOString(),
});

const prepared = message.prepareMessage();
const signature = await ethereum.request({
  method: "personal_sign",
  params: [prepared, address],
});
```

### 3. Verify

```bash theme={null}
curl -X POST https://your-app/api/auth/siwe/verify \
  -H 'Content-Type: application/json' \
  -d '{
    "message": "<EIP-4361 plaintext message>",
    "signature": "0x...",
    "displayName": "vitalik.eth"
  }'
```

Response on success:

```json theme={null}
{
  "token": "pylon_...",
  "user_id": "usr_xyz",
  "address": "0x742d35cc6634c0532925a3b844bc9e7595f0beb0",
  "expires_at": 1737592000
}
```

`displayName` is optional — used only when Pylon creates a new User row for a first-time address. Default is the short-form `0x742d…beb0`.

Errors:

| Status | Code                 | Reason                                                                                                |
| ------ | -------------------- | ----------------------------------------------------------------------------------------------------- |
| 400    | `SIWE_BAD_MESSAGE`   | EIP-4361 message couldn't be parsed                                                                   |
| 401    | `SIWE_VERIFY_FAILED` | Signature didn't recover to the message's address, nonce unknown / consumed, domain mismatch, expired |

## Verification details

Pylon validates:

* **Signature recovery** — secp256k1 ECDSA + Keccak-256 hash of the EIP-191 prefix (`\x19Ethereum Signed Message:\n<len>` + message) recovers a 20-byte address. Must match `message.address` case-insensitively.
* **Nonce** — must exist in the nonce store, must be bound to `message.address`, single-use (consumed on first verify).
* **Domain** — `message.domain` must match the request's `Host` header. Prevents replay across deployments.
* **Issued / expiration** — if `message.expirationTime` is set, must be in the future. If `message.notBefore` is set, must be in the past.

The recovered address is **lowercased** before lookup and storage so checksummed `0x742d35Cc...` and lowercase `0x742d35cc...` collapse to the same User row.

## Security guarantees

* **secp256k1 + Keccak-256** via the `k256` crate — same primitives Ethereum uses, audited.
* **Nonce binding to address** — using `nonce-for-A` to sign-in-as-B fails verify.
* **Single-use nonces** — consumed on first verify regardless of success.
* **Domain pinning** — `message.domain` validated against `Host` header; replay across deployments rejected.
* **Expiration enforcement** — `expirationTime` and `notBefore` honored.
* **EIP-191 prefix** properly applied — defeats signature reuse from any other Ethereum-signed payload.

## Where to go next

* **[Sessions](/auth/sessions)** — what `/siwe/verify` mints
* **[OAuth](/auth/oauth)** — other identity providers for the same user (link multiple sign-in methods to one account)
