Skip to main content
Research Preview — Code snippets demonstrate the intended API and need testing against the actual implementation.

Build a Location-Gated Smart Contract

Create a smart contract that triggers actions when a geospatial policy check passes — “is the user within 500m of the Eiffel Tower?”
About location verification: This guide uses location data as input, but doesn’t verify where that data came from. GPS is spoofable. We’re working on the first Location Proof plugins to provide evidence-backed location claims — these are still in development and will integrate with the flow shown here.

What You’ll Learn

1

Register a reference location

Create a location attestation for the Eiffel Tower
2

Check the geospatial policy

Use Astral to verify proximity
3

Gate the smart contract

Use EAS resolvers to trigger onchain actions based on the policy result

Prerequisites

npm install @decentralized-geo/astral-sdk ethers
You’ll need:
  • A wallet with testnet ETH (Base Sepolia)
  • Basic TypeScript and Solidity knowledge

Step 1: Initialize the SDK

import { AstralSDK } from '@decentralized-geo/astral-sdk';
import { ethers } from 'ethers';

// Connect your wallet
const provider = new ethers.JsonRpcProvider('https://sepolia.base.org');
const wallet = new ethers.Wallet(process.env.PRIVATE_KEY, provider);

// Initialize SDK
const astral = new AstralSDK({
  chainId: 84532,  // Base Sepolia
  signer: wallet,
  apiUrl: 'https://api.astral.global'
});

Step 2: Register the Eiffel Tower

Create a location attestation for the Eiffel Tower that can be referenced by UID.
// Define the Eiffel Tower as a point
const eiffelTowerLocation = {
  type: 'Point',
  coordinates: [2.2945, 48.8584]  // [longitude, latitude]
};

// Create an onchain location attestation
const eiffelTower = await astral.location.onchain.create({
  location: eiffelTowerLocation,
  memo: 'Eiffel Tower - Iconic Paris landmark'
});

console.log('Eiffel Tower UID:', eiffelTower.uid);
console.log('Transaction hash:', eiffelTower.txHash);
// Save this UID - it's a reference to this location
Location attestations follow the Location Protocol v0.2 schema (verification in progress — see Location Records). Anyone can now reference the Eiffel Tower by UID instead of hardcoding coordinates.

Step 3: Check the Geospatial Policy

Prerequisite: Before running this step, you need to deploy your resolver contract and register your schema with EAS. See the Prerequisite section below — the RESOLVER_SCHEMA_UID from that step is required here.
When the user is ready to claim, submit their location to Astral for policy evaluation.
Location source matters: The userCoords below comes from GPS, which is spoofable. We’re working on Location Proof plugins to provide evidence-backed location claims — these are still in development.
// User's location (from GPS or location proof plugin)
const userCoords = {
  type: 'Point',
  coordinates: [2.2951, 48.8580]
};

// Create user's location attestation
const userLocation = await astral.location.onchain.create({
  location: userCoords
});

// Check proximity using compute module
const result = await astral.compute.within(
  userLocation.uid,      // User's location
  eiffelTower.uid,       // Target landmark
  500,                   // Radius in meters
  {
    schema: RESOLVER_SCHEMA_UID,
    recipient: wallet.address
  }
);

// Behind the scenes:
// 1. Astral computes ST_DWithin in PostGIS (inside TEE)
// 2. Signs a policy attestation with the result
// 3. Returns delegated attestation for you to submit

console.log('Within 500m:', result.result);  // true or false
console.log('Input references:', result.inputRefs);

// Submit onchain if policy passed
if (result.result) {
  const submission = await astral.compute.submit(result.delegatedAttestation);
  console.log('Policy attestation UID:', submission.uid);
}
The service runs inside EigenCompute’s TEE, providing verifiable execution. The signed attestation proves the computation was performed correctly.
For UX feedback: Use Turf.js for instant client-side distance calculations before submitting to Astral for verified computation.

Step 4: The Smart Contract (EAS Resolver)

So far, we’ve performed a geospatial policy check in a verifiable computing environment. How do we connect this to smart contracts? EAS Resolvers are smart contracts that receive callbacks when attestations are submitted to the EAS contract. When you submit a Policy Attestation to EAS with a schema that has a resolver attached, EAS automatically calls your resolver’s onAttest function with the attestation data. This is where you implement your business logic. Here’s an example resolver that gates an action based on the policy result:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@eas/contracts/resolver/SchemaResolver.sol";
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";

