Skip to main content

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

  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

EndpointMethodAuthPurpose
/api/auth/siwe/nonceGETnoneMint a single-use nonce for ?address=0x...
/api/auth/siwe/verifyPOSTnoneVerify 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:
StatusCodeReason
400INVALID_ADDRESSNot 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:
StatusCodeReason
400SIWE_BAD_MESSAGEEIP-4361 message couldn’t be parsed
401SIWE_VERIFY_FAILEDSignature 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).
  • Domainmessage.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 pinningmessage.domain validated against Host header; replay across deployments rejected.
  • Expiration enforcementexpirationTime 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)