Developers Forum for XinFin XDC Network

Rushabh Parmar
Rushabh Parmar

Posted on

Integrating AI with Smart Contracts on XDC Network

๐ŸŽฏ What You Will Build

In this guide you will manually deploy a real, working AI-powered smart contract on XDC Apothem Testnet step by step. Here is the contract we deployed live while writing this article:

Contract Address 0xE031843227668f530DC0E59Bb093ED6763Dcd84D
Deploy TX 0x7fa75b52...c7843
Loan Application TX 0xe95ba331...4387
AI Score Submission TX 0xcf230923...edf9
Network XDC Apothem Testnet (Chain ID 51)
AI Score 711 / 1000
Risk Category MEDIUM
Decision โœ… APPROVED

You can verify every transaction right now on XDCScan Testnet.


Introduction

Blockchain and Artificial Intelligence are two of the most transformative technologies of our time. Separately they are powerful. Together they unlock entirely new possibilities โ€” smarter contracts, automated decision-making, and decentralized intelligence.

XDC Network is a perfect platform for this combination:

  • EVM compatible โ€” write contracts in Solidity, use standard tools like Hardhat
  • Near-zero gas fees (~$0.00001 per transaction)
  • 2-second finality โ€” AI decisions confirmed almost instantly
  • 2,000 TPS โ€” scales to high-frequency AI-triggered actions

What Are We Building?

An AI-Powered Loan Approval System โ€” a real-world pattern used in trade finance and credit scoring:

[ User calls applyForLoan() on-chain ]
              โ†“
[ AI model analyzes applicant off-chain ]
              โ†“
[ Oracle submits credit score on-chain ]
              โ†“
[ Contract automatically APPROVES or REJECTS ]
              โ†“
[ Decision is permanent and publicly verifiable ]
Enter fullscreen mode Exit fullscreen mode

This is called the oracle pattern โ€” the standard way to connect AI (off-chain) with smart contracts (on-chain). The AI runs off-chain because it is too expensive to run ML models on-chain. A trusted "oracle" wallet then submits the result to the smart contract.


Prerequisites

Tool Version Install
Node.js v18+ nodejs.org
Python v3.10+ python.org
MetaMask Latest metamask.io
Git Any git-scm.com

Step 1 โ€” Add XDC Apothem Testnet to MetaMask

Open MetaMask โ†’ Settings โ†’ Networks โ†’ Add Network and fill in:

Field Value
Network Name XDC Apothem Testnet
RPC URL https://erpc.apothem.network
Chain ID 51
Currency Symbol TXDC
Block Explorer https://testnet.xdcscan.com

Then get free test XDC from the faucet:

๐Ÿ‘‰ https://faucet.apothem.network

Important: You need two wallets for this tutorial โ€” one as the Deployer (contract owner) and one as the Oracle (the AI result submitter). Create both in MetaMask and fund both from the faucet.


Step 2 โ€” Set Up the Project

Open your terminal (Command Prompt on Windows) and run:

mkdir xdc-ai-demo
cd xdc-ai-demo
npm init -y
npm install --save-dev hardhat@2.22.19 @nomicfoundation/hardhat-ethers@3 ethers@6 dotenv --legacy-peer-deps
Enter fullscreen mode Exit fullscreen mode

Once installed, initialise Hardhat:

npx hardhat init
Enter fullscreen mode Exit fullscreen mode

When prompted, select "Create a JavaScript project" and accept all defaults.


Step 3 โ€” Configure Hardhat for XDC Apothem

Create a .env file in the root of your project โ€” never commit this to Git:

DEPLOYER_KEY=your_deployer_private_key_here
ORACLE_KEY=your_oracle_private_key_here
Enter fullscreen mode Exit fullscreen mode

To export a private key from MetaMask: click your account โ†’ Account Details โ†’ Export Private Key. Use test-only wallets only.

Add .env to .gitignore:

echo ".env" >> .gitignore
Enter fullscreen mode Exit fullscreen mode

Now update hardhat.config.js:

require("@nomicfoundation/hardhat-ethers");
require("dotenv").config();

