Flare Studio Logo
Analytics Dashboard

Complete Foundry Web2JSON Guide: From Setup to Production

Official Flare Foundry documentation with complete setup instructions, step-by-step breakdowns, and beginner-friendly explanations.

Quick Start: Foundry Setup Guide

Step 1: Clone and Setup Foundry

bash
# Install Foundry
curl -L https://foundry.paradigm.xyz | bash
foundryup

# Clone the Flare Foundry starter
git clone https://github.com/flare-foundation/flare-foundry-starter
cd flare-foundry-starter

# Install dependencies
forge install

Step 2: Environment Configuration

Create a .env file in the project root:

bash
# .env - Foundry Configuration
PRIVATE_KEY=0xYourPrivateKeyHere
COSTON2_RPC_URL=https://coston2-api.flare.network/ext/C/rpc
FLARE_RPC_URL=https://flare-api.flare.network/ext/C/rpc
VERIFIER_API_KEY=00000000-0000-0000-0000-000000000000
WEB2JSON_VERIFIER_URL_TESTNET=https://web2json-verifier-test.flare.rocks/
COSTON2_DA_LAYER_URL=https://ctn2-data-availability.flare.network/
X_API_KEY=your_da_layer_api_key

Step 3: Build the Project

bash
# Build all contracts
forge build

Web2Json Attestation Type

The Web2Json attestation type enables data collection from an arbitrary Web2 source. You can learn more about it in the official specification.

Info: The Web2Json attestation type is currently only available on the Flare Testnet Coston2.

We will now demonstrate how the FDC protocol can be used to collect the data of a given Star Wars API request. The request we will be making is https://swapi.info/api/people/3. The same procedure works for all public APIs.

In this guide, we will follow the steps outlined in the FDC overview. We will define separate scripts in script/fdcExample/Web2Json.s.sol that handle different stages of the validation process.

Foundry Script Architecture

Simple Explanation: Foundry uses a modular approach where each step is a separate contract. Think of it like having different specialists for each job in a factory assembly line.

Factory Assembly Line Analogy:
PrepareAttestationRequest: Design department (creates the blueprint)
SubmitAttestationRequest: Shipping department (sends to blockchain)
RetrieveDataAndProof: Quality control (verifies the product)
Deploy: Manufacturing (creates the final product)

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

import {Script} from "dependencies/forge-std-1.9.5/src/Script.sol";

string constant attestationTypeName = "Web2Json";
string constant dirPath = "data/";

// Each contract handles one specific step
contract PrepareAttestationRequest is Script {
    // Creates the request blueprint
}

contract SubmitAttestationRequest is Script {
    // Sends request to blockchain
}

contract RetrieveDataAndProof is Script {
    // Gets verified data back
}

contract Deploy is Script {
    // Deploys our application contract
}

Why Multiple Contracts? This modular approach makes the code more maintainable and allows running individual steps independently. Each contract focuses on one specific responsibility.

Step 1: Prepare Your Data Request

Simple Explanation: This is where we create a detailed “shopping list“ that tells Flare exactly what data we want and how to process it.

Required Fields Breakdown

attestationType

Tells Flare “I want Web2 data“ → Converted to hex: “Web2Json“

sourceId

Tells Flare “From public websites“ → Converted to hex: “PublicWeb2“

Request Body Details

The requestBody is where we specify exactly what data to fetch and how to process it:

json
{
  "url": "Where to get the data (website address)",
  "httpMethod": "How to fetch it (GET, POST, etc.)", 
  "headers": "Any special headers needed",
  "queryParams": "URL parameters to include",
  "body": "Data to send for POST requests",
  "postProcessJq": "Which fields to extract and how to format them",
  "abiSignature": "How to package the data for blockchain use"
}

Real Example: Star Wars API Request

Here's the actual configuration we use to fetch R2-D2's data:

solidity
// Inside PrepareAttestationRequest contract
string public apiUrl = "https://swapi.info/api/people/3";
string public httpMethod = "GET";
string public headers = '{\"Content-Type\":\"text/plain\"}';
string public queryParams = "{}";
string public body = "{}";
string public postProcessJq =
    '{name: .name, height: .height, mass: .mass, numberOfFilms: .films | length, uid: (.url | split(\"/\") | .[-1] | tonumber)}';
