Skip to main content
Research Preview — APIs may change. GitHub

Delivery verification

You’re building a delivery platform. Buyers lock payment in escrow. When the courier arrives at the delivery address, the escrow releases automatically. No manual confirmation, no disputes about whether the courier actually showed up.
About location verification: This guide uses GPS coordinates as input. GPS is spoofable. Astral is developing location proof plugins for stronger verification — these are still in development.

How it works

  1. Buyer creates a delivery order and locks payment in an escrow contract
  2. The delivery address is registered as a location record onchain
  3. When the courier arrives, their app checks proximity using compute.within
  4. Astral returns a signed result confirming the courier is within range
  5. The signed result is submitted onchain, triggering the escrow to release funds

The escrow contract

The resolver contract holds funds and releases them when it receives a valid signed result proving the courier arrived.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@eas/contracts/resolver/SchemaResolver.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

contract DeliveryEscrow is SchemaResolver, ReentrancyGuard {
    address public astralSigner;

    struct Delivery {
        address buyer;
        address courier;
        bytes32 destinationUID;  // Location record of delivery address
        uint256 amount;
        uint256 radius;          // Acceptable radius in meters
        uint256 deadline;
        bool completed;
        bool refunded;
    }

    mapping(bytes32 => Delivery) public deliveries;
    mapping(bytes32 => bool) public usedAttestations;

    event DeliveryCreated(bytes32 indexed deliveryId, address buyer, uint256 amount);
    event DeliveryCompleted(bytes32 indexed deliveryId, address courier);
    event DeliveryRefunded(bytes32 indexed deliveryId, address buyer);

    constructor(IEAS eas, address _astralSigner) SchemaResolver(eas) {
        astralSigner = _astralSigner;
    }

    // Buyer creates delivery escrow
    function createDelivery(
        bytes32 deliveryId,
        address courier,
        bytes32 destinationUID,
        uint256 radiusMeters,
        uint256 deadline
    ) external payable {
        require(msg.value > 0, "Must send payment");
        require(deliveries[deliveryId].buyer == address(0), "Already exists");
        require(deadline > block.timestamp, "Invalid deadline");

        deliveries[deliveryId] = Delivery({
            buyer: msg.sender,
            courier: courier,
            destinationUID: destinationUID,
            amount: msg.value,
            radius: radiusMeters * 100,  // Convert to cm
            deadline: deadline,
            completed: false,
            refunded: false
        });

        emit DeliveryCreated(deliveryId, msg.sender, msg.value);
    }

    function onAttest(
        Attestation calldata attestation,
        uint256 /*value*/
    ) internal override returns (bool) {
        require(attestation.attester == astralSigner, "Not from Astral");
        require(!usedAttestations[attestation.uid], "Already used");
        usedAttestations[attestation.uid] = true;

        // Decode the signed result
        (
            bool isWithinRadius,
            bytes32[] memory inputRefs,
            uint64 timestamp,
            string memory operation
        ) = abi.decode(
            attestation.data,
            (bool, bytes32[], uint64, string)
        );

        // Verify operation type
        require(
            keccak256(bytes(operation)) == keccak256(bytes("within")),
            "Wrong operation"
        );

        // Extract delivery ID from recipient field
        bytes32 deliveryId = bytes32(uint256(uint160(attestation.recipient)));
        Delivery storage delivery = deliveries[deliveryId];

        require(delivery.buyer != address(0), "Delivery not found");
        require(!delivery.completed, "Already completed");
        require(!delivery.refunded, "Already refunded");
        require(block.timestamp <= delivery.deadline, "Deadline passed");

        // Verify correct destination was checked
        require(inputRefs.length >= 2, "Invalid inputs");
        require(inputRefs[1] == delivery.destinationUID, "Wrong destination");

        // Verify courier is within radius
        require(isWithinRadius, "Not at delivery location");

        // Verify timestamp is recent
        require(timestamp > block.timestamp - 30 minutes, "Result too old");

        // Complete delivery
        delivery.completed = true;

        // Release funds to courier
        (bool success, ) = delivery.courier.call{value: delivery.amount}("");
        require(success, "Transfer failed");

        emit DeliveryCompleted(deliveryId, delivery.courier);
        return true;
    }

    // Buyer can refund after deadline
    function refund(bytes32 deliveryId) external nonReentrant {
        Delivery storage delivery = deliveries[deliveryId];

        require(msg.sender == delivery.buyer, "Not buyer");
        require(!delivery.completed, "Already completed");
        require(!delivery.refunded, "Already refunded");
        require(block.timestamp > delivery.deadline, "Deadline not passed");

        delivery.refunded = true;

        (bool success, ) = delivery.buyer.call{value: delivery.amount}("");
        require(success, "Refund failed");

        emit DeliveryRefunded(deliveryId, delivery.buyer);
    }

    function onRevoke(Attestation calldata, uint256) internal pure override returns (bool) {
        return false;
    }
}

