Skip to main content
ProofBridge uses wallet-based authentication — no usernames or passwords. For EVM wallets, this follows the Sign-In With Ethereum (SIWE) standard. For Stellar wallets, it follows the SEP-10 challenge-response protocol. Both flows produce the same result: a short-lived access token and a long-lived refresh token you can use to authenticate subsequent requests.
All three authentication endpoints are unauthenticated — do not include an Authorization header when calling them.

Authentication flow

1

Request a challenge

Call POST /v1/auth/challenge with your wallet address and chain kind. The server returns a signed challenge (a SIWE message for EVM or a base64-encoded transaction for Stellar) that you must sign with your wallet’s private key.
2

Sign the challenge

Sign the challenge message locally using your wallet. For EVM, use eth_signMessage or equivalent. For Stellar, co-sign the returned XDR transaction.
3

Submit the signed message

Call POST /v1/auth/login with your address, chain kind, and the signature (EVM) or co-signed transaction (Stellar). The server verifies the signature and returns access and refresh tokens.
4

Use the access token

Include Authorization: Bearer <access> in the headers of every authenticated request.
5

Refresh when expired

When the access token expires, call POST /v1/auth/refresh with your refresh token to obtain a new access token without re-signing.

POST /v1/auth/challenge

Request a nonce challenge for your wallet address. The returned value is the message (EVM) or transaction (Stellar) that you must sign.

Request body

address
string
required
Your wallet address. Use 0x-prefixed 20-byte hex for EVM (e.g., 0x742d35Cc6634C0532925a3b844Bc454e4438f44e), or a G-strkey for Stellar (e.g., GAHJJJKMOKYE4RVPZEWZTKH5FVI4PA3VL7GK2LFNUBSGBV).
chainKind
string
required
The chain family the wallet belongs to. Must be "EVM" or "STELLAR".

Response fields

chainKind
string
required
Echoed chain kind: "EVM" or "STELLAR".
address
string
required
Echoed wallet address in canonical form.
expiresAt
string
required
ISO 8601 timestamp after which the challenge is no longer valid.
nonce
string
Unique nonce embedded in the SIWE message. Returned for EVM challenges only.
domain
string
SIWE domain. Returned for EVM challenges only.
uri
string
SIWE URI. Returned for EVM challenges only.
transaction
string
Server-signed SEP-10 challenge transaction in base64 XDR format. Returned for Stellar challenges only.
networkPassphrase
string
Stellar network passphrase. Returned for Stellar challenges only.
{
  "address": "0x742d35Cc6634C0532925a3b844Bc454e4438f44e",
  "chainKind": "EVM"
}

POST /v1/auth/login

Submit your signed challenge to receive JWT tokens. The request body differs slightly depending on chainKind.

Request body

chainKind
string
required
Must match the chainKind used when requesting the challenge: "EVM" or "STELLAR".
message
string
The SIWE message string returned from the challenge step. Required when chainKind is "EVM".
signature
string
Your wallet’s signature over the SIWE message. Required when chainKind is "EVM".
transaction
string
The co-signed SEP-10 challenge transaction in base64 XDR format. Required when chainKind is "STELLAR".

Response fields

user
object
required
tokens
object
required
{
  "chainKind": "EVM",
  "message": "app.pfbridge.xyz wants you to sign in with your Ethereum account:\n0x742d35Cc6634C0532925a3b844Bc454e4438f44e\n\nSign in to ProofBridge\n\nURI: https://app.pfbridge.xyz\nVersion: 1\nChain ID: 1\nNonce: a3f9b2c1d4e5f607\nIssued At: 2026-04-15T12:00:00.000Z",
  "signature": "0x4d2a1c3e5f6b7a8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d01"
}

POST /v1/auth/refresh

Exchange a valid refresh token for a new access token. Use this when your access token expires to avoid forcing the user to re-sign.

Request body

refresh
string
required
The refresh token returned from the login response.

Response fields

tokens
object
required
{
  "refresh": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1NTBlODQwMC1lMjliLTQxZDQtYTcxNi00NDY2NTU0NDAwMDAiLCJ0eXBlIjoicmVmcmVzaCJ9.sig"
}

POST /v1/auth/link

Attach a second wallet (on the other chain kind) to the currently authenticated session. Used for the dual-chain sign-in flow described in Connect your wallet. The call takes the same signed-challenge payload as /v1/auth/login but runs against the existing user instead of creating or looking up one by address. On success, the wallet is recorded on the caller’s User record and no new session is issued — the caller’s current access token keeps working.
Authorization
string
required
Bearer <accessToken> — the session token from the wallet that is already signed in.

Request body

Identical to POST /v1/auth/login:
  • EVM: { "message": "...", "signature": "0x...", "chainKind": "EVM" }
  • Stellar: { "transaction": "<signed-XDR>", "chainKind": "STELLAR" }

Behaviour

  • The chainKind must be the one not currently linked to the account. Re-linking the same chain kind returns 409 Conflict.
  • Linking an address that already belongs to a different account returns 409 Conflict — disconnect it from the other account first.
  • On success, returns the updated user record. No new tokens are issued; the existing access / refresh tokens continue to work and now represent both wallets.

JavaScript example: full authentication flow

The following example shows the complete EVM authentication flow using ethers.js and the Fetch API.
import { ethers } from "ethers";

async function authenticate(walletAddress) {
  const BASE_URL = "https://api.pfbridge.xyz";

  // Step 1: Request a challenge
  const challengeRes = await fetch(`${BASE_URL}/v1/auth/challenge`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ address: walletAddress, chainKind: "EVM" }),
  });
  const { message: siweMessage } = await challengeRes.json();

  // Step 2: Sign the challenge with the connected wallet
  const provider = new ethers.BrowserProvider(window.ethereum);
  const signer = await provider.getSigner();
  const signature = await signer.signMessage(siweMessage);

  // Step 3: Submit the signed message
  const loginRes = await fetch(`${BASE_URL}/v1/auth/login`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      chainKind: "EVM",
      message: siweMessage,
      signature,
    }),
  });
  const { tokens } = await loginRes.json();

  // Step 4: Store tokens and use the access token in future requests
  localStorage.setItem("pb_access", tokens.access);
  localStorage.setItem("pb_refresh", tokens.refresh);

  return tokens.access;
}

// Step 5: Refresh when the access token expires
async function refreshTokens() {
  const BASE_URL = "https://api.pfbridge.xyz";
  const refresh = localStorage.getItem("pb_refresh");

  const res = await fetch(`${BASE_URL}/v1/auth/refresh`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ refresh }),
  });
  const { tokens } = await res.json();

  localStorage.setItem("pb_access", tokens.access);
  localStorage.setItem("pb_refresh", tokens.refresh);

  return tokens.access;
}
Store tokens securely. Do not persist access tokens in localStorage in production environments — use httpOnly cookies or an in-memory store to reduce XSS exposure.