string public abiSignature =
    '{\"components\": [{\"internalType\": \"string\", \"name\": \"name\", \"type\": \"string\"},{\"internalType\": \"uint256\", \"name\": \"height\", \"type\": \"uint256\"},{\"internalType\": \"uint256\", \"name\": \"mass\", \"type\": \"uint256\"},{\"internalType\": \"uint256\", \"name\": \"numberOfFilms\", \"type\": \"uint256\"},{\"internalType\": \"uint256\", \"name\": \"uid\", \"type\": \"uint256\"}],\"name\": \"task\",\"type\": \"tuple\"}';

What Each Field Does:

  • postProcessJq: “Extract name, height, mass, count films, and get ID from URL“
  • abiSignature: “Package data as a tuple with these specific types for the blockchain“
  • headers: “Use text/plain content type for this API“

Step 2: Text to Hex Conversion

Simple Explanation: Blockchain computers don't understand normal text. These functions translate human-readable words into computer-readable hex codes.

Language Translation Analogy:
Human: “Web2Json“ (English words)
Computer: “0x576562324a736f6e0000...“ (Computer language)

The Encoding Process

Flare provides two key functions in their Base library to handle this conversion:

1. toHexString - Basic Hex Conversion

solidity
function toHexString(bytes memory data) public pure returns (string memory) {
    bytes memory alphabet = "0123456789abcdef";
    
    bytes memory str = new bytes(2 + data.length * 2);
    str[0] = "0";
    str[1] = "x";
    for (uint i = 0; i < data.length; i++) {
        str[2 + i * 2] = alphabet[uint(uint8(data[i] >> 4))];
        str[3 + i * 2] = alphabet[uint(uint8(data[i] & 0x0f))];
    }
    return string(str);
}

How This Works:

  • • Takes raw bytes and converts each byte to two hex characters
  • • Adds “0x“ prefix so blockchain knows it's hex data
  • • Example: “W“ → 0x57, “e“ → 0x65, “b“ → 0x62

2. toUtf8HexString - UTF8 with Padding

solidity
function toUtf8HexString(string memory _string) internal pure returns (string memory) {
    string memory encodedString = toHexString(abi.encodePacked(_string));
    uint256 stringLength = bytes(encodedString).length;
    require(stringLength <= 64, "String too long");
    uint256 paddingLength = 64 - stringLength + 2;
    for (uint256 i = 0; i < paddingLength; i++) {
        encodedString = string.concat(encodedString, "0");
    }
    return encodedString;
}

Why Padding Matters:

  • • Blockchain expects exactly 32 bytes (64 hex characters)
  • • “Web2Json“ is only 8 characters → needs padding to reach 32 bytes
  • • Adds zeros to fill the remaining space
  • • Final result: “0x576562324a736f6e0000000..“

Real Conversion Examples

text
Input: "Web2Json"
Step 1: Convert to bytes → [0x57, 0x65, 0x62, 0x32, 0x4a, 0x73, 0x6f, 0x6e]
Step 2: Convert to hex → "0x576562324a736f6e"
Step 3: Add padding → "0x576562324a736f6e000..."

Input: "PublicWeb2"  
Output: "0x5075626c696357656232000..."

Step 3: Build the Complete Request

Simple Explanation: Now we package everything together into a proper HTTP request that we can send to Flare's verifier service.

Shipping Package Analogy:
Headers: Shipping label with destination and security info
Body: The actual contents of your package
API Key: Your security clearance for the shipping company

Helper Functions

Flare provides helper functions to build the request properly:

solidity
function prepareAttestationRequest(
    string memory attestationType,
    string memory sourceId,
    string memory requestBody
) internal view returns (string[] memory, string memory) {
    // We read the API key from the .env file
    string memory apiKey = vm.envString("VERIFIER_API_KEY");

    // Preparing headers
    string[] memory headers = prepareHeaders(apiKey);
    // Preparing body
    string memory body = prepareBody(attestationType, sourceId, requestBody);

    console.log("headers: %s", string.concat("{", headers[0], ", ", headers[1]), "}");
    console.log("body: %s", body);
    return (headers, body);
}

