AI Wealth Advisor with Blockchain Audit

Connect a Python AI advisor to a Node.js blockchain microservice on Sepolia.

Introduction

30 Second Summary

When a financial advisor recommends you move your retirement savings into a new fund, you trust that recommendation won't be quietly changed after the fact. In regulated industries, every piece of advice needs a permanent, tamper-proof record.

In this project, you will build a microservice architecture that automatically records every AI-generated financial recommendation onto a blockchain ledger. Your Python AI wealth advisor will communicate with a dedicated Node.js blockchain service, creating an immutable audit trail that proves no advice was altered after delivery.

What You'll Build

A Node.js Express microservice that bridges your existing Python AI wealth advisor with your deployed Ethereum smart contract, recording and verifying advisory interactions via REST endpoints.

By the end of this project, you'll have:

  • A Node.js Express microservice running on port 3001 that handles all blockchain operations including hashing, recording, and verifying advisory records on Sepolia.
  • An integrated advisory flow where your Python FastAPI backend automatically calls the blockchain service after every Gemini response, anchoring each recommendation on-chain.
  • A frontend verification badge that displays "Verified on-chain" with clickable Etherscan links after each advisory response.
  • Secret Mission: A verification panel that lets users independently check any past advisory's integrity by recomputing its hash and comparing against the on-chain record.

Are there any prerequisites?

This project builds on two previous projects: a working FastAPI wealth advisor backend using Gemini 2.5 Flash, and a deployed AdvisoryAuditTrail contract on Sepolia with your contract address saved. You also need Node.js v22+, MetaMask with Sepolia ETH, and an Alchemy account already configured.

Set Up the Node.js Blockchain Microservice

Your AI wealth advisor generates recommendations on port 8000. Your smart contract stores tamper-proof hashes on Sepolia. Right now, these two systems don't talk to each other.

In this step, you'll create the bridge: a dedicated Node.js Express microservice that handles all blockchain operations. Running on port 3001, this service uses ethers.js to interact with your deployed contract. Separating blockchain logic into its own service is standard practice in fintech architecture.

In this step, get ready to:
  • Create a blockchain-service project with Express 5, ethers.js, dotenv, and cors installed.
  • Configure environment variables and load the contract ABI from your Hardhat artifacts.
  • Start an Express server on port 3001 with a working health check endpoint.
Create the project directory and install dependencies

Your blockchain microservice lives in its own folder, completely separate from your Python wealth-advisor project. This separation is intentional.

  • Open the integrated terminal in VS Code by pressing Ctrl+` (Windows) or Cmd+` (macOS).
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create the project directory and move into it by running these commands:
mkdir blockchain-service
cd blockchain-service
  • Initialize the project and install dependencies by running:
npm init -y
npm install express ethers dotenv cors

You should see output confirming packages were added. A node_modules folder and package-lock.json file now exist alongside package.json.

Seeing 'npm: command not found'?

This means Node.js is not installed or not in your PATH. Confirm Node.js v22+ is installed by running node --version. If it is not found, revisit the Node.js installation from the prerequisite project.

Help me fix npm not being found

  • Create the project folder structure by running:
mkdir -p src/routes abi
  • Confirm the folders exist by running:
ls

You should see abi, node_modules, package-lock.json, package.json, and src in the output.

Now open this new project folder in VS Code so you can edit files.

  • In VS Code, click File in the top menu bar.
  • Click Open Folder.
  • Select the blockchain-service folder on your Desktop, then click Open.

The npm init -y command created a default package.json, but you need to configure it for ES modules and add custom scripts.

  • In the Explorer sidebar, click package.json to open it.
  • Replace the entire contents of the file with this code:
{
  "name": "blockchain-audit-service",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "start": "node src/index.js",
    "dev": "node --watch src/index.js"
  },
  "dependencies": {
    "cors": "^2.8.5",
    "dotenv": "^16.4.7",
    "ethers": "^6.13.4",
    "express": "^5.1.0"
  }
}

What does this configuration do?

  • "type": "module" tells Node.js to treat .js files as ES modules, so you can use import/export syntax instead of require().
  • The dev script uses node --watch to auto-restart the server whenever you save a file change.
  • The start script runs the production entry point at src/index.js.
  • Save package.json.
  • Re-install dependencies to sync the lock file with your updated package.json by opening the terminal (Ctrl+` or Cmd+`) and running:
npm install

Seeing a JSON parse error?

Double-check that you have no trailing commas after the last item in the dependencies object. JSON does not allow trailing commas. Also confirm all curly braces are matched.

Help me debug my package.json syntax

✔️ Awesome, I've got everything!

Great. Make sure package.json is saved before moving on.

ⓧ I'd like to double check the full code

{
  "name": "blockchain-audit-service",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "start": "node src/index.js",
    "dev": "node --watch src/index.js"
  },
  "dependencies": {
    "cors": "^2.8.5",
    "dotenv": "^16.4.7",
    "ethers": "^6.13.4",
    "express": "^5.1.0"
  }
}
Configure environment variables and the contract ABI

Your microservice needs three secrets to interact with the blockchain: your Alchemy RPC endpoint, your deployed contract address, and a wallet private key for signing transactions.

  • In the Explorer sidebar, right-click the empty area and select New File.
  • Name the file .env and press Enter.
  • Paste the following configuration, replacing each placeholder with your actual values:
SEPOLIA_RPC_URL=your-alchemy-sepolia-rpc-url-here
CONTRACT_ADDRESS=your-deployed-contract-address-here
PRIVATE_KEY=your-sepolia-wallet-private-key-here
PORT=3001

Where do I find these values?

  • SEPOLIA_RPC_URL: Your Alchemy dashboard shows this under your Sepolia app's API key section. It looks like https://eth-sepolia.g.alchemy.com/v2/....
  • CONTRACT_ADDRESS: The address you saved when you deployed the AdvisoryAuditTrail contract in the previous project.
  • PRIVATE_KEY: Export this from MetaMask (Account Details > Show Private Key). This is safe on Sepolia testnet only.

Never use a mainnet private key here

This project uses Sepolia test ETH. Your private key in .env should belong to a wallet that only holds test tokens. Never commit .env to version control or share it.

  • Save .env.

Next, your microservice needs the contract's ABI (Application Binary Interface) to know which functions it can call on-chain.

  • Locate the ABI artifact in your Hardhat project. The file is at artifacts/contracts/AdvisoryAuditTrail.sol/AdvisoryAuditTrail.json inside your advisory-audit-trail folder on the Desktop.
  • Copy this file into your new project's abi/ folder. In the terminal, run:
cp ~/Desktop/advisory-audit-trail/artifacts/contracts/AdvisoryAuditTrail.sol/AdvisoryAuditTrail.json abi/
  • Confirm the file was copied by running:
ls abi/

You should see AdvisoryAuditTrail.json in the output.