module.exports = {
  solidity: "0.8.20",
  networks: {
    apothem: {
      url:      "https://erpc.apothem.network",
      accounts: [process.env.DEPLOYER_KEY, process.env.ORACLE_KEY],
      chainId:  51,

      // โš ๏ธ  XDC-specific settings โ€” required for successful deployment
      // After the XDC 2.0 upgrade, Apothem minimum gas price is 12.5 gwei
      // XDC does NOT support EIP-1559 โ€” always use gasPrice (not maxFeePerGas)
      gasPrice: 12500000000,   // 12.5 gwei in wei
    }
  }
};
Enter fullscreen mode Exit fullscreen mode

Step 4 โ€” Write the Smart Contract

Delete the default contracts/Lock.sol and create contracts/AIScoreOracle.sol:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

/// @title AIScoreOracle
/// @notice AI-powered loan approval contract on XDC Network
/// @dev Live on Apothem: 0xE031843227668f530DC0E59Bb093ED6763Dcd84D
contract AIScoreOracle {

    address public owner;
    address public trustedOracle;     // Wallet that submits AI scores
    uint256 public approvalThreshold; // Minimum score to get approved (0โ€“1000)
    uint256 public totalApplications;

    struct Application {
        address applicant;
        uint256 aiScore;          // AI credit score (0โ€“1000)
        string  riskCategory;     // "LOW", "MEDIUM", "HIGH", "PENDING"
        bool    approved;
        bool    processed;
        uint256 timestamp;
    }

    mapping(address => Application) public applications;
    address[] public applicantList;

    event LoanApplied(address indexed applicant, uint256 timestamp);
    event AIScoreSubmitted(
        address indexed applicant,
        uint256 score,
        string  risk,
        bool    approved
    );

    modifier onlyOwner()  { require(msg.sender == owner,         "Only owner");  _; }
    modifier onlyOracle() { require(msg.sender == trustedOracle, "Only oracle"); _; }

    constructor(address _oracle, uint256 _threshold) {
        owner             = msg.sender;
        trustedOracle     = _oracle;
        approvalThreshold = _threshold;
    }

    /// @notice Any user can apply for a loan (once per address)
    function applyForLoan() external {
        require(applications[msg.sender].timestamp == 0, "Already applied");

        applications[msg.sender] = Application({
            applicant:    msg.sender,
            aiScore:      0,
            riskCategory: "PENDING",
            approved:     false,
            processed:    false,
            timestamp:    block.timestamp
        });

        applicantList.push(msg.sender);
        totalApplications++;
        emit LoanApplied(msg.sender, block.timestamp);
    }

    /// @notice Oracle submits the AI credit score for an applicant
    function submitAIScore(address applicant, uint256 score) external onlyOracle {
        require(score <= 1000,                          "Score must be 0-1000");
        require(applications[applicant].timestamp != 0, "No application found");
        require(!applications[applicant].processed,     "Already processed");

        Application storage app = applications[applicant];
        app.aiScore   = score;
        app.processed = true;
        app.approved  = score >= approvalThreshold;

        if      (score >= 800)               app.riskCategory = "LOW";
        else if (score >= approvalThreshold) app.riskCategory = "MEDIUM";
        else                                 app.riskCategory = "HIGH";

        emit AIScoreSubmitted(applicant, score, app.riskCategory, app.approved);
    }

    /// @notice Read the loan decision for any address
    function getDecision(address applicant) external view returns (
        uint256 score,
        string memory risk,
        bool    approved,
        bool    processed,
        uint256 appliedAt
    ) {
        Application memory app = applications[applicant];
        return (app.aiScore, app.riskCategory, app.approved, app.processed, app.timestamp);
    }

    /// @notice Total number of applicants ever
    function getApplicantCount() external view returns (uint256) {
        return applicantList.length;
    }
}
Enter fullscreen mode Exit fullscreen mode

What each function does

Function Who Calls It What It Does
applyForLoan() Any user Registers a loan application on-chain
submitAIScore(address, uint256) Oracle only Submits AI score, triggers approve/reject
getDecision(address) Anyone Reads the stored decision for any address
getApplicantCount() Anyone Returns total number of applicants

