Wallet Authentication
Agents authenticate with an EVM wallet signature. First-time wallets are auto-provisioned with a prepaid tenant.
How It Works
Section titled “How It Works”- Agent creates or loads an EVM wallet.
- Agent requests a challenge from
POST https://bridge.frameworks.network/auth/wallet-challengeor the public MCP toolrequest_wallet_challengewith its address and chain ID. - Agent signs the returned message verbatim using EIP-191
personal_sign. - Agent exchanges it once through
POST /auth/wallet-login, GraphQLwalletLogin, or MCP wallet headers. - Server atomically consumes the challenge and creates a prepaid tenant if this is a new wallet.
Message Format
Section titled “Message Format”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.networkVersion: 1Chain ID: 1Nonce: <server nonce>Issued At: 2026-08-20T00:00:00ZExpiration 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.
Signing Examples
Section titled “Signing Examples”TypeScript (viem)
Section titled “TypeScript (viem)”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 });Python (eth-account)
Section titled “Python (eth-account)”from eth_account import Accountfrom eth_account.messages import encode_defunctimport osimport 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()Headers
Section titled “Headers”For MCP and HTTP requests, pass wallet credentials in headers:
| Header | Value |
|---|---|
X-Wallet-Address | 0x-prefixed Ethereum address |
X-Wallet-Signature | EIP-191 personal_sign signature |
X-Wallet-Message | The 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.
Client Configuration
Section titled “Client Configuration”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.
Bearer Token (Alternative)
Section titled “Bearer Token (Alternative)”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.
Auto-Provisioning
Section titled “Auto-Provisioning”When a new wallet authenticates for the first time:
- A new tenant is created with
billing_model = 'prepaid'. - A new user is created (email is
NULL). - Prepaid balance is initialized at €0.
- 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.
Trust Model
Section titled “Trust Model”| Account Type | Billing Model | Trust Level |
|---|---|---|
| Wallet-only | prepaid (mandatory) | Low — must fund balance first |
| Email (verified) | Free postpaid | Metered, zero-priced Free |
| Wallet + verified email | User choice | Prepaid, Free, or paid postpaid |
Security
Section titled “Security”- Store private keys locally. Never send them to any API.
- Only send wallet headers to
*.frameworks.networkdomains. - 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.