contract EiffelTowerNFT is SchemaResolver, ERC721 {
    address public astralSigner;
    uint256 public nextTokenId = 1;
    mapping(address => bool) public hasMinted;

    constructor(IEAS eas, address _astralSigner)
        SchemaResolver(eas)
        ERC721("Eiffel Tower Visitor", "EIFFEL")
    {
        astralSigner = _astralSigner;
    }

    function onAttest(
        Attestation calldata attestation,
        uint256 /*value*/
    ) internal override returns (bool) {
        // 1. Verify attestation is from Astral
        require(attestation.attester == astralSigner, "Not from Astral");

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

        // 3. Verify policy passed
        require(policyPassed, "Not close enough");

        // 4. One mint per address
        require(!hasMinted[attestation.recipient], "Already minted");

        // 5. Mint NFT atomically
        hasMinted[attestation.recipient] = true;
        _mint(attestation.recipient, nextTokenId++);

        return true;
    }

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

Prerequisite: Deploy Resolver and Register Schema

You must complete this step before running Step 3. The RESOLVER_SCHEMA_UID is required for the policy check to trigger your resolver.
import { ethers } from 'ethers';
import { SchemaRegistry } from '@ethereum-attestation-service/eas-sdk';

// Deploy resolver
const EiffelNFT = await ethers.getContractFactory("EiffelTowerNFT");
const resolver = await EiffelNFT.deploy(
  EAS_ADDRESS,
  ASTRAL_SIGNER_ADDRESS  // See security notes for signer management
);
await resolver.waitForDeployment();

// Register schema with EAS
const schemaRegistry = new SchemaRegistry(SCHEMA_REGISTRY_ADDRESS);
schemaRegistry.connect(wallet);

const schema = "bool result,bytes32[] inputRefs,uint64 timestamp,string operation";

const tx = await schemaRegistry.register({
  schema,
  resolverAddress: await resolver.getAddress(),
  revocable: false
});
const receipt = await tx.wait();

const RESOLVER_SCHEMA_UID = receipt.logs[0].args.uid;
console.log('Schema UID:', RESOLVER_SCHEMA_UID);
// Use this UID in Step 3 when calling astral.compute.within()

The Complete Flow

import { AstralSDK } from '@decentralized-geo/astral-sdk';
import { ethers } from 'ethers';

// Setup
const provider = new ethers.JsonRpcProvider('https://sepolia.base.org');
const wallet = new ethers.Wallet(process.env.PRIVATE_KEY, provider);

const astral = new AstralSDK({
  chainId: 84532,
  signer: wallet,
  apiUrl: 'https://api.astral.global'
});

// 0. Deploy resolver and register schema (see Prerequisite section)
const RESOLVER_SCHEMA_UID = '0x...';  // From schema registration

// 1. Reference location exists (or create it)
const EIFFEL_TOWER_UID = '0x...';  // Pre-registered landmark

// 2. User arrives — create location attestation
const userLocation = await astral.location.onchain.create({
  location: { type: 'Point', coordinates: [2.2951, 48.8580] }
});

// 3. Check policy
const result = await astral.compute.within(
  userLocation.uid,
  EIFFEL_TOWER_UID,
  500,  // 500 meters
  { schema: RESOLVER_SCHEMA_UID, recipient: wallet.address }
);

console.log('Policy check result:', result.result);

// 4. Submit to trigger resolver and execute onchain action
if (result.result) {
  const submission = await astral.compute.submit(result.delegatedAttestation);
  console.log('Action triggered! Attestation UID:', submission.uid);
}

Alternative: Offchain Location Attestations

If you don’t need onchain location records, use offchain attestations to save gas:
// Create offchain attestation (no gas)
const userLocation = await astral.location.offchain.create({
  location: { type: 'Point', coordinates: [2.2951, 48.8580] }
});

// Use the offchain attestation UID + URI
const result = await astral.compute.within(
  { uid: userLocation.uid, uri: 'ipfs://...' },  // Offchain reference
  EIFFEL_TOWER_UID,
  500,
  { schema: RESOLVER_SCHEMA_UID, recipient: wallet.address }
);

Next Steps