File not found when copying?

If your Hardhat project folder is not named advisory-audit-trail or is not on the Desktop, adjust the path to match your actual folder location. You can also manually copy the file using Finder (macOS) or File Explorer (Windows) into the abi folder.

Help me find my contract ABI file

Now create the configuration module that loads these environment variables and the ABI into a single config object.

  • In the Explorer sidebar, right-click the src folder and select New File.
  • Name the file config.js and press Enter.
  • Paste the following code into src/config.js:
import { readFileSync } from "fs";
import { fileURLToPath } from "url";
import { dirname, join } from "path";
import dotenv from "dotenv";

dotenv.config();

const __dirname = dirname(fileURLToPath(import.meta.url));
const abiPath = join(__dirname, "..", "abi", "AdvisoryAuditTrail.json");
const artifact = JSON.parse(readFileSync(abiPath, "utf-8"));

export const config = {
  rpcUrl: process.env.SEPOLIA_RPC_URL,
  contractAddress: process.env.CONTRACT_ADDRESS,
  privateKey: process.env.PRIVATE_KEY,
  port: process.env.PORT || 3001,
  contractAbi: artifact.abi || artifact,
};

What does this code do?

  • dotenv.config() loads the .env file values into process.env.
  • The __dirname workaround is needed because ES modules don't have __dirname built in like CommonJS does.
  • artifact.abi || artifact handles both full Hardhat artifacts (which have an abi key) and standalone ABI arrays.
  • All configuration is exported as a single config object so other modules import from one place.
  • Save src/config.js.

Module not found errors later?

If you see an error about dotenv not being found when you start the server, confirm your terminal is inside the blockchain-service directory (where node_modules lives). Also verify that package.json contains "type": "module".

Help me fix module not found errors

✔️ Awesome, I've got everything!

Great. Double check you've saved src/config.js before continuing.

ⓧ I'd like to double check the full code

import { readFileSync } from "fs";
import { fileURLToPath } from "url";
import { dirname, join } from "path";
import dotenv from "dotenv";

dotenv.config();

const __dirname = dirname(fileURLToPath(import.meta.url));
const abiPath = join(__dirname, "..", "abi", "AdvisoryAuditTrail.json");
const artifact = JSON.parse(readFileSync(abiPath, "utf-8"));

export const config = {
  rpcUrl: process.env.SEPOLIA_RPC_URL,
  contractAddress: process.env.CONTRACT_ADDRESS,
  privateKey: process.env.PRIVATE_KEY,
  port: process.env.PORT || 3001,
  contractAbi: artifact.abi || artifact,
};
Create the Express server and test the health endpoint

With your configuration in place, it's time to create the actual Express server. This is the entry point that starts listening on port 3001 and responds to incoming requests.

  • In the Explorer sidebar, right-click the src folder and select New File.
  • Name the file index.js and press Enter.
  • Paste the following code into src/index.js:
import express from "express";
import cors from "cors";
import { config } from "./config.js";

const app = express();

app.use(cors({ origin: "*" }));
app.use(express.json());

// Health check
app.get("/health", async (req, res) => {
  res.json({
    status: "healthy",
    service: "blockchain-audit-service",
  });
});

app.listen(config.port, () => {
  console.log(`Blockchain audit service running on port ${config.port}`);
});

What does this code do?

  • cors({ origin: "*" }) allows any origin to call this service during development. Your Python frontend on port 8000 can make requests without being blocked.
  • express.json() parses incoming JSON request bodies automatically.
  • The /health endpoint lets you confirm the service is running. Production services always expose a health check for monitoring.
  • app.listen starts the server on the port defined in your .env file (3001).
  • Save src/index.js.
  • Start the development server by running this command in the terminal:
npm run dev

You should see Blockchain audit service running on port 3001 printed in the terminal.

  • Open your browser and navigate to http://localhost:3001/health.

You should see a JSON response with "status":"healthy" and "service":"blockchain-audit-service".

Server not starting or port already in use?

  • If you see an error about port 3001 being in use, another process is occupying that port. Stop it with lsof -i :3001 (macOS) or netstat -ano | findstr :3001 (Windows) to find the process, then kill it.
  • If you see a syntax error mentioning import, confirm that package.json includes "type": "module". Without it, Node.js treats files as CommonJS and does not understand import statements.
  • If you see an error about the ABI file not being found, confirm abi/AdvisoryAuditTrail.json exists in your project root.

Help me troubleshoot my Express server

✔️ Awesome, I've got everything!

Great. Keep the server running in this terminal for the rest of the project.

ⓧ I'd like to double check the full code

import express from "express";
import cors from "cors";
import { config } from "./config.js";

const app = express();

app.use(cors({ origin: "*" }));
app.use(express.json());

// Health check
app.get("/health", async (req, res) => {
  res.json({
    status: "healthy",
    service: "blockchain-audit-service",
  });
});

app.listen(config.port, () => {
  console.log(`Blockchain audit service running on port ${config.port}`);
});

Your blockchain microservice is up and responding. Next up, you'll build the core endpoints that compute hashes and record advisory interactions on-chain.

Build the Hash and Record Endpoints

Your microservice has a running Express server, but it can't do anything with the blockchain yet. The core challenge is computing a keccak256 hash in JavaScript that exactly matches what your Solidity contract computes on-chain.

In this step, you'll build the service layer that replicates your contract's hashing logic using ethers.js, wraps the on-chain transaction calls, and exposes REST endpoints for recording and verifying advisory records.

In this step, get ready to:
  • Create a hash computation service that mirrors the Solidity contract's generateRecordHash logic.
  • Create a contract interaction service that submits transactions and verifies records on Sepolia.
  • Build REST endpoints that accept advisory data, compute hashes, and record them on-chain.
Create the hash computation service

Your deployed AdvisoryAuditTrail contract has a generateRecordHash function that produces a unique fingerprint for each advisory record. For your microservice to submit matching hashes, it needs to replicate that exact logic off-chain using ethers.js.

  • Create the src/services/ directory by running this command in your terminal (inside blockchain-service/):
mkdir src/services
  • Confirm the directory exists by running:
ls src/services

You should see no error. The folder is empty for now.

  • Create a new file at src/services/hashService.js and paste the following code:
import { keccak256, AbiCoder, toUtf8Bytes } from "ethers";

const abiCoder = AbiCoder.defaultAbiCoder();

export function computeRecordHash(clientId, recommendation, amount, advisoryTimestamp) {
  // Pre-hash the recommendation string (matches keccak256(bytes(recommendation)) in Solidity)
  const recommendationHash = keccak256(toUtf8Bytes(recommendation));

  // ABI encode with the same types as Solidity's abi.encode
  const encoded = abiCoder.encode(
    ["bytes32", "bytes32", "uint256", "uint256"],
    [clientId, recommendationHash, amount, advisoryTimestamp]
  );

  // Final keccak256 hash
  return keccak256(encoded);
}