Buyer flow

The buyer creates a delivery order by registering the destination onchain and locking payment.
import { AstralSDK } from '@decentralized-geo/astral-sdk';
import { ethers } from 'ethers';

async function createDeliveryOrder(
  destinationCoords: [number, number],
  courierAddress: string,
  paymentAmount: bigint,
  wallet: Signer
) {
  const astral = new AstralSDK({ chainId: 84532, signer: wallet });
  const escrow = new Contract(ESCROW_ADDRESS, ESCROW_ABI, wallet);

  // Create destination location record
  const destination = await astral.location.onchain.create({
    location: { type: 'Point', coordinates: destinationCoords },
    memo: "Delivery Address"
  });

  // Generate delivery ID
  const deliveryId = ethers.keccak256(ethers.AbiCoder.defaultAbiCoder().encode(
    ['address', 'address', 'uint256'],
    [await wallet.getAddress(), courierAddress, Date.now()]
  ));

  // Create escrow
  const tx = await escrow.createDelivery(
    deliveryId,
    courierAddress,
    destination.uid,
    100,  // 100 meter radius
    Math.floor(Date.now() / 1000) + 86400,  // 24 hour deadline
    { value: paymentAmount }
  );

  await tx.wait();

  return {
    deliveryId,
    destinationUID: destination.uid
  };
}

Courier flow

When the courier arrives, their app checks proximity and submits the signed result to release payment.
async function confirmDelivery(
  deliveryId: string,
  destinationUID: string,
  wallet: Signer
) {
  const astral = new AstralSDK({ chainId: 84532, signer: wallet });

  // Get current location
  const coords = await getCurrentLocation();

  // Create location record
  const courierLocation = await astral.location.onchain.create({
    location: { type: 'Point', coordinates: coords }
  });

  // Prove within radius of destination
  const proof = await astral.compute.within(
    courierLocation.uid,
    destinationUID,
    100,  // Must match contract radius
    {
      schema: SCHEMA_UID,
      recipient: deliveryId  // Pass delivery ID
    }
  );

  if (!proof.result) {
    throw new Error('Not close enough to delivery address');
  }

  // Submit signed result — triggers payment release
  const tx = await astral.compute.submit(proof.delegatedAttestation);
  await tx.wait();

  return { success: true, transactionHash: tx.hash };
}

Mobile integration

// React Native example
import Geolocation from '@react-native-community/geolocation';

function DeliveryConfirmButton({ deliveryId, destinationUID }) {
  const [status, setStatus] = useState<'idle' | 'checking' | 'confirming' | 'done'>('idle');

  const handleConfirm = async () => {
    setStatus('checking');

    Geolocation.getCurrentPosition(
      async (position) => {
        const coords: [number, number] = [
          position.coords.longitude,
          position.coords.latitude
        ];

        try {
          setStatus('confirming');
          await confirmDelivery(deliveryId, destinationUID, wallet);
          setStatus('done');
          Alert.alert('Success', 'Delivery confirmed! Payment released.');
        } catch (error) {
          setStatus('idle');
          Alert.alert('Error', error.message);
        }
      },
      (error) => {
        setStatus('idle');
        Alert.alert('Location Error', 'Could not get your location');
      },
      { enableHighAccuracy: true }
    );
  };

  return (
    <Button
      title={status === 'confirming' ? 'Confirming...' : 'Confirm Delivery'}
      onPress={handleConfirm}
      disabled={status !== 'idle'}
    />
  );
}

Extensions

Multi-signature confirmation

Require both courier arrival and buyer confirmation:
function confirmByBuyer(bytes32 deliveryId) external {
    require(msg.sender == deliveries[deliveryId].buyer);
    deliveries[deliveryId].buyerConfirmed = true;
}

// In onAttest, check both:
require(delivery.buyerConfirmed, "Buyer hasn't confirmed receipt");

Dispute resolution

Add an arbiter role for disputes:
address public arbiter;

function resolveDispute(bytes32 deliveryId, bool payToCourier) external {
    require(msg.sender == arbiter);
    // Handle dispute resolution
}

Next: Parametric insurance

Policies that trigger based on verified proximity