function prepareHeaders(string memory apiKey) internal pure returns (string[] memory) {
    string[] memory headers = new string[](2);
    headers[0] = string.concat('"X-API-KEY": ', apiKey);
    headers[1] = '"Content-Type": "application/json"';
    return headers;
}

function prepareBody(
    string memory attestationType,
    string memory sourceId,
    string memory body
) internal pure returns (string memory) {
    return string.concat(
        '{"attestationType": ',
        '"', attestationType, '"',
        ', "sourceId": ',
        '"', sourceId, '"',
        ', "requestBody": ',
        body,
        "}"
    );
}

What Gets Created:

json
{
  "attestationType": "0x576562324a736f6e000000000000000000000000000000000000000000000000",
  "sourceId": "0x5075626c6963576562320000000000000000000000000000000000000000000000", 
  "requestBody": {
    "url": "https://swapi.info/api/people/3",
    "httpMethod": "GET",
    "headers": "{\"Content-Type\":\"text/plain\"}",
    "queryParams": "{}",
    "body": "{}",
    "postProcessJq": "{name: .name, height: .height, mass: .mass, numberOfFilms: .films | length, uid: (.url | split(\"/\") | .[-1] | tonumber)}",
    "abiSignature": "{\"components\": [{\"internalType\": \"string\", \"name\": \"name\", \"type\": \"string\"},{\"internalType\": \"uint256\", \"name\": \"height\", \"type\": \"uint256\"},{\"internalType\": \"uint256\", \"name\": \"mass\", \"type\": \"uint256\"},{\"internalType\": \"uint256\", \"name\": \"numberOfFilms\", \"type\": \"uint256\"},{\"internalType\": \"uint256\", \"name\": \"uid\", \"type\": \"uint256\"}],\"name\": \"task\",\"type\": \"tuple\"}"
  }
}

Running the Prepare Script

bash
source .env
forge script script/fdcExample/Web2Json.s.sol:PrepareAttestationRequest \
  --private-key $PRIVATE_KEY \
  --rpc-url $COSTON2_RPC_URL \
  --etherscan-api-key $FLARE_RPC_API_KEY \
  --broadcast \
  --ffi

What Happens: This script creates the request and saves the abiEncodedRequest to a file (data/Web2Json_abiEncodedRequest.txt) for the next step.

Step 4: Submit Request to Blockchain

Simple Explanation: Now we take our prepared request and actually submit it to the Flare blockchain. This is like paying for and shipping your package.

Shipping Process Analogy:
Check shipping cost: See how much gas fee is required
Pay and ship: Submit transaction with the fee
Get tracking number: Receive transaction hash and round ID

The Submission Process

This step involves interacting with Flare's smart contracts to submit our request:

solidity
function submitAttestationRequest(bytes memory abiEncodedRequest) internal {
    // Step 1: Load private key and check request fee
    uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
    vm.startBroadcast(deployerPrivateKey);
    
    IFdcRequestFeeConfigurations fdcRequestFeeConfigurations = ContractRegistry
        .getFdcRequestFeeConfigurations();
    uint256 requestFee = fdcRequestFeeConfigurations.getRequestFee(abiEncodedRequest);
    console.log("request fee: %s", requestFee);
    vm.stopBroadcast();

    // Step 2: Submit the actual request
    vm.startBroadcast(deployerPrivateKey);
    IFdcHub fdcHub = ContractRegistry.getFdcHub();
    console.log("fcdHub address:");
    console.log(address(fdcHub));
    fdcHub.requestAttestation{value: requestFee}(abiEncodedRequest);
    vm.stopBroadcast();

    // Step 3: Calculate voting round
    IFlareSystemsManager flareSystemsManager = ContractRegistry.getFlareSystemsManager();
    uint32 roundId = flareSystemsManager.getCurrentVotingEpochId();
    console.log("roundId: %s", Strings.toString(roundId));
}