What does this code do?

This function mirrors the exact steps your Solidity contract performs inside generateRecordHash:

  • keccak256(toUtf8Bytes(recommendation)) hashes the recommendation string. This matches what Solidity does with keccak256(bytes(recommendation)).
  • abiCoder.encode packs the four values into a byte sequence using the same type layout as Solidity's abi.encode.
  • The final keccak256 call produces the same 32-byte hash the contract would return on-chain.

Why does hash equivalence matter?

If the off-chain hash does not match the on-chain hash byte-for-byte, the contract's verifyRecord function will never find the record. ABI encoding ensures both sides pack the data identically before hashing.

  • Save src/services/hashService.js.

You won't see a runtime change yet. The next file will use this function to submit hashes to the blockchain.

✔️ Awesome, I've got everything!

Great. Make sure you've saved the file before moving on.

ⓧ I'd like to double check the full code

import { keccak256, AbiCoder, toUtf8Bytes } from "ethers";

const abiCoder = AbiCoder.defaultAbiCoder();

export function computeRecordHash(clientId, recommendation, amount, advisoryTimestamp) {
  // Pre-hash the recommendation string (matches keccak256(bytes(recommendation)) in Solidity)
  const recommendationHash = keccak256(toUtf8Bytes(recommendation));

  // ABI encode with the same types as Solidity's abi.encode
  const encoded = abiCoder.encode(
    ["bytes32", "bytes32", "uint256", "uint256"],
    [clientId, recommendationHash, amount, advisoryTimestamp]
  );

  // Final keccak256 hash
  return keccak256(encoded);
}
Create the contract interaction service

With the hashing logic ready, you need a module that connects to Sepolia, signs transactions with your wallet, and calls the contract's functions.

  • Create a new file at src/services/contractService.js and paste the following code:
import { JsonRpcProvider, Wallet, Contract } from "ethers";
import { config } from "../config.js";

const provider = new JsonRpcProvider(config.rpcUrl);
const wallet = new Wallet(config.privateKey, provider);
const contract = new Contract(config.contractAddress, config.contractAbi, wallet);

export async function recordAdvisory(recordHash) {
  const tx = await contract.recordAdvisory(recordHash);
  const receipt = await tx.wait();

  return {
    tx_hash: receipt.hash,
    block_number: receipt.blockNumber,
    etherscan_url: `https://sepolia.etherscan.io/tx/${receipt.hash}`,
  };
}

export async function verifyRecord(recordHash) {
  const result = await contract.verifyRecord(recordHash);
  const [timestamp, advisor, exists] = result;

  return {
    exists: exists,
    timestamp: Number(timestamp),
    advisor: advisor,
  };
}

export async function getBlockNumber() {
  return await provider.getBlockNumber();
}

What does this code do?

  • JsonRpcProvider connects to Sepolia via your Alchemy RPC URL. Wallet signs transactions with your private key. Contract wraps your deployed contract's address and ABI so you can call its functions like normal JavaScript methods.
  • recordAdvisory(recordHash) submits a transaction to the contract's recordAdvisory function, waits for the block to be mined, then returns the transaction hash and an Etherscan link.
  • verifyRecord(recordHash) calls the contract's read-only verifyRecord function. This costs no gas because it only reads state.
  • getBlockNumber() fetches the latest Sepolia block number. You'll use this to make the health check endpoint confirm that the blockchain connection is live.
  • Save src/services/contractService.js.

You won't see a change yet. The next chunk wires these services into HTTP endpoints you can call.

✔️ Awesome, I've got everything!

Great. Make sure the file is saved before continuing.

ⓧ I'd like to double check the full code

import { JsonRpcProvider, Wallet, Contract } from "ethers";
import { config } from "../config.js";

const provider = new JsonRpcProvider(config.rpcUrl);
const wallet = new Wallet(config.privateKey, provider);
const contract = new Contract(config.contractAddress, config.contractAbi, wallet);

export async function recordAdvisory(recordHash) {
  const tx = await contract.recordAdvisory(recordHash);
  const receipt = await tx.wait();

  return {
    tx_hash: receipt.hash,
    block_number: receipt.blockNumber,
    etherscan_url: `https://sepolia.etherscan.io/tx/${receipt.hash}`,
  };
}

export async function verifyRecord(recordHash) {
  const result = await contract.verifyRecord(recordHash);
  const [timestamp, advisor, exists] = result;

  return {
    exists: exists,
    timestamp: Number(timestamp),
    advisor: advisor,
  };
}

export async function getBlockNumber() {
  return await provider.getBlockNumber();
}
Create the audit routes and update the entry point

Now you'll expose the hash and contract services as HTTP endpoints. The Python backend will call these endpoints to record advisory interactions on-chain.

  • Create a new file at src/routes/audit.js and paste the following code:
import { Router } from "express";
import { keccak256, toUtf8Bytes } from "ethers";
import { computeRecordHash } from "../services/hashService.js";
import { recordAdvisory, verifyRecord } from "../services/contractService.js";

const router = Router();

router.post("/record", async (req, res) => {
  try {
    const { customer_id, recommendation, amount, advisory_timestamp } = req.body;

    // Convert customer_id to bytes32 via keccak256
    const clientId = keccak256(toUtf8Bytes(customer_id));

    // Compute the record hash
    const recordHash = computeRecordHash(
      clientId,
      recommendation,
      BigInt(amount),
      BigInt(advisory_timestamp)
    );

    // Record on-chain
    const result = await recordAdvisory(recordHash);

    res.json({
      record_hash: recordHash,
      advisory_timestamp: advisory_timestamp,
      ...result,
    });
  } catch (error) {
    console.error("Recording failed:", error.message);
    res.status(500).json({ error: error.message });
  }
});

router.post("/verify", async (req, res) => {
  try {
    const { customer_id, recommendation, amount, advisory_timestamp } = req.body;

    // Compute the same hash
    const clientId = keccak256(toUtf8Bytes(customer_id));
    const recordHash = computeRecordHash(
      clientId,
      recommendation,
      BigInt(amount),
      BigInt(advisory_timestamp)
    );

    // Check on-chain
    const result = await verifyRecord(recordHash);

    res.json({
      record_hash: recordHash,
      exists: result.exists,
      on_chain_timestamp: result.timestamp,
      advisor_address: result.advisor,
      integrity: result.exists ? "VERIFIED" : "NOT_FOUND",
    });
  } catch (error) {
    console.error("Verification failed:", error.message);
    res.status(500).json({ error: error.message });
  }
});

export default router;

