Skip to main content

Offchain Service Integration

For this walkthrough, we’ll use a web application as an example, though most other services require only a subset of these steps.
1

Backend Integration

Fetching Predicate Attestations requires an API key that must be secured. Using a passthrough proxy is best practice!
2

Frontend Integration

Embed the Predicate Attestation into the transaction object, allowing users to self-authorize onchain.

Mental model

  • With specific transaction information, your backend requests the PredicateAPI for an attestation.
  • The PredicateAPI returns a message detailing if the request is compliant and a corresponding attestation.
  • Your frontend includes that message when calling a predicated contract function.

Backend Integration

Since you’ll be serving a UI to end users, we recommend storing the API key and making Predicate API calls on your backend service:
Example Backend Proxy
import {PredicateClient, PredicateRequest, packFunctionArgs, signaturesToBytes} from '@predicate/core';

const predicateClient = new PredicateClient({
    apiUrl: 'https://api.predicate.io',
    apiKey: process.env.PREDICATE_API_KEY!
});

// Create individual route that returns predicate attestation
app.post('/api/predicate/evaluate', async (req, res) => {
    const { from, to, functionArgs, chainId } = req.body;
    
    try {
        // Encode the private function that is invoked by the predicated function
        const data = packFunctionArgs("sendCoin(address,uint256)", functionArgs);
        
        const request: PredicateRequest = {
            from: from,
            to: to,
            data: data,
            msg_value: '0',
            chain_id: chainId
        };

        const evaluationResult = await predicateClient.evaluatePolicy(request);
        
        if (!evaluationResult.is_compliant) {
            return res.status(400).json({ 
                error: 'Transaction not compliant',
                results: evaluationResult.results
            });
        }

        const predicateMessage = signaturesToBytes(evaluationResult);
        res.json({ predicateMessage });
        
    } catch (error) {
        res.status(500).json({ error: 'Failed to evaluate policy' });
    }
});

Frontend Integration

Use our SDK to nest the attestation details into the transaction object before the user is prompted to sign:
Frontend Implementation Snippet
// Fetch attestation from your backend
const response = await fetch('/api/predicate/evaluate', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
        from: userAddress,
        to: contractAddress,
        functionArgs: [receiverAddress, amount]
    })
});

const { predicateMessage } = await response.json();

// Submit transaction with predicate message
const tx = await contract.sendCoin(
    receiverAddress,
    amount,
    [
        predicateMessage.taskId,
        predicateMessage.expireByBlockNumber,
        predicateMessage.signerAddresses,
        predicateMessage.signatures
    ]
);

Understanding Predicate Functions

In both integration patterns, you’ll have a Predicated Function on your smart contract - this is the function that requires a ‘Predicate Message’ Attestation in order to be invoked. You can see this commented in the Complete Integration Example
Complete Integration Example
import {PredicateClient, PredicateRequest, packFunctionArgs, signaturesToBytes} from '@predicate/core';
import { ethers } from 'ethers';

const predicateClient = new PredicateClient({
    apiUrl: 'https://api.predicate.io',
    apiKey: process.env.PREDICATE_API_KEY!
});

const contractABI = [
    "function sendCoin(address to, uint256 amount, tuple(string, uint256, address[], bytes[]) predicateMessage)"
];

const provider = new ethers.JsonRpcProvider(process.env.RPC);
const wallet = new ethers.Wallet(process.env.PRIVATE_KEY || "", provider);
const contract = new ethers.Contract("YOUR_CONTRACT_ADDRESS", contractABI, wallet);

async function sendCompliantTransaction() {
    const functionArgs = [receiverAddress, amount];
    const contractAddress = await contract.getAddress();

    // Encode the private function that is invoked by the predicated function
    const data = packFunctionArgs("sendCoin(address,uint256)", functionArgs);
    
    const request: PredicateRequest = {
        from: wallet.address,
        to: contractAddress,
        data: data,
        msg_value: '0'
    };

    const evaluationResult = await predicateClient.evaluatePolicy(request);
    
    if (!evaluationResult.is_compliant) {
        console.error("Transaction not compliant");
        return;
    }

    const predicateMessage = signaturesToBytes(evaluationResult);

    const tx = await contract.sendCoin(
        functionArgs[0],
        functionArgs[1],
        [
            predicateMessage.taskId,
            predicateMessage.expireByBlockNumber,
            predicateMessage.signerAddresses,
            predicateMessage.signatures
        ]
    );

    const receipt = await tx.wait();
    console.log("Transaction successful:", receipt);
}

Next Steps

Contact the Predicate team for production API keys and higher rate limits.
I