Step-by-Step Breakdown:

  1. 1. Check Fee: Ask Flare “How much does this request cost?“
  2. 2. Submit Request: Pay the fee and send request to FdcHub contract
  3. 3. Get Round ID: Find out which voting period our request is in
  4. 4. Save Data: Store round ID for the next step

Flare's Contract Registry

Flare uses a “ContractRegistry“ system that acts like a universal remote control for all Flare services:

solidity
// These are like different "buttons" on your remote control:

ContractRegistry.getFdcHub()                    // "Submit request" button
ContractRegistry.getFdcRequestFeeConfigurations() // "Check price" button  
ContractRegistry.getFlareSystemsManager()        // "Check time" button
ContractRegistry.getRelay()                      // "Verify completion" button

Running the Submit Script

bash
forge script script/fdcExample/Web2Json.s.sol:SubmitAttestationRequest \
  --private-key $PRIVATE_KEY \
  --rpc-url $COSTON2_RPC_URL \
  --etherscan-api-key $FLARE_RPC_API_KEY \
  --broadcast \
  --ffi

What to Expect: You'll see the request fee, transaction hash, and voting round ID. The script saves the round ID to data/Web2Json_votingRoundId.txt.

Step 5: Wait for Verification & Get Proof

Simple Explanation: Flare's network needs time to verify the data. We wait for the voting round to complete, then retrieve the cryptographic proof.

Quality Control Analogy:
Waiting period: Factory quality control checks the product
Verification: Multiple validators confirm data is authentic
Proof generation: Create certificate of authenticity

Voting Round Timing

Round Duration

90 seconds per voting round

Finalization Time

1-3 minutes total wait

Check Progress

Use Flare Systems Explorer

Retrieving the Proof

After waiting, we ask the Data Availability (DA) Layer for the verified data and proof:

solidity
contract RetrieveDataAndProof is Script {
    using Surl for *;
    
    function run() external {
        // Load environment variables and saved data
        string memory daLayerUrl = vm.envString("COSTON2_DA_LAYER_URL");
        string memory apiKey = vm.envString("X_API_KEY");
        
        string memory requestBytes = vm.readLine("data/Web2Json_abiEncodedRequest.txt");
        string memory votingRoundId = vm.readLine("data/Web2Json_votingRoundId.txt");
        
        // Prepare the proof request
        string[] memory headers = Base.prepareHeaders(apiKey);
        string memory body = string.concat(
            '{"votingRoundId":', votingRoundId,
            ',"requestBytes":"', requestBytes, '"}'
        );
        
        // Send request to DA Layer
        string memory url = string.concat(daLayerUrl, "api/v1/fdc/proof-by-request-round-raw");
        (, bytes memory data) = Base.postAttestationRequest(url, headers, body);
        
        // Parse the response
        bytes memory dataJson = parseData(data);
        ParsableProof memory proof = abi.decode(dataJson, (ParsableProof));
        
        // Save proof for later use
        Base.writeToFile(
            dirPath,
            string.concat(attestationTypeName, "_proof"),
            StringsBase.toHexString(abi.encode(_proof)),
            true
        );
    }
}

What You Get Back:

json
{
  "attestationType": "0x576562324a736f6e00000000000...",
  "proofs": [
    "0x4305b2025b90e3316dfdad...",
    "0x2bdf5b10f6027a520f1e...",
    "0xff12aaa4fcb39d21c50...",
    "0xadd7610ccf4a2fcea7..."
  ],
  "responseHex": "0x00000000000000000..."
}

Running the Retrieve Script

bash
forge script script/fdcExample/Web2Json.s.sol:RetrieveDataAndProof \
  --private-key $PRIVATE_KEY \
  --rpc-url $COSTON2_RPC_URL \
  --etherscan-api-key $FLARE_RPC_API_KEY \
  --broadcast \
  --ffi

Step 6: Verify the Cryptographic Proof

Simple Explanation: This is where we check that the data we received is authentic and hasn't been tampered with.

Security Seal Analogy:
Merkle Proof: Like a tamper-evident security seal
On-chain verification: Checking the seal against official records
Gas optimization: Store only the proof, not the data

How FDC Verification Works

FDC uses a clever system to save gas costs while maintaining security:

solidity
// FDC's gas optimization strategy:
// 1. Store only Merkle proofs on-chain (cheap)
// 2. Keep actual data off-chain (expensive to store)
// 3. Verify proofs match on-chain records

function verifyWeb2Json(IWeb2Json.Proof memory _proof) internal view returns (bool) {
    // Format the proof for verification
    IWeb2Json.Proof memory formattedProof = IWeb2Json.Proof(_proof.proofs, _proof.data);
    
    // Ask Flare: "Is this proof valid?"
    bool isValid = ContractRegistry.getFdcVerification().verifyWeb2Json(formattedProof);
    
    console.log("proof is valid: %s", StringsBase.toString(isValid));
    return isValid;
}

Merkle Tree Explanation:

  • Like a family tree for data - each piece connects to prove authenticity
  • Root hash stored on-chain represents all the data
  • Merkle proof shows how your data connects to the root
  • Verification checks if the proof matches the on-chain root

Why This Matters

Cost Savings

Storing proofs is much cheaper than storing full data on-chain

Security

Anyone can verify data authenticity at any time

Step 7: Build Your Application Contract

Simple Explanation: Now we create a smart contract that actually uses the verified Star Wars data. This is our final application.

Collection Display Analogy:
Data structures: Like display cases and labels for your collectibles
Verification: Security system that only accepts authenticated items
Storage: Your actual collection display

Contract Structure

We'll create a Star Wars character collection that only accepts verified data:

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

// What we want to store permanently
struct StarWarsCharacter {
    string name;
    uint256 numberOfMovies;
    uint256 apiUid;
    uint256 bmi;  // Calculated from verified data!
}

// Temporary structure for data transport  
struct DataTransportObject {
    string name;
    uint256 height;
    uint256 mass; 
    uint256 numberOfMovies;
    uint256 apiUid;
}

// Our main collection contract
contract StarWarsCharacterList {
    mapping(uint256 => StarWarsCharacter) public characters;
    uint256[] public characterIds;

    // STEP 1: Verify the proof is authentic
    function isWeb2JsonProofValid(IWeb2Json.Proof calldata _proof) 
        private view returns (bool) 
    {
        return ContractRegistry.getFdcVerification().verifyWeb2Json(_proof);
    }

    // STEP 2: Add a new character with verification
    function addCharacter(IWeb2Json.Proof calldata data) public {
        // First, verify the proof is valid
        require(isWeb2JsonProofValid(data), "Invalid proof");

        // STEP 3: Unpack the verified data
        DataTransportObject memory dto = abi.decode(
            data.data.responseBody.abi_encoded_data,
            (DataTransportObject)
        );

        // STEP 4: Check for duplicates
        require(characters[dto.apiUid].apiUid == 0, "Character already exists");

        // STEP 5: Calculate BMI from verified height & mass
        uint256 bmi = (dto.mass * 100 * 100) / (dto.height * dto.height);

        // STEP 6: Create and store our character
        StarWarsCharacter memory character = StarWarsCharacter({
            name: dto.name,
            numberOfMovies: dto.numberOfMovies,
            apiUid: dto.apiUid,
            bmi: bmi  // Derived data from verified sources!
        });

        characters[dto.apiUid] = character;
        characterIds.push(dto.apiUid);
    }

    // STEP 7: View the entire collection
    function getAllCharacters() public view returns (StarWarsCharacter[] memory) {
        StarWarsCharacter[] memory result = new StarWarsCharacter[](characterIds.length);
        for (uint256 i = 0; i < characterIds.length; i++) {
            result[i] = characters[characterIds[i]];
        }
        return result;
    }
}

Real Data Transformation:

json
// What Flare verifies (DataTransportObject):
{
  "name": "R2-D2",
  "height": 96,        // 96 cm
  "mass": 32,          // 32 kg  
  "numberOfMovies": 7,
  "apiUid": 3
}

// What we store (StarWarsCharacter):
{
  "name": "R2-D2",
  "numberOfMovies": 7, 
  "apiUid": 3,
  "bmi": 3472          // (32 * 100 * 100) / (96 * 96) = 3472
}

Deploy the Contract