What do these endpoints do?

  • POST /record takes advisory data (customer ID, recommendation text, amount, timestamp), computes the hash, and submits it to the blockchain. It returns the transaction hash and Etherscan URL once the block is mined.
  • POST /verify takes the same advisory data, recomputes the hash, and checks if it exists on-chain. This is a read-only call that costs no gas.
  • Both endpoints convert customer_id to a bytes32 using keccak256(toUtf8Bytes(...)). This matches how your Hardhat tests formatted client IDs before calling the contract.
  • Save src/routes/audit.js.

✔️ Awesome, I've got everything!

Great. Now you need to mount this router in your entry point.

ⓧ I'd like to double check the full code

import { Router } from "express";
import { keccak256, toUtf8Bytes } from "ethers";
import { computeRecordHash } from "../services/hashService.js";
import { recordAdvisory, verifyRecord } from "../services/contractService.js";

const router = Router();

router.post("/record", async (req, res) => {
  try {
    const { customer_id, recommendation, amount, advisory_timestamp } = req.body;

    // Convert customer_id to bytes32 via keccak256
    const clientId = keccak256(toUtf8Bytes(customer_id));

    // Compute the record hash
    const recordHash = computeRecordHash(
      clientId,
      recommendation,
      BigInt(amount),
      BigInt(advisory_timestamp)
    );

    // Record on-chain
    const result = await recordAdvisory(recordHash);

    res.json({
      record_hash: recordHash,
      advisory_timestamp: advisory_timestamp,
      ...result,
    });
  } catch (error) {
    console.error("Recording failed:", error.message);
    res.status(500).json({ error: error.message });
  }
});

router.post("/verify", async (req, res) => {
  try {
    const { customer_id, recommendation, amount, advisory_timestamp } = req.body;

    // Compute the same hash
    const clientId = keccak256(toUtf8Bytes(customer_id));
    const recordHash = computeRecordHash(
      clientId,
      recommendation,
      BigInt(amount),
      BigInt(advisory_timestamp)
    );

    // Check on-chain
    const result = await verifyRecord(recordHash);

    res.json({
      record_hash: recordHash,
      exists: result.exists,
      on_chain_timestamp: result.timestamp,
      advisor_address: result.advisor,
      integrity: result.exists ? "VERIFIED" : "NOT_FOUND",
    });
  } catch (error) {
    console.error("Verification failed:", error.message);
    res.status(500).json({ error: error.message });
  }
});

export default router;

The audit routes are ready, but Express doesn't know about them yet. You need to import and mount the router in your entry point, and also upgrade the health check to confirm the Sepolia connection is live.

  • Open src/index.js and replace the entire contents with the updated version below:
import express from "express";
import cors from "cors";
import { config } from "./config.js";
import auditRouter from "./routes/audit.js";
import { getBlockNumber } from "./services/contractService.js";

const app = express();

app.use(cors({ origin: "*" }));
app.use(express.json());

// Health check
app.get("/health", async (req, res) => {
  try {
    const blockNumber = await getBlockNumber();
    res.json({
      status: "healthy",
      service: "blockchain-audit-service",
      sepolia_block: blockNumber,
    });
  } catch (error) {
    res.status(503).json({ status: "unhealthy", error: error.message });
  }
});

// Mount audit routes
app.use("/", auditRouter);

app.listen(config.port, () => {
  console.log(`Blockchain audit service running on port ${config.port}`);
});

What changed in the entry point?

  • Two new imports: auditRouter (your new routes) and getBlockNumber (from your contract service).
  • The health check now calls getBlockNumber() to verify the Alchemy RPC connection is working. If it fails, the endpoint returns a 503 status.
  • app.use("/", auditRouter) mounts your record and verify routes at the root path, making them available as POST /record and POST /verify.
  • Save src/index.js.
  • In the terminal where your server is running, press Ctrl+C to stop it, then restart it by running:
npm start

You should see Blockchain audit service running on port 3001 in your terminal with no errors.

  • Verify the upgraded health check by visiting http://localhost:3001/health in your browser.

You should now see a sepolia_block number in the JSON response, confirming your Alchemy connection is live.

  • Test the record endpoint by opening a new terminal window and running this curl command:
curl -X POST http://localhost:3001/record \
  -H "Content-Type: application/json" \
  -d '{"customer_id": "C001", "recommendation": "Test advisory", "amount": 50000, "advisory_timestamp": 1719500000}'

What should I see?

The request will take 10-20 seconds while the transaction is mined on Sepolia. Once confirmed, you'll get a JSON response containing record_hash, tx_hash, block_number, and an etherscan_url you can open in your browser to see the confirmed transaction.

Seeing an error instead of a transaction?

  • If you see a connection error, check that your SEPOLIA_RPC_URL in .env is correct and starts with https://.
  • If you see a "nonce" or "insufficient funds" error, make sure your MetaMask wallet has Sepolia test ETH and that the PRIVATE_KEY in .env matches the wallet that deployed your contract.
  • If you see a "CONTRACT_ADDRESS" or "execution reverted" error, verify that the CONTRACT_ADDRESS in .env matches your deployed AdvisoryAuditTrail contract address from your earlier project.

Why is my blockchain microservice returning an error instead of a transaction?

✔️ Awesome, I've got everything!

Great. Double check you've saved all four files.

ⓧ I'd like to double check the full code

import express from "express";
import cors from "cors";
import { config } from "./config.js";
import auditRouter from "./routes/audit.js";
import { getBlockNumber } from "./services/contractService.js";

const app = express();

app.use(cors({ origin: "*" }));
app.use(express.json());

// Health check
app.get("/health", async (req, res) => {
  try {
    const blockNumber = await getBlockNumber();
    res.json({
      status: "healthy",
      service: "blockchain-audit-service",
      sepolia_block: blockNumber,
    });
  } catch (error) {
    res.status(503).json({ status: "unhealthy", error: error.message });
  }
});

// Mount audit routes
app.use("/", auditRouter);

app.listen(config.port, () => {
  console.log(`Blockchain audit service running on port ${config.port}`);
});

Your blockchain microservice is fully operational. It can compute hashes, record them on Sepolia, and verify records. Next up, you'll wire your Python FastAPI backend to call this service automatically after every advisory response.

Connect the Python Backend to the Blockchain Microservice

Your blockchain microservice is running and proven. Every advisory record you POST to port 3001 lands on Sepolia with a confirmed transaction hash.

Now your Python backend needs to talk to it. In this step, you will wire the FastAPI advisory flow so that after Gemini generates a response, the backend automatically calls the blockchain microservice to create a tamper-proof on-chain record.

In this step, get ready to:
  • Install the requests library for inter-service HTTP calls.
  • Add a blockchain recording function to advisory_engine.py.
  • Update the /advice endpoint to return blockchain verification data.
Add the blockchain recording function to advisory_engine.py

The Python backend needs the requests library to make HTTP calls to your Node.js microservice. This is a standard approach for microservice architecture where each service communicates over HTTP.

  • Install the requests library in your wealth-advisor project by running:
