Step 10: Provision Project
Phase: implement
Context
You have wallet_address and selected_tier. Time to create the run402 project.
What to do
Create the project
POST https://api.run402.com/projects/v1
Content-Type: application/json
{
"name": "app-name-slug",
"tier": "prototype"
}
First request will return 402 Payment Required:
{
"error": "Payment required",
"accepts": [{
"scheme": "exact",
"price": "$0.10",
"network": "eip155:84532",
"to": "0x..."
}]
}
x402 payment flow:
- Read the 402 response to get the price, network, and recipient address.
- Sign a payment authorization with the wallet (off-chain — no ETH needed for gas).
- Retry the request with the
x-402-paymentheader containing the signed payment. - The facilitator (Coinbase CDP) handles on-chain settlement.
Full 402 response example
The 402 response includes the payment requirements you need to parse:
HTTP/1.1 402 Payment Required
Content-Type: application/json
{
"error": "Payment required",
"accepts": [{
"scheme": "exact",
"network": "eip155:84532",
"maxAmountRequired": "100000",
"resource": "https://api.run402.com/projects/v1",
"description": "Create run402 project (prototype tier)",
"mimeType": "application/json",
"payTo": "0x...",
"maxTimeoutSeconds": 300,
"extra": {
"name": "USDC",
"version": "2",
"chainId": 84532,
"tokenAddress": "0x036CbD53842c5426634e7929541eC2318f3dCF7e"
}
}]
}
Retry with x-402-payment header
After signing the payment, retry the exact same request with the payment header:
POST https://api.run402.com/projects/v1
Content-Type: application/json
x-402-payment: eyJzY2hlbWUiOiJleGFjdCIsIm5ldHdvcmsiOiJlaXAxNTU6ODQ1MzIiLC...
{
"name": "app-name-slug",
"tier": "prototype"
}
The x-402-payment header is a base64-encoded JSON object containing the signed EIP-712 payment authorization. The @x402/fetch library handles this automatically.
x402 payment code (Node.js)
Working snippet using @x402/fetch and @x402/evm:
⚠ WARNING — signer creation gotcha:
Do NOT pass a walletClient to toClientEvmSigner. Pass the viem account object directly (the return value of privateKeyToAccount).
Why: the account object has .address as a top-level property. A walletClient nests it under .account.address, which causes the address to resolve to undefined inside EIP-3009 payload creation — and the resulting signed payment silently fails.
// ✅ CORRECT — pass account directly
const account = privateKeyToAccount(key);
const signer = toClientEvmSigner(account, publicClient);
// ❌ WRONG — walletClient nests .address under .account
const walletClient = createWalletClient({ account, ... });
const signer = toClientEvmSigner(walletClient, publicClient); // BROKEN
import { privateKeyToAccount } from 'viem/accounts';
import { createPublicClient, http } from 'viem';
import { baseSepolia } from 'viem/chains';
import { wrapFetchWithPayment, x402Client } from '@x402/fetch';
import { ExactEvmScheme, toClientEvmSigner } from '@x402/evm';
// 1. Set up wallet from private key
const account = privateKeyToAccount(process.env.WALLET_PRIVATE_KEY);
const publicClient = createPublicClient({ chain: baseSepolia, transport: http() });
// 2. Create signer — use account directly (flat object with .address)
const signer = toClientEvmSigner(account, publicClient);
// 3. Set up x402 client with ExactEvmScheme (use `new`)
const client = new x402Client();
client.register('eip155:84532', new ExactEvmScheme(signer));
// 4. Wrap fetch to auto-handle 402 responses
const fetchPaid = wrapFetchWithPayment(fetch, client);
// 5. Make API call — first attempt returns 402, library retries with payment
const res = await fetchPaid('https://api.run402.com/projects/v1', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'my-app', tier: 'prototype' })
});
const project = await res.json();
Common gotchas
toClientEvmSigner(account, publicClient)— pass the viemaccountobject directly. It has a flat.addressproperty, not nested under.account.new ExactEvmScheme(signer)— must usenew. Calling withoutnewthrows a TypeError.new x402Client()— also needsnew.client.register('eip155:84532', ...)— use the CAIP-2 network ID, not a chain name.
Successful response (201):
{
"project_id": "prj_1772125073085_0001",
"anon_key": "eyJhbGciOiJIUzI1NiI...",
"service_key": "eyJhbGciOiJIUzI1NiI...",
"schema_slot": "p0001",
"tier": "prototype",
"lease_expires_at": "2026-03-11T16:57:53.085Z"
}
CRITICAL: Store project_id, anon_key, and service_key in memory. You will need these for every subsequent API call.
Note: The response also includes schema_slot — you do not need this value. The API handles schema routing automatically via your keys.
API URL
The base API URL for all subsequent calls is: https://api.run402.com
All REST, auth, storage, and admin endpoints use this base URL with the project's keys for routing.
What to tell the user
"Your project is set up! I've created a secure space for your app's data. Now I'll start building."
Expected output
project_id— The run402 project identifieranon_key— JWT for client-side API access (respects RLS)service_key— JWT for admin access (bypasses RLS) — NEVER expose to usersapi_url— Base API URL (https://api.run402.com)lease_expires_at— When the project lease expires