Step 5 โ€” Compile the Contract

npx hardhat compile
Enter fullscreen mode Exit fullscreen mode

Expected output:

Compiled 1 Solidity file successfully (evm target: paris).
Enter fullscreen mode Exit fullscreen mode

Step 6 โ€” Write the Deploy Script

Delete scripts/deploy.js and recreate it:

// scripts/deploy.js
const hre = require("hardhat");

async function main() {
  const [deployer, oracle] = await hre.ethers.getSigners();

  console.log("Deploying AIScoreOracle to XDC Apothem...\n");
  console.log("Deployer :", deployer.address);
  console.log("Oracle   :", oracle.address);

  const deployerBalance = await hre.ethers.provider.getBalance(deployer.address);
  console.log("Balance  :", hre.ethers.formatEther(deployerBalance), "TXDC\n");

  // Deploy: pass oracle address and approval threshold of 700 out of 1000
  const AIScoreOracle = await hre.ethers.getContractFactory("AIScoreOracle");
  const contract      = await AIScoreOracle.deploy(oracle.address, 700, {
    gasLimit: 2_000_000,      // Set manually โ€” XDC estimateGas is unreliable
    gasPrice: hre.ethers.parseUnits("12.5", "gwei")  // Apothem minimum after XDC 2.0
  });

  await contract.waitForDeployment();

  const contractAddress = await contract.getAddress();
  const deployTx        = contract.deploymentTransaction();

  console.log("Contract deployed!");
  console.log("Address  :", contractAddress);
  console.log("TX Hash  :", deployTx.hash);
  console.log("Explorer :", `https://testnet.xdcscan.com/address/${contractAddress}`);
  console.log("TX Link  :", `https://testnet.xdcscan.com/tx/${deployTx.hash}`);
}

main().catch((err) => {
  console.error(err);
  process.exit(1);
});
Enter fullscreen mode Exit fullscreen mode

Step 7 โ€” Deploy to XDC Apothem

npx hardhat run scripts/deploy.js --network apothem
Enter fullscreen mode Exit fullscreen mode

You will see output like:

Deploying AIScoreOracle to XDC Apothem...

Deployer : 0x2D9305d0c20106beDA3C19d9B8ac8f8AD061b0D5
Oracle   : 0xE65AbC44aa93B25320D9BB9eB6B5cEa3C0a21872
Balance  : 1000.0 TXDC

Contract deployed!
Address  : 0xE031843227668f530DC0E59Bb093ED6763Dcd84D
TX Hash  : 0x7fa75b52a379b20a1c3a4d8966a7c78f1ac29ba69eb25dded01185dcc66c7843
Explorer : https://testnet.xdcscan.com/address/0xE031843227668f530DC0E59Bb093ED6763Dcd84D
TX Link  : https://testnet.xdcscan.com/tx/0x7fa75b52a379b20a1c3a4d8966a7c78f1ac29ba69eb25dded01185dcc66c7843
Enter fullscreen mode Exit fullscreen mode

Open the Explorer link โ€” your contract is live on XDC Apothem! ๐ŸŽ‰


Step 8 โ€” User Applies for a Loan

Write an interaction script. Create scripts/apply.js:

// scripts/apply.js
const hre = require("hardhat");

const CONTRACT_ADDRESS = "YOUR_DEPLOYED_CONTRACT_ADDRESS";

async function main() {
  const [deployer] = await hre.ethers.getSigners();

  const AIScoreOracle = await hre.ethers.getContractFactory("AIScoreOracle");
  const contract      = AIScoreOracle.attach(CONTRACT_ADDRESS);

  console.log("Applying for a loan from:", deployer.address);

  const tx = await contract.applyForLoan({
    gasLimit: 300_000,
    gasPrice: hre.ethers.parseUnits("12.5", "gwei")
  });
  await tx.wait();

  console.log("Application submitted!");
  console.log("TX Hash  :", tx.hash);
  console.log("TX Link  :", `https://testnet.xdcscan.com/tx/${tx.hash}`);

  // Check pending status
  const [score, risk, approved, processed] = await contract.getDecision(deployer.address);
  console.log("\nOn-chain status:");
  console.log("  Risk      :", risk);       // PENDING
  console.log("  Processed :", processed);  // false
  console.log("  Score     :", score.toString());
  console.log("\nWaiting for AI oracle to score this application...");
}