pip install requests==2.32.3
  • Open requirements.txt in your wealth-advisor project folder.
  • Add requests==2.32.3 as a new line at the end of the file so it looks like this:
fastapi[standard]==0.138.1
google-genai==2.10.0
uvicorn==0.34.0
python-dotenv==1.1.0
requests==2.32.3

Why pin the version?

Pinning requests==2.32.3 ensures every developer and deployment uses the exact same version. This prevents subtle bugs from version drift between environments.

  • Save requirements.txt.

Now you will add the blockchain recording function. This function POSTs advisory data to the Node.js microservice and handles failures gracefully so a blockchain outage never blocks the advisory response.

  • Open advisory_engine.py in your wealth-advisor project.
  • Find the import section at the top of the file. Replace it with the following imports to add requests and time:
import os
import time

import requests
from google import genai
from google.genai import types
from dotenv import load_dotenv

from models import CustomerProfile
from data_service import calculate_portfolio_metrics

What are the new imports?

The time module provides time.time() for generating Unix timestamps. The requests library handles HTTP calls from Python to your Node.js microservice.

  • Find the line client = genai.Client(api_key=os.getenv("GEMINI_API_KEY")) and add the following constant directly below it:
BLOCKCHAIN_SERVICE_URL = os.getenv("BLOCKCHAIN_SERVICE_URL", "http://localhost:3001")

Why use an environment variable with a default?

Loading from an env var means you can point to a different URL in production without changing code. The default http://localhost:3001 works for local development where both services run on the same machine.

Now add the recording function. This is the bridge between your Python backend and the blockchain microservice.

  • Scroll down past the format_customer_context function. Add the following record_on_blockchain function below it:
def record_on_blockchain(customer_id: str, recommendation: str, amount: int) -> dict | None:
    """Call the blockchain microservice to record an advisory interaction."""
    try:
        response = requests.post(
            f"{BLOCKCHAIN_SERVICE_URL}/record",
            json={
                "customer_id": customer_id,
                "recommendation": recommendation,
                "amount": amount,
                "advisory_timestamp": int(time.time()),
            },
            timeout=30,
        )
        if response.status_code == 200:
            return response.json()
        else:
            print(f"Blockchain service error: {response.status_code} - {response.text}")
            return None
    except requests.exceptions.RequestException as e:
        print(f"Blockchain service unreachable: {e}")
        return None

What does this function do?

It POSTs the advisory data (customer ID, recommendation text, portfolio value, and a Unix timestamp) to your Node.js microservice's /record endpoint.

The timeout=30 gives Sepolia up to 30 seconds to confirm the transaction. If the blockchain service is down or returns an error, the function returns None instead of crashing. This ensures advisory responses always reach the customer even if the audit trail is temporarily unavailable.

  • Save advisory_engine.py.

Seeing an import error for requests?

Make sure you ran pip install requests==2.32.3 in the same terminal environment where your FastAPI server runs. If you are using a virtual environment, activate it first.

Help me debug this import error.

Update generate_advice to call the blockchain service

The existing generate_advice function returns a plain text string. You need it to also call the blockchain recording and return a richer dictionary containing both the advice and the blockchain result.

  • In advisory_engine.py, find your existing generate_advice function and replace it entirely with this updated version:
def generate_advice(customer: CustomerProfile, question: str) -> dict:
    system_instruction = format_customer_context(customer)

    response = client.models.generate_content(
        model="gemini-2.5-flash",
        contents=question,
        config=types.GenerateContentConfig(
            system_instruction=system_instruction,
            max_output_tokens=1024,
        ),
    )

    advice_text = response.text

    # Record the advisory interaction on-chain via microservice
    metrics = calculate_portfolio_metrics(customer)
    blockchain_result = record_on_blockchain(
        customer_id=customer.id,
        recommendation=advice_text,
        amount=int(metrics["total_portfolio_value"]),
    )

    return {
        "advice": advice_text,
        "blockchain": blockchain_result,
    }

What changed in generate_advice?

After Gemini generates the advice text, the function now calls record_on_blockchain with the customer ID, the full recommendation text, and the portfolio value. It returns a dictionary with two keys: "advice" (the text for the customer) and "blockchain" (the transaction result or None if recording failed).

  • Save advisory_engine.py.

✔️ Awesome, I've got everything!

Great. Double check you have saved advisory_engine.py with all the changes.

ⓧ I'd like to double check the full code

import os
import time

import requests
from google import genai
from google.genai import types
from dotenv import load_dotenv

from models import CustomerProfile
from data_service import calculate_portfolio_metrics

load_dotenv()

client = genai.Client(api_key=os.getenv("GEMINI_API_KEY"))

BLOCKCHAIN_SERVICE_URL = os.getenv("BLOCKCHAIN_SERVICE_URL", "http://localhost:3001")

SYSTEM_PROMPT = """You are WealthAI, a professional digital wealth advisor integrated into a bank's mobile application. Your role is to provide personalized, data-driven financial guidance.

Guidelines:
- Always reference specific numbers from the customer's financial profile when making recommendations
- Provide actionable advice, not generic platitudes
- Use a warm but professional tone appropriate for a banking application
- Include specific percentages, dollar amounts, and timeframes in your suggestions
- Acknowledge the customer's risk tolerance level when suggesting investments
- Never guarantee returns or make promises about market performance
- Add a brief disclaimer at the end of investment-specific advice
- Keep responses concise (3-5 paragraphs maximum) for mobile readability

Customer Financial Profile:
Name: {name}
Age: {age}
Annual Income: ${income:,.0f}
Risk Tolerance: {risk_tolerance}/10
Financial Goals: {goals}

Monthly Spending Breakdown:
{spending_breakdown}

Investment Portfolio (Total: ${total_value:,.0f}):
{portfolio_breakdown}

Portfolio Metrics:
- Savings Rate: {savings_rate}% of income
- Number of Asset Classes: {num_assets}
- Highest Single Allocation: {max_allocation}%
"""


def format_customer_context(customer: CustomerProfile) -> str:
    metrics = calculate_portfolio_metrics(customer)
    spending_lines = "\n".join(
        f"- {s.category.replace('_', ' ').title()}: ${s.monthly_amount:,.0f}/month"
        for s in customer.spending
    )
    portfolio_lines = "\n".join(
        f"- {a.asset_class.replace('_', ' ').title()}: {a.allocation_percent}% (${a.value:,.0f})"
        for a in customer.portfolio
    )
    return SYSTEM_PROMPT.format(
        name=customer.name,
        age=customer.age,
        income=customer.income,
        risk_tolerance=customer.risk_tolerance,
        goals=", ".join(g.replace("_", " ").title() for g in customer.financial_goals),
        spending_breakdown=spending_lines,
        total_value=metrics["total_portfolio_value"],
        portfolio_breakdown=portfolio_lines,
        savings_rate=metrics["savings_rate"],
        num_assets=metrics["num_asset_classes"],
        max_allocation=metrics["max_single_allocation_percent"],
    )


