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:

  1. Read the 402 response to get the price, network, and recipient address.
  2. Sign a payment authorization with the wallet (off-chain — no ETH needed for gas).
  3. Retry the request with the x-402-payment header containing the signed payment.
  4. 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 viem account object directly. It has a flat .address property, not nested under .account.
  • new ExactEvmScheme(signer) — must use new. Calling without new throws a TypeError.
  • new x402Client() — also needs new.
  • 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 identifier
  • anon_key — JWT for client-side API access (respects RLS)
  • service_key — JWT for admin access (bypasses RLS) — NEVER expose to users
  • api_url — Base API URL (https://api.run402.com)
  • lease_expires_at — When the project lease expires

Memory directive