Create an NFT collection where minting requires passing a geospatial policy check — verifying the user is within range of a target location.
About location verification: This guide uses GPS coordinates as input. GPS is spoofable. We’re working on Location Proof plugins that will replace navigator.geolocation for stronger verification — these are still in development.
import { SchemaRegistry } from '@ethereum-attestation-service/eas-sdk';const schemaRegistry = new SchemaRegistry(SCHEMA_REGISTRY_ADDRESS);// Boolean policy schema for 'within' operationconst schema = "bool result,bytes32[] inputRefs,uint64 timestamp,string operation";const tx = await schemaRegistry.connect(signer).register({ schema, resolverAddress: nftContract.address, revocable: true // IMPORTANT: Must be true - Astral signs with revocable=true});const receipt = await tx.wait();const SCHEMA_UID = receipt.logs[0].args.uid;console.log('Schema UID:', SCHEMA_UID);
Schema must use revocable: true — Astral signs delegated attestations with revocable: true. If your schema is registered with revocable: false, EAS will reject the attestation with an Irrevocable() or InvalidSignature() error. See #9 for discussion on this behavior.
Location source: The navigator.geolocation API provides GPS coordinates which are spoofable. In production, replace with Location Proof plugins as they become available.
Copy
import { AstralSDK } from '@decentralized-geo/astral-sdk';import * as turf from '@turf/turf';const LANDMARK_UID = '0x...'; // Your reference locationconst SCHEMA_UID = '0x...'; // Your schemaconst RADIUS_METERS = 500;async function checkEligibility(userCoords: [number, number]) { const astral = new AstralSDK({ chainId: 84532 }); // Quick local check first (UX) const landmark = await astral.location.get(LANDMARK_UID); const distance = turf.distance( turf.point(userCoords), turf.point(landmark.geometry.coordinates), { units: 'meters' } ); if (distance > RADIUS_METERS) { return { eligible: false, message: `You're ${Math.round(distance)}m away. Get within ${RADIUS_METERS}m to mint!` }; } return { eligible: true, message: `You're close enough! Ready to mint.`, distance };}async function mintNFT(userCoords: [number, number], wallet: Signer) { const astral = new AstralSDK({ chainId: 84532, signer: wallet }); // Create user's location attestation const userLocation = await astral.location.create({ type: 'Point', coordinates: userCoords }, { submitOnchain: true }); // Compute proximity and submit attestation const result = await astral.compute.within( userLocation.uid, LANDMARK_UID, RADIUS_METERS, { schema: SCHEMA_UID, recipient: await wallet.getAddress() } ); if (!result.result) { throw new Error('Location check failed - not close enough'); } // Submit to EAS (triggers resolver → mints NFT) const tx = await astral.eas.submitDelegated(result.delegatedAttestation); const receipt = await tx.wait(); return { transactionHash: tx.hash, attestationUID: result.attestation.uid };}
The inputRefs array in policy attestations identifies the inputs used for the computation. The format depends on how inputs were provided:
Input Type
inputRef Value
Onchain attestation UID
The attestation UID directly
Raw GeoJSON
keccak256(abi.encode(geojsonString)) — a content hash
If your contract checks inputRefs[1] == landmarkUID, it will only work when the landmark was passed as an attestation UID, not as raw GeoJSON. For raw GeoJSON, you’d need to compute the expected content hash.