def record_on_blockchain(customer_id: str, recommendation: str, amount: int) -> dict | None:
    """Call the blockchain microservice to record an advisory interaction."""
    try:
        response = requests.post(
            f"{BLOCKCHAIN_SERVICE_URL}/record",
            json={
                "customer_id": customer_id,
                "recommendation": recommendation,
                "amount": amount,
                "advisory_timestamp": int(time.time()),
            },
            timeout=30,
        )
        if response.status_code == 200:
            return response.json()
        else:
            print(f"Blockchain service error: {response.status_code} - {response.text}")
            return None
    except requests.exceptions.RequestException as e:
        print(f"Blockchain service unreachable: {e}")
        return None


def generate_advice(customer: CustomerProfile, question: str) -> dict:
    system_instruction = format_customer_context(customer)

    response = client.models.generate_content(
        model="gemini-2.5-flash",
        contents=question,
        config=types.GenerateContentConfig(
            system_instruction=system_instruction,
            max_output_tokens=1024,
        ),
    )

    advice_text = response.text

    # Record the advisory interaction on-chain via microservice
    metrics = calculate_portfolio_metrics(customer)
    blockchain_result = record_on_blockchain(
        customer_id=customer.id,
        recommendation=advice_text,
        amount=int(metrics["total_portfolio_value"]),
    )

    return {
        "advice": advice_text,
        "blockchain": blockchain_result,
    }
Update main.py to expose blockchain data in the response

The /advice endpoint currently expects generate_advice to return a plain string. Now that it returns a dictionary, you need to update the endpoint to extract the blockchain fields and include them in the JSON response.

  • Open main.py in your wealth-advisor project.
  • Find the imports at the top. Update the advisory_engine import line and add a requests import so the top of your file looks like this:
import requests
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse

from models import AdvisoryRequest
from data_service import get_customer_by_id, load_customers, calculate_portfolio_metrics
from advisory_engine import generate_advice, BLOCKCHAIN_SERVICE_URL
from risk_scorer import calculate_risk_score

What changed in the imports?

You added import requests at the top (needed for the verify endpoint you will add next). You also import BLOCKCHAIN_SERVICE_URL from advisory_engine so main.py can forward requests to the same blockchain service URL.

  • Find the existing get_advice function (the @app.post("/advice") endpoint). Replace it with this updated version that handles the dictionary response:
@app.post("/advice")
def get_advice(request: AdvisoryRequest):
    customer = get_customer_by_id(request.customer_id)
    if not customer:
        raise HTTPException(status_code=404, detail="Customer not found")
    result = generate_advice(customer, request.question)
    response = {
        "advice": result["advice"],
        "customer_name": customer.name,
    }
    if result["blockchain"]:
        response["tx_hash"] = result["blockchain"]["tx_hash"]
        response["etherscan_url"] = result["blockchain"]["etherscan_url"]
        response["record_hash"] = result["blockchain"]["record_hash"]
    return response

What does the updated endpoint do?

It calls generate_advice which now returns a dict with "advice" and "blockchain" keys. If blockchain recording succeeded (not None), the response includes tx_hash, etherscan_url, and record_hash. If it failed, the customer still gets their advice without blockchain fields.

You also need a verification endpoint so the frontend can independently check past advisory records against the blockchain. This completes the connection between your Python backend and the blockchain microservice.

  • Add the following /verify endpoint below the get_advice function and above the get_risk_score function:
@app.get("/verify")
def verify_advisory(
    customer_id: str,
    recommendation: str,
    amount: int,
    advisory_timestamp: int
):
    """Forward verification request to the blockchain microservice."""
    try:
        resp = requests.post(
            f"{BLOCKCHAIN_SERVICE_URL}/verify",
            json={
                "customer_id": customer_id,
                "recommendation": recommendation,
                "amount": amount,
                "advisory_timestamp": advisory_timestamp,
            },
            timeout=15,
        )
        if resp.status_code == 200:
            return resp.json()
        else:
            raise HTTPException(status_code=502, detail="Blockchain service error")
    except requests.exceptions.RequestException:
        raise HTTPException(status_code=503, detail="Blockchain service unreachable")

What does the verify endpoint do?

It accepts advisory parameters as query params, forwards them to the Node.js microservice's POST /verify endpoint, and returns the on-chain verification result. This acts as a passthrough so the frontend only needs to talk to one backend.

  • Save main.py.
  • Start (or restart) the FastAPI server by running:
uvicorn main:app --reload --port 8000
  • Make sure your Node.js blockchain service is still running in another terminal on port 3001.
  • Test the integrated system by running this curl command:
curl -X POST http://localhost:8000/advice -H "Content-Type: application/json" -d '{"customer_id": "cust_001", "question": "Should I increase my stock allocation?"}'

You should see a JSON response containing advice, customer_name, tx_hash, etherscan_url, and record_hash fields. The Etherscan URL should link to a confirmed Sepolia transaction.

Not seeing tx_hash in the response?

Check that the Node.js service is running on port 3001. If it crashed, restart it with npm run dev from the blockchain-service folder.

If the response has advice but no blockchain fields, look at the Python terminal output for error messages like "Blockchain service unreachable" or "Blockchain service error".

Help me debug why my Python backend isn't getting blockchain data back from the Node.js microservice.

✔️ Awesome, I've got everything!

Great. Double check you have saved both main.py and requirements.txt.

ⓧ I'd like to double check the full code

import requests
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse

from models import AdvisoryRequest
from data_service import get_customer_by_id, load_customers, calculate_portfolio_metrics
from advisory_engine import generate_advice, BLOCKCHAIN_SERVICE_URL
from risk_scorer import calculate_risk_score

app = FastAPI(title="WealthAI Advisory API")

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)


@app.get("/health")
def health_check():
    return {"status": "healthy", "service": "WealthAI Advisory"}


@app.get("/customers")
def list_customers():
    customers = load_customers()
    return [
        {"id": c.id, "name": c.name, "age": c.age, "income": c.income}
        for c in customers
    ]


@app.get("/customers/{customer_id}")
def get_customer(customer_id: str):
    customer = get_customer_by_id(customer_id)
    if not customer:
        raise HTTPException(status_code=404, detail="Customer not found")
    metrics = calculate_portfolio_metrics(customer)
    return {"profile": customer.model_dump(), "metrics": metrics}


@app.post("/advice")
def get_advice(request: AdvisoryRequest):
    customer = get_customer_by_id(request.customer_id)
    if not customer:
        raise HTTPException(status_code=404, detail="Customer not found")
    result = generate_advice(customer, request.question)
    response = {
        "advice": result["advice"],
        "customer_name": customer.name,
    }
    if result["blockchain"]:
        response["tx_hash"] = result["blockchain"]["tx_hash"]
        response["etherscan_url"] = result["blockchain"]["etherscan_url"]
        response["record_hash"] = result["blockchain"]["record_hash"]
    return response


