Skip to content

Wallet Authentication

Agents authenticate with an EVM wallet signature. First-time wallets are auto-provisioned with a prepaid tenant.

  1. Agent creates or loads an EVM wallet.
  2. Agent requests a challenge from POST https://bridge.frameworks.network/auth/wallet-challenge or the public MCP tool request_wallet_challenge with its address and chain ID.
  3. Agent signs the returned message verbatim using EIP-191 personal_sign.
  4. Agent exchanges it once through POST /auth/wallet-login, GraphQL walletLogin, or MCP wallet headers.
  5. Server atomically consumes the challenge and creates a prepaid tenant if this is a new wallet.

The server returns an EIP-4361-style message similar to:

app.frameworks.network wants you to sign in with your Ethereum account:
0x...
Sign in to FrameWorks.
URI: https://app.frameworks.network
Version: 1
Chain ID: 1
Nonce: <server nonce>
Issued At: 2026-08-20T00:00:00Z
Expiration Time: 2026-08-20T00:05:00Z
  • Do not build or modify this message client-side.
  • Challenges expire after five minutes and can be consumed only once. Concurrent exchanges have one winner.
import { createWalletClient, http } from "viem";
import { privateKeyToAccount } from "viem/accounts";
const account = privateKeyToAccount("0x...");
const client = createWalletClient({ account, transport: http() });
const challenge = await fetch("https://bridge.frameworks.network/auth/wallet-challenge", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ address: account.address, chain_id: 1 }),
}).then((response) => response.json());
const signature = await client.signMessage({ message: challenge.message });
from eth_account import Account
from eth_account.messages import encode_defunct
import os
import requests
wallet = Account.from_key(os.environ["FRAMEWORKS_WALLET_PRIVKEY"])
challenge = requests.post(
"https://bridge.frameworks.network/auth/wallet-challenge",
json={"address": wallet.address, "chain_id": 1},
).json()
signed = Account.sign_message(
encode_defunct(text=challenge["message"]),
private_key=os.environ["FRAMEWORKS_WALLET_PRIVKEY"]
)
signature = signed.signature.hex()

For MCP and HTTP requests, pass wallet credentials in headers:

HeaderValue
X-Wallet-Address0x-prefixed Ethereum address
X-Wallet-SignatureEIP-191 personal_sign signature
X-Wallet-MessageThe exact server-issued challenge

For direct GraphQL integrations, request the challenge over REST and exchange it with walletLogin, then use Authorization: Bearer <jwt>. The REST login endpoint creates refreshable HttpOnly cookies for first-party browser sessions. An MCP wallet-header exchange returns X-Access-Token; switch to bearer/API-token auth because the challenge headers cannot be replayed.

Static MCP client configuration must use a bearer API token. Wallet headers are single-use bootstrap credentials, not permanent headers.

MCP also exposes authenticated list_linked_wallets, link_wallet, unlink_wallet, link_email, and activate_free_tier tools. Wallet linking requires a fresh challenge for the new address. Email linking sends the normal verification message; after verification, Free can be activated without a billing profile or Stripe/Mollie. The service serializes unlink attempts and refuses to remove the final wallet from an account that has no verified password sign-in method.

If you prefer a traditional token flow:

{
"mcpServers": {
"frameworks": {
"url": "https://bridge.frameworks.network/mcp",
"headers": {
"Authorization": "Bearer YOUR_API_TOKEN"
}
}
}
}

Get API tokens from Developer → Credentials in the dashboard.

When a new wallet authenticates for the first time:

  1. A new tenant is created with billing_model = 'prepaid'.
  2. A new user is created (email is NULL).
  3. Prepaid balance is initialized at €0.
  4. A wallet identity record links the wallet address to the user and tenant.

The agent can immediately read account://status. account_ready remains true for the authenticated control plane, while rated_work_ready is false and an INSUFFICIENT_BALANCE blocker applies only to rated work. The next actions are either top up prepaid credit or link/verify email and activate Free.

Account TypeBilling ModelTrust Level
Wallet-onlyprepaid (mandatory)Low — must fund balance first
Email (verified)Free postpaidMetered, zero-priced Free
Wallet + verified emailUser choicePrepaid, Free, or paid postpaid
  • Store private keys locally. Never send them to any API.
  • Only send wallet headers to *.frameworks.network domains.
  • Wallet signatures are verified server-side using ecrecover (EIP-191).
  • The server stores only a challenge hash and rejects expired or replayed challenges.
  • REST and MCP challenge/login calls share the public per-IP abuse limit; throttling returns 429, never 402.
  • x402 payments are not login credentials and never mint a session.