INTEGRATING PHYSICAL VERIFICATION INTO YOUR RWA PLATFORM: A STEP-BY-STEP GUIDE WITH PTVS v1.0
How to add on-chain physical verification to tokenized real-world assets using PTVSClaimInjector.sol
If you're building an RWA tokenization platform, you've probably hit this wall:
Your smart contract knows who owns the token, but it doesn't know if the asset behind the token still exists.
MiCA Art. 36 requires continuous proof of reserve assets. Price oracles give you market value, but they can't verify physical integrity. You need physical verification — and it needs to be on-chain, deterministic, and legally admissible.
In this tutorial, I'll show you how to integrate the Prop Trust Verified Standard (PTVS v1.0) into your existing RWA platform using the open-source PTVSClaimInjector.sol smart contract.
What you'll build:
- Deploy PTVSClaimInjector on a testnet
- Generate a forensic inspection report (canonical JSON)
- Compute SHA-256 hash
- Inject a Verifiable Claim on-chain
- Query the claim from your dApp
- Integrate with ERC-3643/T-REX for compliance
Time required: approximately 1 hour
Prerequisites: Basic Solidity, familiarity with Hardhat or Foundry, MetaMask wallet
Let's build.
STEP 1: UNDERSTAND THE ARCHITECTURE
Before writing code, let's understand the data flow:
Physical Asset
|
v
PTCE Expert (on-site inspection)
|
v
PDF/A Report + QES Signature
|
v
Canonical JSON (deterministic serialization)
|
v
SHA-256 Hash (forensicHash)
|
v
PTVSClaimInjector.sol (on-chain)
|
v
Verifiable Claim stored
|
v
Circuit breaker logic triggered
|
v
ERC-3643/T-REX compliance enforced
Key concept: The forensicHash is the cryptographic link between the physical evidence (PDF/A report) and the on-chain claim. Any modification to the report changes the hash, making tampering detectable.
STEP 2: DEPLOY PTVSCLAIMINJECTOR
2.1 Get the Contract
PTVSClaimInjector is open-source (MIT License). Clone the repository:
git clone https://github.com/aurema-group/ptvs-sdk-python.git
cd ptvs-sdk-python
Or install via npm (for JavaScript/TypeScript projects):
npm install @aurema-group/ptvs-sdk
2.2 Contract Source Code
Here's the complete PTVSClaimInjector.sol contract:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/access/Ownable.sol";
contract PTVSClaimInjector is Ownable {
enum ClaimStatus { VERIFIED, CONDITIONAL, EXPIRED, REVOKED }
struct VerifiableClaim {
bytes32 assetId;
uint8 ptvsScore;
bytes32 forensicHash;
uint256 inspectionTimestamp;
address ptceAddress;
ClaimStatus status;
}
mapping(bytes32 => VerifiableClaim) public claims;
mapping(address => bool) public authorizedPTCEs;
event ClaimInjected(
bytes32 indexed assetId,
uint8 ptvsScore,
bytes32 forensicHash,
address indexed ptceAddress,
uint256 timestamp
);
event ClaimRenewed(
bytes32 indexed assetId,
uint8 newScore,
bytes32 newHash,
uint256 timestamp
);
event ClaimRevoked(
bytes32 indexed assetId,
string reason,
uint256 timestamp
);
event CircuitBreakerTriggered(
bytes32 indexed assetId,
uint8 score,
string action,
uint256 timestamp
);
modifier onlyAuthorizedPTCE() {
require(authorizedPTCEs[msg.sender], "Not authorized PTCE");
_;
}
constructor() Ownable(msg.sender) {}
function authorizePTCE(address ptce) external onlyOwner {
authorizedPTCEs[ptce] = true;
}
function revokePTCE(address ptce) external onlyOwner {
authorizedPTCEs[ptce] = false;
}
function injectClaim(
bytes32 assetId,
uint8 ptvsScore,
bytes32 forensicHash,
uint256 inspectionTimestamp,
bytes memory ptceSignature
) external onlyAuthorizedPTCE {
ClaimStatus status;
if (ptvsScore >= 70) {
status = ClaimStatus.VERIFIED;
} else if (ptvsScore >= 50) {
status = ClaimStatus.CONDITIONAL;
} else {
status = ClaimStatus.REVOKED;
}
claims[assetId] = VerifiableClaim({
assetId: assetId,
ptvsScore: ptvsScore,
forensicHash: forensicHash,
inspectionTimestamp: inspectionTimestamp,
ptceAddress: msg.sender,
status: status
});
emit ClaimInjected(
assetId,
ptvsScore,
forensicHash,
msg.sender,
block.timestamp
);
// Trigger circuit breaker if needed
if (status == ClaimStatus.REVOKED) {
emit CircuitBreakerTriggered(
assetId,
ptvsScore,
"Trading paused",
block.timestamp
);
}
}
function renewClaim(
bytes32 assetId,
uint8 newScore,
bytes32 newHash,
uint256 newTimestamp,
bytes memory ptceSignature
) external onlyAuthorizedPTCE {
require(claims[assetId].assetId != bytes32(0), "Claim does not exist");
ClaimStatus newStatus;
if (newScore >= 70) {
newStatus = ClaimStatus.VERIFIED;
} else if (newScore >= 50) {
newStatus = ClaimStatus.CONDITIONAL;
} else {
newStatus = ClaimStatus.REVOKED;
}
claims[assetId] = VerifiableClaim({
assetId: assetId,
ptvsScore: newScore,
forensicHash: newHash,
inspectionTimestamp: newTimestamp,
ptceAddress: msg.sender,
status: newStatus
});
emit ClaimRenewed(assetId, newScore, newHash, block.timestamp);
}
function revokeClaim(
bytes32 assetId,
string calldata reason
) external onlyOwner {
require(claims[assetId].assetId != bytes32(0), "Claim does not exist");
claims[assetId].status = ClaimStatus.REVOKED;
emit ClaimRevoked(assetId, reason, block.timestamp);
emit CircuitBreakerTriggered(
assetId,
claims[assetId].ptvsScore,
reason,
block.timestamp
);
}
function getClaim(bytes32 assetId)
external
view
returns (VerifiableClaim memory)
{
return claims[assetId];
}
function isClaimValid(bytes32 assetId) external view returns (bool) {
VerifiableClaim memory claim = claims[assetId];
return claim.status == ClaimStatus.VERIFIED;
}
}
2.3 Deploy to Testnet
Using Hardhat:
npx hardhat run scripts/deploy.js --network sepolia
Or using Foundry:
forge create src/PTVSClaimInjector.sol:PTVSClaimInjector --rpc-url $SEPOLIA_RPC_URL --private-key $PRIVATE_KEY
Save the deployed contract address — you'll need it for the next steps.
STEP 3: GENERATE CANONICAL JSON AND COMPUTE HASH
3.1 Create Inspection Data
First, create the inspection findings:
Python example:
import json
from Crypto.Hash import SHA256
inspection_data = {
"assetId": "0x1a2b3c4d5e6f7890abcdef1234567890abcdef1234567890abcdef12345678",
"assetName": "Edificio residencial · Calle Mayor 42, Valencia",
"assetType": "real_estate",
"inspectionDate": "2026-08-15T10:30:00Z",
"jurisdiction": "ES-VC",
"ptceId": "PTCE-0161",
"ptvsScore": 85,
"structural": {
"score": 85,
"cracks": 0,
"corrosion": 1,
"degradation": 0
},
"legal": {
"score": 92,
"encumbrances": 0,
"titleClear": True
},
"environmental": {
"score": 78,
"contamination": 0,
"hazards": 0
},
"documentation": {
"score": 95
},
"insurance": {
"score": 88,
"active": True
},
"notes": "Minor corrosion detected on balcony railings. Recommend treatment within 6 months."
}
3.2 Canonicalize JSON
Canonical JSON ensures deterministic serialization (sorted keys, no whitespace):
# Canonicalize: sort keys alphabetically, remove whitespace
canonical_json = json.dumps(
inspection_data,
sort_keys=True,
separators=(',', ':'),
ensure_ascii=False
)
print("Canonical JSON:")
print(canonical_json)
Output example:
{"assetId":"0x1a2b3c4d5e6f7890abcdef1234567890abcdef1234567890abcdef12345678","assetName":"Edificio residencial · Calle Mayor 42, Valencia","assetType":"real_estate",...}
3.3 Compute SHA-256 Hash
# Compute SHA-256 hash
hash_obj = SHA256.new(canonical_json.encode('utf-8'))
forensic_hash = '0x' + hash_obj.hexdigest()
print(f"Forensic Hash: {forensic_hash}")
# Output: 0xa3f2b8c9d4e5f6789012345678901234567890abcdef1234567890abcdef12345678
This hash is what you'll inject on-chain. It's the cryptographic link between your physical evidence and the on-chain claim.
STEP 4: INJECT CLAIM ON-CHAIN
4.1 Python Integration (Web3.py)
from web3 import Web3
# Connect to testnet
w3 = Web3(Web3.HTTPProvider('https://sepolia.infura.io/v3/YOUR_PROJECT_ID'))
# Contract ABI (simplified)
abi = [
{
"inputs": [
{"name": "assetId", "type": "bytes32"},
{"name": "ptvsScore", "type": "uint8"},
{"name": "forensicHash", "type": "bytes32"},
{"name": "inspectionTimestamp", "type": "uint256"},
{"name": "ptceSignature", "type": "bytes"}
],
"name": "injectClaim",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [{"name": "assetId", "type": "bytes32"}],
"name": "getClaim",
"outputs": [
{
"components": [
{"name": "assetId", "type": "bytes32"},
{"name": "ptvsScore", "type": "uint8"},
{"name": "forensicHash", "type": "bytes32"},
{"name": "inspectionTimestamp", "type": "uint256"},
{"name": "ptceAddress", "type": "address"},
{"name": "status", "type": "uint8"}
],
"name": "",
"type": "tuple"
}
],
"stateMutability": "view",
"type": "function"
}
]
# Load contract
contract_address = '0xYOUR_DEPLOYED_CONTRACT_ADDRESS'
contract = w3.eth.contract(address=contract_address, abi=abi)
# PTCE account (must be authorized)
ptce_account = w3.eth.account.from_key('YOUR_PTCE_PRIVATE_KEY')
# Prepare transaction
asset_id = bytes.fromhex(inspection_data['assetId'][2:]) # Remove '0x'
ptvs_score = inspection_data['ptvsScore']
forensic_hash = bytes.fromhex(forensic_hash[2:]) # Remove '0x'
inspection_timestamp = int(datetime.now().timestamp())
ptce_signature = b'' # In production, this would be the QES signature
# Build transaction
nonce = w3.eth.get_transaction_count(ptce_account.address)
tx = contract.functions.injectClaim(
asset_id,
ptvs_score,
forensic_hash,
inspection_timestamp,
ptce_signature
).build_transaction({
'from': ptce_account.address,
'nonce': nonce,
'gas': 200000,
'gasPrice': w3.eth.gas_price
})
# Sign and send
signed_tx = ptce_account.sign_transaction(tx)
tx_hash = w3.eth.send_raw_transaction(signed_tx.raw_transaction)
print(f"Transaction sent: {tx_hash.hex()}")
# Wait for confirmation
receipt = w3.eth.wait_for_transaction_receipt(tx_hash)
print(f"Transaction confirmed in block {receipt.blockNumber}")
4.2 JavaScript Integration (Web3.js)
const Web3 = require('web3');
const crypto = require('crypto');
// Connect to testnet
const web3 = new Web3('https://sepolia.infura.io/v3/YOUR_PROJECT_ID');
// Contract instance
const contractAddress = '0xYOUR_DEPLOYED_CONTRACT_ADDRESS';
const contract = new web3.eth.Contract(ABI, contractAddress);
// PTCE account
const ptceAccount = web3.eth.accounts.privateKeyToAccount('YOUR_PTCE_PRIVATE_KEY');
web3.eth.accounts.wallet.add(ptceAccount);
// Inspection data
const inspectionData = {
assetId: '0x1a2b3c4d5e6f7890abcdef1234567890abcdef1234567890abcdef12345678',
ptvsScore: 85,
// ... rest of inspection data
};
// Compute forensic hash (same as Python example)
const canonicalJson = JSON.stringify(inspectionData, Object.keys(inspectionData).sort());
const forensicHash = '0x' + crypto.createHash('sha256').update(canonicalJson).digest('hex');
// Inject claim
async function injectClaim() {
const tx = await contract.methods.injectClaim(
inspectionData.assetId,
inspectionData.ptvsScore,
forensicHash,
Math.floor(Date.now() / 1000),
'0x' // ptceSignature
).send({
from: ptceAccount.address,
gas: 200000
});
console.log('Transaction hash:', tx.transactionHash);
return tx;
}
injectClaim().catch(console.error);
STEP 5: QUERY AND VERIFY CLAIMS
5.1 Read Claim from Contract
# Query claim
claim = contract.functions.getClaim(asset_id).call()
print(f"Asset ID: 0x{claim[0].hex()}")
print(f"PTVS Score: {claim[1]}")
print(f"Forensic Hash: 0x{claim[2].hex()}")
print(f"Inspection Timestamp: {claim[3]}")
print(f"PTCE Address: {claim[4]}")
print(f"Status: {['VERIFIED', 'CONDITIONAL', 'EXPIRED', 'REVOKED'][claim[5]]}")
5.2 Verify Claim Independently
Any third party can verify the claim by recomputing the hash:
def verify_claim(claim, original_json):
"""Verify that on-chain claim matches original inspection report."""
# Recompute hash from original JSON
recomputed_hash = '0x' + SHA256.new(original_json.encode('utf-8')).hexdigest()
# Compare with on-chain hash
on_chain_hash = '0x' + claim[2].hex()
if recomputed_hash == on_chain_hash:
print("Verification successful: hashes match")
return True
else:
print("Verification failed: hashes do not match")
print(f" Expected: {on_chain_hash}")
print(f" Got: {recomputed_hash}")
return False
# Verify the claim
verify_claim(claim, canonical_json)
STEP 6: INTEGRATE WITH ERC-3643/T-REX
If you're using ERC-3643/T-REX for identity and compliance, here's how to integrate PTVS:
6.1 Architecture
ERC-3643/T-REX
├─ Identity Registry (ONCHAINID)
├─ Compliance Module
└─ Token Contract
↕
PTVSClaimInjector (PTVS v1.0)
├─ Physical verification claims
├─ PTVS Score enforcement
└─ Circuit breaker actions
6.2 Compliance Integration Example
// In your token contract
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "./PTVSClaimInjector.sol";
contract RWA_Token is ERC20 {
PTVSClaimInjector public ptvsInjector;
bytes32 public assetId;
constructor(
address _ptvsInjector,
bytes32 _assetId
) ERC20("Real World Asset Token", "RWAT") {
ptvsInjector = PTVSClaimInjector(_ptvsInjector);
assetId = _assetId;
}
function transfer(address to, uint256 amount) public override returns (bool) {
// Check PTVS claim before allowing transfer
require(
ptvsInjector.isClaimValid(assetId),
"Transfer blocked: asset verification expired or revoked"
);
return super.transfer(to, amount);
}
}
This ensures that tokens cannot be transferred if the physical asset verification has expired or been revoked.
STEP 7: BUILD A FRONTEND VERIFICATION WIDGET
7.1 HTML + JavaScript Widget
<!DOCTYPE html>
<html>
<head>
<title>PTVS Verification Widget</title>
<script src="https://cdn.jsdelivr.net/npm/web3@latest/dist/web3.min.js"></script>
</head>
<body>
<h1>Asset Verification</h1>
<input type="text" id="assetId" placeholder="Enter Asset ID (0x...)">
<button onclick="verifyAsset()">Verify</button>
<div id="result"></div>
<script>
const web3 = new Web3('https://sepolia.infura.io/v3/YOUR_PROJECT_ID');
const contractAddress = '0xYOUR_DEPLOYED_CONTRACT_ADDRESS';
const contractABI = [/* ABI here */];
const contract = new web3.eth.Contract(contractABI, contractAddress);
async function verifyAsset() {
const assetId = document.getElementById('assetId').value;
const claim = await contract.methods.getClaim(assetId).call();
const statusMap = ['VERIFIED', 'CONDITIONAL', 'EXPIRED', 'REVOKED'];
const status = statusMap[claim.status];
let html = `
<h2>Verification Result</h2>
<p><strong>Asset ID:</strong> ${claim.assetId}</p>
<p><strong>PTVS Score:</strong> ${claim.ptvsScore}/100</p>
<p><strong>Status:</strong> ${status}</p>
<p><strong>Inspection Date:</strong> ${new Date(claim.inspectionTimestamp * 1000).toLocaleString()}</p>
<p><strong>PTCE Address:</strong> ${claim.ptceAddress}</p>
`;
if (status === 'VERIFIED') {
html += '<p style="color: green;">Asset is verified and eligible for trading</p>';
} else if (status === 'CONDITIONAL') {
html += '<p style="color: orange;">Asset is conditional - increased risk</p>';
} else {
html += '<p style="color: red;">Asset verification revoked - trading paused</p>';
}
document.getElementById('result').innerHTML = html;
}
</script>
</body>
</html>
STEP 8: TEST THE FULL FLOW
8.1 Try the Sandbox First
Before deploying to production, test the complete flow in our public sandbox:
https://proptrustverified.com/sandbox/
The sandbox lets you:
- Define a mock asset
- Run a forensic inspection
- Compute SHA-256 hash
- Simulate on-chain claim injection
- Verify the claim
No backend required. All computations happen in your browser.
8.2 Production Checklist
Before going to mainnet:
- Security audit of your integration code
- Deploy PTVSClaimInjector to mainnet (or L2 like Arbitrum/Polygon for lower gas)
- Authorize your PTCE addresses
- Test circuit breaker logic with mock degraded assets
- Integrate with your existing compliance workflow
- Add monitoring for claim expiration (90-day re-audit cycle)
- Document the verification process for your users
REAL-WORLD EXAMPLE: PROYECTO NAVARRÉS
To see this in action with a real asset, check out Proyecto Navarrés:
- Asset: 108,000 m² urbanizable land in Valencia, Spain
- PTVS Score: 91/100 (VERIFIED)
- Status: Live on-chain with Verifiable Claim
- Case study: https://forensics-oracle.org/case-studies/navarres/
This demonstrates that PTVS v1.0 is not theoretical — it's been executed against real assets.
RESOURCES
- Reference Architecture: https://doi.org/10.5281/zenodo.21719175
- GitHub: https://github.com/aurema-group/ptvs-sdk-python
- Sandbox: https://proptrustverified.com/sandbox/
- Compatibility Program: https://forensics-oracle.org/compatibility/
QUESTIONS?
If you run into issues integrating PTVS, drop a comment below or reach out:
- Technical support: enterprise@proptrustverified.com
- Discord: https://discord.gg/forensics-oracle
- GitHub Issues: https://github.com/aurema-group/ptvs-sdk-python/issues
Happy building!
Aurelio Tamarit Blay is the Lead Researcher of the Forensics Oracle Initiative and creator of the Prop Trust Verified Standard (PTVS v1.0). He is a sworn judicial expert (Exp. No. 0161, Spain) with 33 years of forensic practice. ORCID: 0009-0007-5824-3602.
Tags: blockchain smartcontracts rwa solidity
Top comments (0)