@app.get("/verify")
def verify_advisory(
    customer_id: str,
    recommendation: str,
    amount: int,
    advisory_timestamp: int
):
    """Forward verification request to the blockchain microservice."""
    try:
        resp = requests.post(
            f"{BLOCKCHAIN_SERVICE_URL}/verify",
            json={
                "customer_id": customer_id,
                "recommendation": recommendation,
                "amount": amount,
                "advisory_timestamp": advisory_timestamp,
            },
            timeout=15,
        )
        if resp.status_code == 200:
            return resp.json()
        else:
            raise HTTPException(status_code=502, detail="Blockchain service error")
    except requests.exceptions.RequestException:
        raise HTTPException(status_code=503, detail="Blockchain service unreachable")


@app.get("/risk-score/{customer_id}")
def get_risk_score(customer_id: str):
    customer = get_customer_by_id(customer_id)
    if not customer:
        raise HTTPException(status_code=404, detail="Customer not found")
    return calculate_risk_score(customer)


app.mount("/static", StaticFiles(directory="static"), name="static")


@app.get("/")
def serve_frontend():
    return FileResponse("static/index.html")

Both services are now working together. Every advisory response is automatically anchored to an immutable blockchain record. Next up, you will update the frontend to show this verification status to users with clickable Etherscan links.

Update the Frontend with On-Chain Verification

Your backend is now recording every advisory interaction on the blockchain. But from the user's perspective, nothing has changed. They have no idea their financial advice is being permanently anchored to an immutable ledger.

In this step, you'll close the loop by adding a visual "Verified on-chain" badge to the frontend. Each badge links directly to the Sepolia Etherscan transaction, giving users instant proof that their advisory record exists on-chain.

In this step, get ready to:
  • Add a verification badge function to app.js that displays blockchain proof after each response.
  • Style the badge with a green accent color and chain-link indicator in style.css.
  • Test the complete integrated system end-to-end.
Add the verification badge to app.js

The /advice endpoint already returns etherscan_url in its JSON response. You need a function that takes that URL and renders a clickable badge in the chat area.

  • In static/app.js, find the closing brace of the addMessage function.
  • Add the following addVerificationBadge function directly below it:
function addVerificationBadge(etherscanUrl) {
    const badgeDiv = document.createElement("div");
    badgeDiv.className = "verification-badge";
    badgeDiv.innerHTML = '<a href="' + etherscanUrl + '" target="_blank">Verified on-chain</a>';
    chatArea.appendChild(badgeDiv);
    chatArea.scrollTop = chatArea.scrollHeight;
}

What does this code do?

This function creates a new div element with the class verification-badge and inserts an anchor tag linking to the Etherscan transaction page.

The target="_blank" attribute opens the link in a new tab so the user doesn't leave the advisor. The badge gets appended to chatArea and the view scrolls to keep it visible.

Now you need to call this function at the right moment. The badge should appear immediately after the advisor's response text renders.

  • Inside the sendQuestion function, find this line inside the try block:
        addMessage("advisor", data.advice);
  • Add the following code directly below that line:
        if (data.etherscan_url) {
            addVerificationBadge(data.etherscan_url);
        }

Why check for etherscan_url first?

The blockchain recording can fail silently (network timeout, Sepolia congestion). When that happens, the backend omits etherscan_url from the response. This conditional check ensures the badge only appears when a real on-chain record exists.

  • Save static/app.js.

Not sure where to place the code?

The addVerificationBadge function should sit between the addMessage function and the sendQuestion function. The if (data.etherscan_url) block goes inside sendQuestion's try block, right after the advisor message is added to the chat.

help me find the right location for the verification badge code in app.js

✔️ Awesome, I've got everything!

Great. Make sure you've saved static/app.js before moving on.

ⓧ I'd like to double check the full code

Here is the complete static/app.js file for comparison:

const customerDropdown = document.getElementById("customer-dropdown");
const questionInput = document.getElementById("question-input");
const sendBtn = document.getElementById("send-btn");
const chatArea = document.getElementById("chat-area");
const avatar = document.getElementById("avatar");
const avatarStatus = document.getElementById("avatar-status");

let selectedCustomerId = null;

async function loadCustomers() {
    const response = await fetch("/customers");
    const customers = await response.json();
    customers.forEach(function (customer) {
        const option = document.createElement("option");
        option.value = customer.id;
        option.textContent = customer.name + " (Age: " + customer.age + ", Income: $" + customer.income.toLocaleString() + ")";
        customerDropdown.appendChild(option);
    });
}

customerDropdown.addEventListener("change", async function () {
    selectedCustomerId = customerDropdown.value;
    if (selectedCustomerId) {
        questionInput.disabled = false;
        sendBtn.disabled = false;
        avatarStatus.textContent = "Ready to assist. Ask me anything!";
        chatArea.innerHTML = "";
        addMessage("advisor", "Hello! I have reviewed your financial profile. How can I help you today? You can ask me about investment strategies, spending optimization, retirement planning, or any other financial topic.");

        try {
            const riskResponse = await fetch("/risk-score/" + selectedCustomerId);
            const riskData = await riskResponse.json();
            if (riskData.warnings.length > 0) {
                const warningText = "Portfolio Alert: " + riskData.warnings[0];
                const warningMsg = addMessage("advisor", warningText);
                warningMsg.style.borderColor = "#d4a845";
                warningMsg.style.background = "#2a2a1a";
            }
        } catch (error) {
            console.error("Risk score check failed:", error);
        }
    } else {
        questionInput.disabled = true;
        sendBtn.disabled = true;
        avatarStatus.textContent = "Select a customer to begin";
    }
});

function addMessage(role, text) {
    const messageDiv = document.createElement("div");
    messageDiv.className = "message " + role;
    messageDiv.textContent = text;
    chatArea.appendChild(messageDiv);
    chatArea.scrollTop = chatArea.scrollHeight;
    return messageDiv;
}

function addVerificationBadge(etherscanUrl) {
    const badgeDiv = document.createElement("div");
    badgeDiv.className = "verification-badge";
    badgeDiv.innerHTML = '<a href="' + etherscanUrl + '" target="_blank">Verified on-chain</a>';
    chatArea.appendChild(badgeDiv);
    chatArea.scrollTop = chatArea.scrollHeight;
}