solidity
contract DeployContract is Script {
    function run() external {
        uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
        vm.startBroadcast(deployerPrivateKey);

        // Deploy our collection contract
        StarWarsCharacterList characterList = new StarWarsCharacterList();
        address _address = address(characterList);

        vm.stopBroadcast();

        // Save address for interaction
        Base.writeToFile(
            dirPath,
            string.concat(attestationTypeName, "_address"),
            StringsBase.toHexString(abi.encodePacked(_address)),
            true
        );
    }
}
bash
# Deploy the contract
forge script script/fdcExample/Web2Json.s.sol:DeployContract \
  --private-key $PRIVATE_KEY \
  --rpc-url $COSTON2_RPC_URL \
  --etherscan-api-key $FLARE_RPC_API_KEY \
  --broadcast \
  --verify \
  --ffi
terminal
Web2JSON terminal output

Live terminal

Step 8: Add R2-D2 to Your Collection

Simple Explanation: Final step! We take our verified R2-D2 data and add it to our on-chain collection.

Interaction Script

This script loads all our saved data and interacts with the deployed contract:

solidity
contract InteractWithContract is Script {
    function run() external {
        // Load saved data from files
        string memory addressString = vm.readLine("data/Web2Json_address.txt");
        address _address = vm.parseAddress(addressString);
        
        string memory proofString = vm.readLine("data/Web2Json_proof.txt");
        bytes memory proofBytes = vm.parseBytes(proofString);
        IWeb2Json.Proof memory proof = abi.decode(proofBytes, (IWeb2Json.Proof));
        
        // Connect to our deployed contract
        uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
        vm.startBroadcast(deployerPrivateKey);
        IStarWarsCharacterList characterList = IStarWarsCharacterList(_address);
        
        // Add R2-D2 to the collection!
        characterList.addCharacter(proof);
        vm.stopBroadcast();
    }
}
bash
# Add the verified character to your collection
forge script script/fdcExample/Web2Json.s.sol:InteractWithContract \
  --private-key $PRIVATE_KEY \
  --rpc-url $COSTON2_RPC_URL \
  --etherscan-api-key $FLARE_RPC_API_KEY \
  --broadcast \
  --ffi

Complete Workflow Summary:

  1. 1. Prepare: Create request for Star Wars API data
  2. 2. Encode: Convert text to blockchain-readable hex
  3. 3. Submit: Send request to Flare network with fee
  4. 4. Wait: Let Flare validators verify the data
  5. 5. Retrieve: Get cryptographic proof of verification
  6. 6. Verify: Check proof matches on-chain records
  7. 7. Deploy: Create your application contract
  8. 8. Use: Add verified data to your contract

Complete Foundry Workflow

Here's the complete sequence of commands to run the entire Web2JSON workflow with Foundry:

bash
# Step 1: Prepare the attestation request
forge script script/fdcExample/Web2Json.s.sol:PrepareAttestationRequest --private-key $PRIVATE_KEY --rpc-url $COSTON2_RPC_URL --broadcast --ffi

# Step 2: Submit to blockchain  
forge script script/fdcExample/Web2Json.s.sol:SubmitAttestationRequest --private-key $PRIVATE_KEY --rpc-url $COSTON2_RPC_URL --broadcast --ffi

# Wait 2-4 minutes for round finalization...

# Step 3: Retrieve the proof
forge script script/fdcExample/Web2Json.s.sol:RetrieveDataAndProof --private-key $PRIVATE_KEY --rpc-url $COSTON2_RPC_URL --broadcast --ffi

# Step 4: Deploy your contract
forge script script/fdcExample/Web2Json.s.sol:DeployContract --private-key $PRIVATE_KEY --rpc-url $COSTON2_RPC_URL --broadcast --verify --ffi

# Step 5: Use the verified data
forge script script/fdcExample/Web2Json.s.sol:InteractWithContract --private-key $PRIVATE_KEY --rpc-url $COSTON2_RPC_URL --broadcast --ffi

Congratulations! You've now mastered the complete Foundry Web2JSON workflow. You've learned how to bring real-world data to the blockchain trustlessly using Flare's FDC protocol with Foundry.