main().catch((err) => { console.error(err); process.exit(1); });
Enter fullscreen mode Exit fullscreen mode

Run it:

npx hardhat run scripts/apply.js --network apothem
Enter fullscreen mode Exit fullscreen mode

Output from our live deployment:

Applying for a loan from: 0x2D9305d0c20106beDA3C19d9B8ac8f8AD061b0D5
Application submitted!
TX Hash  : 0xe95ba3312aa0e710fb45cc6980c271a499570c65d89eb45cadda118719d64387
TX Link  : https://testnet.xdcscan.com/tx/0xe95ba3312aa0e710fb45cc6980c271a499570c65d89eb45cadda118719d64387

On-chain status:
  Risk      : PENDING
  Processed : false
  Score     : 0

Waiting for AI oracle to score this application...
Enter fullscreen mode Exit fullscreen mode

Step 9 โ€” AI Oracle Submits the Score

Now the oracle wallet submits the AI-generated score. Create scripts/submit_score.js:

// scripts/submit_score.js
const hre = require("hardhat");

const CONTRACT_ADDRESS = "YOUR_DEPLOYED_CONTRACT_ADDRESS";
const APPLICANT        = "THE_APPLICANT_WALLET_ADDRESS";
const AI_SCORE         = 711;   // Replace with your AI model's output (0โ€“1000)

async function main() {
  // Use the second signer โ€” the oracle wallet
  const [, oracle] = await hre.ethers.getSigners();

  const AIScoreOracle = await hre.ethers.getContractFactory("AIScoreOracle");
  const contract      = AIScoreOracle.attach(CONTRACT_ADDRESS);

  console.log("Oracle wallet     :", oracle.address);
  console.log("Applicant         :", APPLICANT);
  console.log("AI Credit Score   :", AI_SCORE, "/ 1000");
  console.log("Approval Threshold: 700 / 1000");
  console.log("Decision          :", AI_SCORE >= 700 ? "APPROVE" : "REJECT");
  console.log("\nSubmitting AI score to blockchain...");

  const tx = await contract.connect(oracle).submitAIScore(APPLICANT, AI_SCORE, {
    gasLimit: 300_000,
    gasPrice: hre.ethers.parseUnits("12.5", "gwei")
  });
  await tx.wait();

  console.log("\nAI score committed on-chain!");
  console.log("TX Hash  :", tx.hash);
  console.log("TX Link  :", `https://testnet.xdcscan.com/tx/${tx.hash}`);

  // Read final decision
  const [score, risk, approved, processed] = await contract.getDecision(APPLICANT);
  console.log("\nFinal decision from contract:");
  console.log("  AI Score   :", score.toString(), "/ 1000");
  console.log("  Risk Level :", risk);
  console.log("  Processed  :", processed);
  console.log("  Decision   :", approved ? "APPROVED โœ…" : "REJECTED โŒ");
}

main().catch((err) => { console.error(err); process.exit(1); });
Enter fullscreen mode Exit fullscreen mode

Run it:

npx hardhat run scripts/submit_score.js --network apothem
Enter fullscreen mode Exit fullscreen mode

Output from our live deployment:

Oracle wallet     : 0xE65AbC44aa93B25320D9BB9eB6B5cEa3C0a21872
Applicant         : 0x2D9305d0c20106beDA3C19d9B8ac8f8AD061b0D5
AI Credit Score   : 711 / 1000
Approval Threshold: 700 / 1000
Decision          : APPROVE

Submitting AI score to blockchain...

AI score committed on-chain!
TX Hash  : 0xcf230923452c8d054435b685ce67f7ad138e4b37e1688e2e8a6852b8af12edf9
TX Link  : https://testnet.xdcscan.com/tx/0xcf230923452c8d054435b685ce67f7ad138e4b37e1688e2e8a6852b8af12edf9

Final decision from contract:
  AI Score   : 711 / 1000
  Risk Level : MEDIUM
  Processed  : true
  Decision   : APPROVED โœ…
