Skip to main content
Research Preview — Code snippets need testing against actual implementation.

Delivery Verification

Build an escrow contract that releases payment only when the delivery is confirmed at the correct location.
About location verification: This guide uses GPS coordinates as input. GPS is spoofable. We’re working on Location Proof plugins that will replace Geolocation.getCurrentPosition for stronger verification — these are still in development.

Concept

A trustless delivery system where:
  • Buyer locks payment in escrow
  • Delivery person proves they arrived at the destination
  • Escrow releases automatically on location verification

Step 1: The Escrow Contract

// 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 attestation 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 policy attestation
        (
            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, "Attestation 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;
    }
}

Step 2: Buyer Flow

This code runs on the buyer’s client (web app or mobile app) when they create a delivery order.
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 attestation
  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
  };
}

Step 3: Courier Flow

This code runs on the courier’s mobile app when they arrive at the delivery location.
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 attestation
  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 proof → triggers payment release
  const tx = await astral.compute.submit(proof.delegatedAttestation);
  await tx.wait();

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

Step 4: Mobile App 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'}
    />
  );
}

Enhanced Features

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
}

Back: Use Cases

See more use cases