cred.diy/ docs
    Sign inGet an API key
    All documentation

    Wallets & On-Chain - cred.diy API documentation

    Wallets & On-Chain Credits

    A credit type can be deployed as an ERC-20, and issuing credits then mints tokens to the user who earned them. That needs an address, so every end user can carry one. Attach it yourself in a single call, or have the holder prove it by signature — both are enough to receive a mint, and the difference is recorded on the user.

    Attach an Address

    wallet_address is accepted on POST /users, PATCH /users and POST /users/bulk. It is stored lowercase and marked asserted — you vouched for it, nothing was proved. Pass null to detach.

    // Attach an address to a user. One call, no signature — you are
    // vouching for it. Use this when your app already ran its own
    // wallet-connect flow and knows the address is theirs.
    await fetch(
      'https://eutdgemlrpvnxfkkpvlu.supabase.co/functions/v1/credits-api/users',
      {
        method: 'PATCH',
        headers: {
          'Content-Type': 'application/json',
          'x-api-key': 'sk_live_your_api_key'
        },
        body: JSON.stringify({
          user_email: 'user@example.com',
          wallet_address: '0x1234567890abcdef1234567890abcdef12345678'
        })
      }
    );
    
    // The same field works on POST /users and POST /users/bulk, so a wallet
    // can be attached at signup rather than in a second round trip.

    Prove an Address

    The server picks a nonce, the holder signs a message containing it, and the server recovers the signer. Because the server chose the nonce, a signature harvested from another site or an earlier session cannot be replayed here. On success wallet_verified_at is set.

    // Or prove it. Two calls: ask for a message, send back the signature.
    
    // 1. Get a challenge. The message names YOUR project, not Cred — it is
    //    what the user reads in their wallet popup, so show it unmodified.
    const challenge = await fetch(
      'https://eutdgemlrpvnxfkkpvlu.supabase.co/functions/v1/credits-api/wallet/challenge',
      {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'x-api-key': 'sk_live_your_api_key'
        },
        body: JSON.stringify({
          user_email: 'user@example.com',
          address: await signer.getAddress()
        })
      }
    ).then(r => r.json());
    // { nonce: '9f8e…', address: '0x1234…', message: 'Your App wants to…',
    //   expires_at: '2026-08-17T12:10:00.000Z' }
    
    // 2. Sign it and send it back.
    const signature = await signer.signMessage(challenge.message);
    
    const result = await fetch(
      'https://eutdgemlrpvnxfkkpvlu.supabase.co/functions/v1/credits-api/wallet/verify',
      {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'x-api-key': 'sk_live_your_api_key'
        },
        body: JSON.stringify({ nonce: challenge.nonce, signature })
      }
    ).then(r => r.json());
    // { verified: true, user: { wallet_address: '0x1234…',
    //                           wallet_verified_at: '2026-08-17T12:01:04.221Z' } }
    
    // A challenge is single-use and lasts ten minutes. A rejected signature
    // consumes it too — retry means requesting a fresh one.

    Mint on Issue

    When a credit type has minting enabled, POST /issue returns an on_chain block alongside the balance. Every reason a mint does not happen is reported rather than silently skipped.

    // With a wallet on file and an on-chain credit type, issuing credits
    // also mints tokens. The response tells you what happened on-chain.
    const response = await fetch(
      'https://eutdgemlrpvnxfkkpvlu.supabase.co/functions/v1/credits-api/issue',
      {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'x-api-key': 'sk_live_your_api_key'
        },
        body: JSON.stringify({
          user_id: 'user_123',
          user_email: 'user@example.com',
          credit_type_id: 'your_credit_type_id',
          amount: 100,
          reason: 'Welcome bonus',
          // Optional: mint this one issuance elsewhere, ignoring the
          // address on the user.
          wallet_address: '0xabcdef1234567890abcdef1234567890abcdef12'
        })
      }
    );
    
    const data = await response.json();
    // {
    //   balance: 100,
    //   transaction_id: 'txn_abc123',
    //   on_chain: {
    //     status: 'enqueued',
    //     mint_id: 'mint_abc',
    //     transaction_hash: '0x9f8e…'
    //   }
    // }
    
    // The ledger is the system of record and the chain follows it. A mint that
    // cannot be sent NEVER fails the issuance — you get status 'skipped' and a
    // reason instead:
    //   "no wallet address for this end user — set end_users.wallet_address"
    if (data.on_chain?.status === 'skipped') {
      console.warn('Credits issued, no token minted:', data.on_chain.reason);
    }

    Read the Contract

    GET /credit-types publishes the contract address, chain id and decimals, so your app can link to an explorer or convert amounts without hardcoding anything.

    // Read which credit types are on-chain, and where they live.
    const response = await fetch(
      'https://eutdgemlrpvnxfkkpvlu.supabase.co/functions/v1/credits-api/credit-types',
      { headers: { 'x-api-key': 'sk_live_your_api_key' } }
    );
    
    // {
    //   "credit_types": [{
    //     "id": "ct_abc",
    //     "name": "Game Coins",
    //     "symbol": "GC",
    //     "exchange_rate": 100,
    //     "is_on_chain": true,
    //     "blockchain": "base-sepolia",
    //     "contract_address": "0x5FbDB2315678afecb367f032d93F642f64180aa3",
    //     "chain_id": 84532,
    //     "token_decimals": 18,
    //     "mint_on_issue": true,
    //     "deployment_type": "managed"
    //   }]
    // }
    
    // Enough to build an explorer link, convert a credit amount to atomic
    // units, or offer "add this token to your wallet".

    Wallet API Endpoints

    EndpointDescription
    POST /wallet/challengeIssue a single-use message for the holder to sign
    POST /wallet/verifyRecover the signer and record the proved address
    GET /walletRead the address on file and whether it was proved
    DELETE /walletDetach the address — balances are untouched

    Deposits — tokens coming back

    A credit type can collect deposits at an address, and tokens sent there credit the depositor's ledger balance. This is the only path that runs chain → ledger, so the guards are strict: only the configured token into the configured address, only after the confirmations you choose, never a mint from the zero address, and idempotent on (chain_id, transaction_hash, log_index).

    The paying address must already be attached to a user. Tokens from an unrecognised address are recorded as unattributed and held rather than credited — read them with GET /deposits and resolve them by attaching the address.

    Deposits and mint-on-issue are mutually exclusive, enforced in the database. Minting mirrors an issuance rather than moving it, so a holder issued 100 credits also holds 100 tokens — depositing those back would credit them twice, and again, and again.

    What the API will not do

    Deploying a token, enabling minting on a credit type, and setting the address that x402 payments settle to, and configuring deposits all stay in the dashboard, behind your operator login. Each of them either spends gas or decides where money lands, and an API key is a credential your app servers hold — not one that should be able to move either.

    Deploy the token once from Dashboard → Credit Types, then everything above works from your app with an API key.

    Your first credit is one request away.

    Create a project, copy an API key, and issue a credit from your server.