Enter fullscreen mode Exit fullscreen mode

Step 10 โ€” Verify Everything On-Chain

Open the live contract on XDCScan Testnet:

๐Ÿ‘‰ https://testnet.xdcscan.com/address/0xE031843227668f530DC0E59Bb093ED6763Dcd84D

Transactions tab โ€” all 3 transactions visible:

TX Hash Action Gas Used
0x7fa75b52... Contract deploy 907,449
0xe95ba331... applyForLoan() 161,014
0xcf230923... submitAIScore(711) 83,122

Events tab โ€” two on-chain events emitted:

LoanApplied(
  applicant = 0x2D9305d0c20106beDA3C19d9B8ac8f8AD061b0D5,
  timestamp = 1746...
)

AIScoreSubmitted(
  applicant = 0x2D9305d0c20106beDA3C19d9B8ac8f8AD061b0D5,
  score     = 711,
  risk      = "MEDIUM",
  approved  = true
)
Enter fullscreen mode Exit fullscreen mode

Read Contract tab โ€” call getDecision(0x2D9305d0c20106beDA3C19d9B8ac8f8AD061b0D5):

score     โ†’ 711
risk      โ†’ MEDIUM
approved  โ†’ true
processed โ†’ true
appliedAt โ†’ 1746...
Enter fullscreen mode Exit fullscreen mode

Every decision is permanent, immutable, and publicly verifiable โ€” no central authority, no trust required.


Step 11 โ€” Replace Demo AI with a Real Model

In submit_score.js the score is hardcoded as 711. In production, replace that with a real ML model call.

Python scikit-learn example:

# ai_model.py
import joblib
import numpy as np

model = joblib.load("credit_model.pkl")

def get_ai_score(applicant_address: str) -> int:
    """
    Fetch applicant data, run the model, return a score 0โ€“1000.
    """
    features = fetch_applicant_features(applicant_address)
    probability = model.predict_proba([features])[0][1]
    return int(probability * 1000)
Enter fullscreen mode Exit fullscreen mode

Then expose it as an API:

# oracle_api.py  (Flask)
from flask import Flask, jsonify, request
from ai_model import get_ai_score

app = Flask(__name__)

@app.route("/score", methods=["POST"])
def score():
    address = request.json["address"]
    return jsonify({ "score": get_ai_score(address) })
Enter fullscreen mode Exit fullscreen mode

And call it from your Hardhat script:

// In submit_score.js โ€” replace the hardcoded score:
const response = await fetch("http://localhost:5000/score", {
    method:  "POST",
    headers: { "Content-Type": "application/json" },
    body:    JSON.stringify({ address: APPLICANT })
});
const { score: AI_SCORE } = await response.json();
Enter fullscreen mode Exit fullscreen mode

Everything else โ€” the Solidity contract, the Hardhat scripts, the on-chain verification โ€” stays exactly the same.


Important XDC Developer Notes

These are lessons learned from our actual deployment:

Issue Root Cause Fix
under min gas price XDC 2.0 upgrade raised Apothem minimum Use 12.5 gwei minimum
estimateGas returns null XDC Apothem doesn't support eth_estimateGas reliably Set gasLimit manually
EIP-1559 tx rejected XDC does not support type-2 transactions Use gasPrice not maxFeePerGas

Full Live Transaction Summary

All transactions verified on XDCScan Testnet:

Step TX Hash Link
Deploy contract 0x7fa75b52...c7843 View โ†—
Apply for loan 0xe95ba331...4387 View โ†—
AI score (711) submitted 0xcf230923...edf9 View โ†—

Key Takeaways

  • XDC is fully EVM-compatible โ€” Solidity and Hardhat work exactly as on Ethereum
  • The oracle pattern is the standard way to bridge AI (off-chain) and smart contracts (on-chain)
  • XDC Apothem gas settings: minimum 12.5 gwei, always set gasLimit manually, avoid EIP-1559
  • XDC's near-zero fees make per-user AI-triggered actions economically viable at scale
  • Every decision is permanent, immutable, and verifiable on XDCScan Testnet

Resources

Discussion (0)