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.
Pylon ships a complete 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
- Frontend asks Pylon for a fresh nonce for the user’s address.
- 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.
- 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:
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
curl 'https://your-app/api/auth/siwe/nonce?address=0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0'
Response:
{ "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.):
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
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:
{
"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 — what
/siwe/verify mints
- OAuth — other identity providers for the same user (link multiple sign-in methods to one account)