This document provides a detailed, step-by-step explanation of the Flare Data Connector (FDC) workflow, using the Star Wars API example.
We use data from an external API, send it to a server to encode it and prepare an attestation request, after that there will be a voting round with validators fetching and verifying the data that was submitted to the FDC. Once the data has been verified and the voting round finalizes, we can request the DA Layer to fetch the proof. The proof is then sent to a smart contract that will verify the cryptographic proof, perform a basic calculation using the data from the API and store a list of enhanced information on the flare blockchain.
The flow includes these steps:
- API Call (Star Wars API) ↓
- JQ Processing (Extract character data) ↓
- FDC Verification (Validators attest) ↓
- Proof Generation (Cryptographic proof) ↓
- Contract Deployment (StarWarsCharacterListV2) ↓
- Data Submission (addCharacter function) ↓
- On-chain Processing (BMI calculation) ↓
- Permanent Storage (Blockchain)
- Step 1: Attestation Request Preparation & ABI Encoding
- Step 2: Submit Attestation Request to FDC Hub
- Step 3: Wait for Voting Round & Retrieve Proof
- Step 4: Deploy Smart Contract
- Step 5: Use Proof with Smart Contract
- Complete Workflow Summary
- Technical Deep Dive
You define what API to call, how to process the data, and what format you want. The verifier server converts your human-readable request into ABI-encoded binary format.
File: scripts/fdcExample/Web2Json.ts (Lines 18-25)
const apiUrl = "https://swapi.info/api/people/3";
const postProcessJq = `{name: .name, height: .height, mass: .mass, numberOfFilms: .films | length, uid: (.url | split("/") | .[-1] | tonumber)}`;
const httpMethod = "GET";
const headers = "{}";
const queryParams = "{}";
const body = "{}";
const 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"}`;Parameter Breakdown:
apiUrl: The external API endpoint to fetch data frompostProcessJq: JQ filter to transform raw API responsehttpMethod: HTTP method (GET, POST, etc.)headers: HTTP headers (empty for this example)queryParams: URL query parameters (empty for this example)body: Request body (empty for GET requests)abiSignature: Defines the expected data structure for smart contracts
The JQ Filter:
{name: .name, height: .height, mass: .mass, numberOfFilms: .films | length, uid: (.url | split("/") | .[-1] | tonumber)}What Each Part Does:
name: .name→ Extract the "name" fieldheight: .height→ Extract the "height" fieldmass: .mass→ Extract the "mass" fieldnumberOfFilms: .films | length→ Count how many films the character appears inuid: (.url | split("/") | .[-1] | tonumber)→ Extract ID from URL and convert to number
Example Transformation:
// Raw API response from Star Wars API
{
"name": "R2-D2",
"height": "96",
"mass": "32",
"films": ["url1", "url2", "url3", "url4", "url5", "url6"],
"url": "https://swapi.info/api/people/3/"
}
// After JQ filter processing
{
"name": "R2-D2",
"height": "96",
"mass": "32",
"numberOfFilms": 6,
"uid": 3
}The ABI Signature:
{
"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 This Defines:
- The exact data structure your smart contract expects
- Data types for each field (string, uint256)
- Field names that will be used in your contract
File: scripts/fdcExample/Web2Json.ts (Lines 32-42)
async function prepareAttestationRequest(apiUrl: string, postProcessJq: string, abiSignature: string) {
const requestBody = {
url: apiUrl,
httpMethod: httpMethod,
headers: headers,
queryParams: queryParams,
body: body,
postProcessJq: postProcessJq,
abiSignature: abiSignature,
};
const url = `${verifierUrlBase}Web2Json/prepareRequest`;
const apiKey = VERIFIER_API_KEY_TESTNET;
return await prepareAttestationRequestBase(url, apiKey, attestationTypeBase, sourceIdBase, requestBody);
}What This Does:
- Assembles all parameters into a single request object
- Sets the endpoint URL for the verifier server
- Calls the base function that handles the HTTP request
File: scripts/utils/fdc.ts (Lines 25-50)
export async function prepareAttestationRequestBase(
url: string,
apiKey: string,
attestationTypeBase: string,
sourceIdBase: string,
requestBody: any
) {
console.log("Url:", url, "\n");
const attestationType = toUtf8HexString(attestationTypeBase); // "Web2Json" → hex
const sourceId = toUtf8HexString(sourceIdBase); // "PublicWeb2" → hex
const request = {
attestationType: attestationType,
sourceId: sourceId,
requestBody: requestBody,
};
console.log("Prepared request:\n", request, "\n");
const response = await fetch(url, {
method: "POST",
headers: {
"X-API-KEY": apiKey,
"Content-Type": "application/json",
},
body: JSON.stringify(request),
});
if (response.status != 200) {
throw new Error(`Response status is not OK, status ${response.status} ${response.statusText}\n`);
}
console.log("Response status is OK\n");
return await response.json();
}What the Verifier Server Does:
- Receives your human-readable request
- Converts attestation type and source ID to hex format
- Performs ABI encoding of the entire request
- Returns the ABI-encoded binary data
Real Output Example:
Url: https://web2json-verifier-test.flare.rocks/Web2Json/prepareRequest
Prepared request:
{
attestationType: '0x576562324a736f6e000000000000000000000000000000000000000000000000',
sourceId: '0x5075626c69635765623200000000000000000000000000000000000000000000',
requestBody: {
url: 'https://swapi.info/api/people/3',
httpMethod: 'GET',
headers: '{}',
queryParams: '{}',
body: '{}',
postProcessJq: '{name: .name, height: .height, mass: .mass, numberOfFilms: .films | length, uid: (.url | split("/") | .[-1] | tonumber)}',
abiSignature: '{"components": [...], "name": "task", "type": "tuple"}'
}
}
Response status is OK
Data: {
status: 'VALID',
abiEncodedRequest: '0x576562324a736f6e000000000000000000000000000000000000000000000000...'
}
- You define: What API to call, how to process the data, and what format you want
- You send: Human-readable request to verifier server
- Server converts: Your request into ABI-encoded binary format
- You receive: Binary data ready for blockchain submission
You submit the ABI-encoded request to the FDC Hub smart contract, pay a fee, and get a round ID for tracking the voting process.
File: scripts/fdcExample/Web2Json.ts (Lines 85-87)
const data = await prepareAttestationRequest(apiUrl, postProcessJq, abiSignature);
console.log("Data:", data, "\n");
const abiEncodedRequest = data.abiEncodedRequest;
const roundId = await submitAttestationRequest(abiEncodedRequest);File: scripts/utils/fdc.ts (Lines 72-85)
export async function submitAttestationRequest(abiEncodedRequest: string) {
const fdcHub = await getFdcHub(); // 1. Get FDC Hub contract
const requestFee = await getFdcRequestFee(abiEncodedRequest); // 2. Calculate fee
const transaction = await fdcHub.requestAttestation(abiEncodedRequest, {
value: requestFee, // 3. Submit with fee
});
console.log("Submitted request:", transaction.tx, "\n");
const roundId = await calculateRoundId(transaction); // 4. Calculate round ID
console.log(
`Check round progress at: https://${hre.network.name}-systems-explorer.flare.rocks/voting-round/${roundId}?tab=fdc\n`
);
return roundId;
}Step-by-Step Breakdown:
-
Get FDC Hub Contract:
const fdcHub = await getFdcHub();
- Retrieves the FDC Hub smart contract instance
- This is the central contract that manages all FDC requests
-
Calculate Request Fee:
const requestFee = await getFdcRequestFee(abiEncodedRequest);
- Determines how much FLARE tokens you need to pay
- Fee varies based on the complexity of your request
-
Submit Attestation Request:
const transaction = await fdcHub.requestAttestation(abiEncodedRequest, { value: requestFee, });
- Submits your ABI-encoded request to the FDC Hub contract
- Pays the calculated request fee in FLARE tokens
- Returns a blockchain transaction hash
File: scripts/utils/fdc.ts (Lines 55-71)
export async function calculateRoundId(transaction: any) {
const blockNumber = transaction.receipt.blockNumber;
const block = await ethers.provider.getBlock(blockNumber);
const blockTimestamp = BigInt(block.timestamp);
const flareSystemsManager: IFlareSystemsManagerInstance = await getFlareSystemsManager();
const firsVotingRoundStartTs = BigInt(await flareSystemsManager.firstVotingRoundStartTs());
const votingEpochDurationSeconds = BigInt(await flareSystemsManager.votingEpochDurationSeconds());
console.log("Block timestamp:", blockTimestamp, "\n");
console.log("First voting round start ts:", firsVotingRoundStartTs, "\n");
console.log("Voting epoch duration seconds:", votingEpochDurationSeconds, "\n");
const roundId = Number((blockTimestamp - firsVotingRoundStartTs) / votingEpochDurationSeconds);
console.log("Calculated round id:", roundId, "\n");
console.log("Received round id:", Number(await flareSystemsManager.getCurrentVotingEpochId()), "\n");
return roundId;
}Round ID Calculation:
roundId = (blockTimestamp - firstVotingRoundStartTs) / votingEpochDurationSeconds
Real Output Example:
Submitted request: 0x2a2754708bb1d2fad14a0aabad9211e017198f55b4b2b144aa35cf68abe3ab09
Block timestamp: 1754141799n
First voting round start ts: 1658430000n
Voting epoch duration seconds: 90n
Calculated round id: 1063464
Received round id: 1063464
Check round progress at: https://coston2-systems-explorer.flare.rocks/voting-round/1063464?tab=fdc
What the Round ID Means:
- Voting Round: A specific time period when validators will process your request
- Round ID 1063464: The 1,063,464th voting round since the system started
- Duration: Each round lasts 90 seconds
- Validators: During this round, all Flare validators will fetch data, apply JQ filters, and vote
- You have: ABI-encoded request from verifier server
- You pay: Request fee to FDC Hub contract
- You submit: Attestation request to blockchain
- You get: Round ID for tracking progress
- You can monitor: Progress at the systems explorer URL
You wait for the voting round to complete, then retrieve the cryptographic proof from the Data Availability Layer.
File: scripts/fdcExample/Web2Json.ts (Lines 90-90)
const proof = await retrieveDataAndProof(abiEncodedRequest, roundId);File: scripts/fdcExample/Web2Json.ts (Lines 44-48)
async function retrieveDataAndProof(abiEncodedRequest: string, roundId: number) {
const url = `${COSTON2_DA_LAYER_URL}api/v1/fdc/proof-by-request-round-raw`;
console.log("Url:", url, "n");
return await retrieveDataAndProofBaseWithRetry(url, abiEncodedRequest, roundId);
}File: scripts/utils/fdc.ts (Lines 95-105)
export async function retrieveDataAndProofBase(url: string, abiEncodedRequest: string, roundId: number) {
console.log("Waiting for the round to finalize...");
// We check every 10 seconds if the round is finalized
const relay: IRelayInstance = await getRelay();
const fdcVerification: IFdcVerificationInstance = await getFdcVerification();
const protocolId = await fdcVerification.fdcProtocolId();
while (!(await relay.isFinalized(protocolId, roundId))) {
await sleep(30000); // Wait 30 seconds
}
console.log("Round finalized!\n");What Happens During This Waiting Period:
-
Validators are working: During the 90-second voting round, all Flare validators:
- Fetch data from
https://swapi.info/api/people/3 - Apply your JQ filter:
{name: .name, height: .height, mass: .mass, numberOfFilms: .films | length, uid: (.url | split("/") | .[-1] | tonumber)} - Vote on the processed result
- Submit their attestations to the Data Availability Layer
- Fetch data from
-
Your script waits: Checks every 30 seconds if the round is finalized
-
Round finalizes: When enough validators have voted and agreed
File: scripts/utils/fdc.ts (Lines 107-112)
const request = {
votingRoundId: roundId,
requestBytes: abiEncodedRequest,
};
console.log("Prepared request:\n", request, "\n");
await sleep(10000); // Wait 10 seconds
let proof = await postRequestToDALayer(url, request, true);Real Output Example:
Url: https://ctn2-data-availability.flare.network/api/v1/fdc/proof-by-request-round-raw
Prepared request:
{
votingRoundId: 1063464,
requestBytes: '0x576562324a736f6e000000000000000000000000000000000000000000000000...'
}
File: scripts/utils/fdc.ts (Lines 114-119)
console.log("Waiting for the DA Layer to generate the proof...");
while (proof.response_hex == undefined) {
await sleep(10000); // Wait 10 seconds
proof = await postRequestToDALayer(url, request, false);
}
console.log("Proof generated!\n");What Happens:
- DA Layer processes: Takes all validator attestations and generates a cryptographic proof
- Your script polls: Checks every 10 seconds if the proof is ready
- Proof ready: When
proof.response_hexis defined
File: scripts/utils/fdc.ts (Lines 121-122)
console.log("Proof:", proof, "\n");
return proof;Real Output Example:
{
"response_hex": "0x0000000000000000000000000000000000000000000000000000000000000020...",
"attestation_type": "0x576562324a736f6e000000000000000000000000000000000000000000000000",
"proof": [
"0x95dde404dca2898a007e3a86b87b004ec427a9e9e0600612e10105a9078834f9",
"0xcbd4f113c167c63ce14c4c1f8134ff0626da56d9f3d7df22f45f8732c6cbe07d",
"0xbdcbf48251a8970a5a5503ed4f4fe53119686c5eda03070617c841de758c1cd4",
"0x424b5a2db3354e6e7a789021a482878909b72f63b95f64ecd97e21d463c253e0"
]
}What Each Part of the Proof Means:
response_hex: The processed data from the Star Wars API (after JQ filtering) in hex formatattestation_type: Identifies this as a Web2Json attestationproof: Array of Merkle proof elements for cryptographic verification
File: scripts/utils/fdc.ts (Lines 125-137)
export async function retrieveDataAndProofBaseWithRetry(
url: string,
abiEncodedRequest: string,
roundId: number,
attempts: number = 10
) {
for (let i = 0; i < attempts; i++) {
try {
return await retrieveDataAndProofBase(url, abiEncodedRequest, roundId);
} catch (e: any) {
console.log(e, "\n", "Remaining attempts:", attempts - i, "\n");
await sleep(20000);
}
}
throw new Error(`Failed to retrieve data and proofs after ${attempts} attempts`);
}What This Does:
- Tries up to 10 times to retrieve the proof
- Waits 20 seconds between attempts
- Handles network issues or temporary failures
- Wait for validators: 90-second voting round where validators fetch and process data
- Check finalization: Poll every 30 seconds until round is finalized
- Request proof: Send request to Data Availability Layer API
- Wait for proof: Poll every 10 seconds until proof is generated
- Get proof: Receive cryptographic proof with processed data
Total time from output: ~161 seconds (about 2.7 minutes)
You deploy the smart contract that will use the verified data to perform calculations and store results.
File: scripts/fdcExample/Web2Json.ts (Lines 92-92)
const characterList: StarWarsCharacterListV2Instance = await deployAndVerifyContract();File: scripts/fdcExample/Web2Json.ts (Lines 50-62)
async function deployAndVerifyContract() {
const args: any[] = [];
const characterList: StarWarsCharacterListV2Instance = await StarWarsCharacterListV2.new(...args);
try {
await run("verify:verify", {
address: characterList.address,
constructorArguments: args,
});
} catch (e: any) {
console.log(e);
}
console.log("StarWarsCharacterListV2 deployed to", characterList.address, "\n");
return characterList;
}What This Does:
- Deploy contract: Creates a new instance of
StarWarsCharacterListV2on the blockchain - Verify source code: Automatically verifies the contract on the block explorer
- Return instance: Returns the deployed contract instance
Real Output Example:
Successfully submitted source code for contract
contracts/fdcExample/Web2Json.sol:StarWarsCharacterListV2 at 0xE7f6ff7bD309621ae9e2339C829544E6C58bD8Ba
for verification on the block explorer. Waiting for verification result...
Successfully verified contract StarWarsCharacterListV2 on the block explorer.
https://coston2-explorer.flare.network/address/0xE7f6ff7bD309621ae9e2339C829544E6C58bD8Ba#code
StarWarsCharacterListV2 deployed to 0xE7f6ff7bD309621ae9e2339C829544E6C58bD8Ba
- Deploy contract: Creates new smart contract instance
- Verify source code: Automatically verifies on block explorer
- Get contract address: Returns the deployed contract address
- Ready for use: Contract is ready to receive verified data
You use the cryptographic proof with your smart contract to verify the data, perform calculations (like BMI), and store the results on the blockchain.
File: scripts/fdcExample/Web2Json.ts (Lines 94-94)
await interactWithContract(characterList, proof);File: scripts/fdcExample/Web2Json.ts (Lines 66-70)
async function interactWithContract(characterList: StarWarsCharacterListV2Instance, proof: any) {
console.log("Proof hex:", proof.response_hex, "\n");
// A piece of black magic that allows us to read the response type from an artifact
const IWeb2JsonVerification = await artifacts.require("IWeb2JsonVerification");
const responseType = IWeb2JsonVerification._json.abi[0].inputs[0].components[1];
console.log("Response type:", responseType, "\n");
const decodedResponse = web3.eth.abi.decodeParameter(responseType, proof.response_hex);
console.log("Decoded proof:", decodedResponse, "\n");What This Does:
- Takes the hex-encoded data from the proof
- Decodes it back into human-readable format
- Uses the ABI signature to understand the data structure
Real Output Example:
Decoded proof: [
'0x576562324a736f6e000000000000000000000000000000000000000000000000',
'0x5075626c69635765623200000000000000000000000000000000000000000000',
'1063464',
'0',
[
'https://swapi.info/api/people/3',
'GET',
'{}',
'{}',
'{}',
'{name: .name, height: .height, mass: .mass, numberOfFilms: .films | length, uid: (.url | split("/") | .[-1] | tonumber)}',
'{"components": [...], "name": "task", "type": "tuple"}',
url: 'https://swapi.info/api/people/3',
httpMethod: 'GET',
headers: '{}',
queryParams: '{}',
body: '{}',
postProcessJq: '{name: .name, height: .height, mass: .mass, numberOfFilms: .films | length, uid: (.url | split("/") | .[-1] | tonumber)}',
abiSignature: '{"components": [...], "name": "task", "type": "tuple"}'
],
[
'0x0000000000000000000000000000000000000000000000000000000000000020...',
abiEncodedData: '0x0000000000000000000000000000000000000000000000000000000000000020...'
]
]
File: scripts/fdcExample/Web2Json.ts (Lines 72-76)
const transaction = await characterList.addCharacter({
merkleProof: proof.proof,
data: decodedResponse,
});
console.log("Transaction:", transaction.tx, "\n");
console.log("Star Wars Characters:\n", await characterList.getAllCharacters(), "\n");What This Does:
- Calls the
addCharacterfunction on your smart contract - Passes the Merkle proof for verification
- Passes the decoded data for processing
Real Output Example:
Transaction: 0x2bb3b913bd7ffc8317386536b3ffce54ac7a67b0862fba07e7c32d5a4098d8eb
File: contracts/fdcExample/Web2Json.sol (Lines 25-45)
function addCharacter(IWeb2Json.Proof calldata data) public {
require(isJsonApiProofValid(data), "Invalid proof");
DataTransportObject memory dto = abi.decode(
data.data.responseBody.abiEncodedData,
(DataTransportObject)
);
require(characters[dto.apiUid].apiUid == 0, "Character already exists");
StarWarsCharacter memory character = StarWarsCharacter({
name: dto.name,
numberOfMovies: dto.numberOfMovies,
apiUid: dto.apiUid,
bmi: (dto.mass * 100 * 100) / (dto.height * dto.height)
});
characters[dto.apiUid] = character;
characterIds.push(dto.apiUid);
}What the Smart Contract Does:
- Verifies the proof: Calls
isJsonApiProofValid(data)which delegates to the FDC verification system - Decodes the data: Extracts the processed data from the proof
- Calculates BMI: Uses the formula
(mass * 100 * 100) / (height * height) - Stores the character: Saves the data on the blockchain
The BMI Formula:
bmi: (dto.mass * 100 * 100) / (dto.height * dto.height)Why This Formula:
- Height from Star Wars API is in centimeters (e.g., 96 cm)
- Mass from Star Wars API is in kilograms (e.g., 32 kg)
- Standard BMI expects height in meters (e.g., 0.96 m)
The Conversion:
BMI = (mass × 10,000) / height_in_cm²
Example Calculation for R2-D2:
- Height: 96 cm
- Mass: 32 kg
- BMI Calculation:
(32 × 10,000) / (96 × 96) = 320,000 / 9,216 = 34.72... ≈ 34
Real Output Example:
Star Wars Characters:
[
[
'R2-D2',
'6',
'3',
'34',
name: 'R2-D2',
numberOfMovies: '6',
apiUid: '3',
bmi: '34'
]
]
- Decode proof data: Convert hex data back to readable format
- Call smart contract: Pass proof and data to contract
- Verify proof: Smart contract verifies proof validity
- Calculate BMI: Smart contract performs calculations
- Store results: Save processed data on blockchain
- Display results: Show the final calculated data
- Step 1: Prepare attestation request → Get ABI-encoded request
- Step 2: Submit to FDC Hub → Get round ID
- Step 3: Wait for voting & retrieve proof → Get cryptographic proof
- Step 4: Deploy smart contract → Get contract instance
- Step 5: Use proof with smart contract → Calculate BMI & store data
- Fetched external data from Star Wars API
- Processed it with JQ filters to extract specific fields
- Verified it cryptographically through decentralized validators
- Used it in a smart contract to calculate BMI
- Stored the result on the blockchain
The final result: R2-D2's BMI is 34! 🤖📊
- Purpose: Converts human-readable requests to ABI-encoded binary format
- Endpoint:
https://web2json-verifier-test.flare.rocks/Web2Json/prepareRequest - Function: Acts as a "translator" between human specifications and blockchain format
- Purpose: Central contract that manages all FDC requests
- Function: Creates voting rounds and coordinates validator activities
- Fee System: Calculates and collects fees for attestation requests
- Purpose: Decentralized nodes that independently verify data
- Process: Fetch data, apply JQ filters, vote on results
- Consensus: Must agree on processed data for proof generation
- Purpose: Generates cryptographic proofs from validator attestations
- Function: Creates Merkle proofs for data verification
- API: Provides endpoints for proof retrieval
- Purpose: Verifies proof validity in smart contracts
- Function: Ensures data was properly attested by the network
- Integration: Called by smart contracts before using attested data
- Proof retrieval: Up to 10 attempts with 20-second delays
- Round finalization: Polls every 30 seconds
- Proof generation: Polls every 10 seconds
- Voting round: 90 seconds
- Proof generation: Variable (typically 10-30 seconds)
- Total execution: ~2-3 minutes
- Testnet tokens: Required for transaction fees
- API endpoints: Must be accessible
- Blockchain connectivity: Stable connection to Flare network
- Merkle proofs: Ensure data integrity
- Validator consensus: Decentralized verification
- Proof validation: Smart contract verification
- No central authority: Decentralized validators
- Independent verification: Multiple validators confirm data
- Cryptographic guarantees: Mathematical proof of data authenticity
The FDC workflow provides a robust, trustless way to integrate external data with blockchain applications. By following these 5 steps, you can:
- Define data requirements in human-readable format
- Submit requests to the decentralized network
- Wait for verification by independent validators
- Deploy smart contracts to use the verified data
- Perform calculations and store results on-chain
This system ensures that external data is verified, processed, and made available to smart contracts in a secure, decentralized manner, enabling complex applications that require real-world data integration.