async function sendQuestion() {
    const question = questionInput.value.trim();
    if (!question || !selectedCustomerId) return;

    addMessage("user", question);
    questionInput.value = "";

    avatar.classList.add("speaking");
    avatarStatus.textContent = "Analyzing your profile...";
    const loadingMsg = addMessage("advisor", "Thinking...");
    loadingMsg.classList.add("loading");

    sendBtn.disabled = true;
    questionInput.disabled = true;

    try {
        const response = await fetch("/advice", {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify({
                customer_id: selectedCustomerId,
                question: question,
            }),
        });

        const data = await response.json();
        chatArea.removeChild(loadingMsg);
        addMessage("advisor", data.advice);

        if (data.etherscan_url) {
            addVerificationBadge(data.etherscan_url);
        }
    } catch (error) {
        chatArea.removeChild(loadingMsg);
        addMessage("advisor", "I apologize, but I encountered an issue processing your request. Please try again.");
    } finally {
        avatar.classList.remove("speaking");
        avatarStatus.textContent = "Ready to assist. Ask me anything!";
        sendBtn.disabled = false;
        questionInput.disabled = false;
        questionInput.focus();
    }
}

sendBtn.addEventListener("click", sendQuestion);
questionInput.addEventListener("keydown", function (e) {
    if (e.key === "Enter") sendQuestion();
});

loadCustomers();
Style the verification badge

The badge needs to stand out from regular chat messages. A green accent color signals trust and verification, and a chain-link icon reinforces the blockchain connection.

  • Open static/style.css and add the following styles at the bottom of the file:
/* Blockchain verification badge */
.verification-badge {
    display: inline-flex;
    align-items: center;
    margin: 8px 0 8px 48px;
    padding: 6px 12px;
    background: rgba(34, 197, 94, 0.1);
    border: 1px solid rgba(34, 197, 94, 0.3);
    border-radius: 16px;
    font-size: 0.8rem;
}

.verification-badge::before {
    content: "\1F517";
    margin-right: 6px;
}

.verification-badge a {
    color: #22c55e;
    text-decoration: none;
    font-weight: 500;
}

.verification-badge a:hover {
    text-decoration: underline;
}

What do these styles do?

  • The .verification-badge container uses a semi-transparent green background with a matching border, creating a subtle pill shape.
  • The ::before pseudo-element inserts a chain-link emoji as a visual indicator of blockchain provenance.
  • The anchor styles use #22c55e (a green that signals "verified") and underline on hover for accessibility.
  • Save static/style.css.

Badge not showing any green?

Make sure the class name in your CSS (.verification-badge) matches the class set in your JavaScript ("verification-badge"). Also confirm that style.css is linked in your index.html file.

help me debug why my verification badge styles aren't appearing

Test the complete integrated system

You now have all three layers wired together: the Python AI backend, the Node.js blockchain microservice, and the frontend. Time to verify the full flow end-to-end.

  • Make sure both servers are running. Your uvicorn server should be on port 8000 and your Node.js blockchain service on port 3001.
  • Open your browser and navigate to http://localhost:8000.
  • Select a customer from the dropdown.
  • Type a question like Should I rebalance my portfolio? and press Enter.
  • Watch the avatar pulse while processing. After the advisory response appears, you should see a green "Verified on-chain" badge below the message.
  • Click the badge link. It should open a new tab showing a confirmed transaction on Sepolia Etherscan.

Why does the badge take a moment to appear?

The flow is sequential: Gemini generates the advice, then the Python backend calls the Node.js service, which submits a transaction to Sepolia and waits for block confirmation. This takes 12-15 seconds. The badge only appears once the transaction is confirmed on-chain.

Advice appears but no badge?

  • Check that your Node.js blockchain service is running. Open a new terminal tab and run curl http://localhost:3001/health to confirm it responds.
  • Look at your uvicorn terminal for messages like "Blockchain service unreachable". This means the Python backend cannot reach port 3001.
  • Open the browser console (Cmd+Option+J on macOS or Ctrl+Shift+J on Windows) and check that the /advice response includes an etherscan_url field.

help me debug why my verification badge isn't appearing after advisory responses

Secret mission

Add a Verification Panel

Your advisor now records interactions automatically. But how does a regulator or auditor verify a past record? Build a verification panel that lets users input advisory details and independently prove the record has not been tampered with.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally with Sepolia test ETH (not real money). There are no ongoing costs while your servers are stopped. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

Resources you used:

  • Running uvicorn server on port 8000 (local process, no cost while stopped).
  • Running Node.js blockchain service on port 3001 (local process, no cost while stopped).
  • The blockchain-service/ project folder on your Desktop.
  • Your Alchemy Sepolia RPC app (free tier, 300M compute units/month).
  • Your deployed AdvisoryAuditTrail contract on Sepolia (permanent by design, no ongoing cost).

Keep everything running

No action needed. Choose this if you plan to keep experimenting with the advisor and blockchain integration.

  • Both servers only run when you start them manually. No background processes persist after you close the terminal.
  • Sepolia transactions cost test ETH only. Your Alchemy free tier handles development traffic easily.
  • Your deployed contract remains on Sepolia permanently at zero cost.

Pause - I'll come back to this later

Shut down running processes to free up terminal windows. Your files, contract, and accounts persist at zero cost.

  • Switch to the terminal running your Node.js blockchain service and press Ctrl+C to stop it.
  • Switch to the terminal running uvicorn and press Ctrl+C to stop it.
  • Your project folders, contract, and Alchemy app all persist at zero cost until you return.

Delete - I don't want to use this again

Remove all local project resources and clean up your Alchemy app. Your deployed contract remains on Sepolia permanently (this is by design and costs nothing).

  • Switch to the terminal running your Node.js blockchain service and press Ctrl+C to stop it.
  • Switch to the terminal running uvicorn and press Ctrl+C to stop it.
  • Delete the blockchain-service/ folder by running this in your terminal:

macOS

cd ~/Desktop
rm -rf blockchain-service

Windows

cd ~\Desktop
rmdir /s /q blockchain-service
  • To delete your Alchemy Sepolia app, go to the Alchemy dashboard, select your app under the Apps tab, click Settings, then click Delete in the Delete App section.
  • Your deployed AdvisoryAuditTrail contract on Sepolia cannot be removed. This is by design. It costs nothing to leave on-chain.

Nice Work!

Nice Work!

You just built a production-grade microservice architecture where AI-generated financial advice is automatically anchored to an immutable blockchain ledger. Every recommendation your Gemini advisor produces now has a tamper-proof on-chain record that regulators and auditors can independently verify.

You've learned how to:

  • Build a dedicated Node.js Express microservice that handles all blockchain operations, following industry-standard microservice architecture patterns for service separation.
  • Integrate automatic on-chain recording into a Python FastAPI advisory flow via inter-service HTTP calls, ensuring every AI-generated recommendation is permanently anchored to an immutable ledger.
  • Update a frontend with real-time blockchain verification badges and clickable Etherscan links that prove each advisory response was recorded on-chain.
  • Secret Mission: Built a verification panel that independently proves advisory record integrity by recomputing keccak256 hashes and comparing them against on-chain records to detect tampering.

Ready to quiz yourself?