# Develop Locally
Source: https://docs.astral.global/agent-quickstart
Everything an AI coding agent needs to get Astral running locally
**Research Preview** — APIs may change. [GitHub](https://github.com/AstralProtocol)
# Agent Quickstart
Paste the block below into Claude Code (or any AI coding agent). It contains everything the agent needs to clone, configure, and run the Astral stack locally.
This is a self-contained prompt — copy the whole thing. The agent will read the `CLAUDE.md` files in each repo for deeper context once it's up and running.
```text theme={null}
The Astral Protocol v0 — what you need to know
Two repos, one service, one SDK.
Repos:
- astral-location-services — Express API that runs geospatial computations via PostGIS
and signs EAS attestations. github.com/AstralProtocol/astral-location-services.
(This will run in a TEE; local-only for now while staging deployment is in progress.)
- astral-sdk — TypeScript SDK (@decentralized-geo/astral-sdk) that talks to the service
and submits attestations onchain. github.com/DecentralizedGeo/astral-sdk
(This will run on an edge device or web server, with plugins installed on client
devices needing to be located.)
Local setup (service):
git clone git@github.com:AstralProtocol/astral-location-services.git
cd astral-location-services
npm install
# Start PostGIS (needs Docker)
# Default port is 5432. If you already have Postgres running on 5432,
# edit docker-compose.dev.yml to map a different host port (e.g., "5433:5432")
# and update DATABASE_URL in .env.local to match.
docker compose -f docker-compose.dev.yml up -d
# Create .env.local from the template
cp .env.example packages/astral-service/.env.local
# Fill in SIGNER_PRIVATE_KEY (any test wallet key works for local dev)
# Generate one with: node -e "console.log(require('ethers').Wallet.createRandom().privateKey)"
# Run the service
cd packages/astral-service
# Note: the service doesn't use dotenv — you must pass the env file explicitly
node --env-file=.env.local --import tsx src/index.ts
# Runs on http://localhost:3000 (or whatever PORT you set in .env.local)
# Health check: curl http://localhost:3000/health
# Quick smoke test:
curl -X POST http://localhost:3000/compute/v0/distance \
-H "Content-Type: application/json" \
-d '{
"from": {"type": "Point", "coordinates": [-73.9857, 40.7484]},
"to": {"type": "Point", "coordinates": [-0.1278, 51.5074]},
"chainId": 84532
}'
# Should return ~5581421 meters with a signed attestation
Local setup (SDK):
git clone git@github.com:DecentralizedGeo/astral-sdk.git
cd astral-sdk
pnpm install # SDK uses pnpm, not npm
pnpm run build
pnpm run test # 402 tests, should all pass
Architecture in 30 seconds:
- Client calls SDK → SDK calls service API → service does PostGIS computation → signs a
delegated EAS attestation → returns it → SDK submits it onchain (caller pays gas,
Astral is attester)
- Main entry points: src/compute/ComputeModule.ts (SDK), src/compute/routes/ (service)
- Operations: distance, area, length, contains, within, intersects
- API uses `from`/`to` fields for input geometries (not geometryA/geometryB),
and `chainId` is required on every request
- There's also a verify module (src/verify/) that evaluates location proofs against
claims
Key files:
- Service: SPEC.md is the authoritative technical doc
- SDK: CLAUDE.md has all the commands and project structure
- Both repos have CLAUDE.md files — read those first
```
# POST /compute/v0/area
Source: https://docs.astral.global/api-reference/compute/area
Calculate area of a polygon
**Research Preview** — This API is under development.
# Area
Calculate the area of a polygon in square meters.
```
POST /compute/v0/area
```
## Request body
Target chain ID (e.g., `84532` for Base Sepolia).
The polygon geometry to measure. See [Input types](/api-reference/types#input) for accepted formats.
EAS schema UID. The server uses a default schema if not provided.
Ethereum address to receive the attestation. Defaults to the zero address.
## Example request
```bash cURL theme={null}
curl -X POST https://staging-api.astral.global/compute/v0/area \
-H "Content-Type: application/json" \
-H "X-API-Key: your-api-key" \
-d '{
"chainId": 84532,
"geometry": "0xpolygon...",
"recipient": "0xdef456..."
}'
```
```typescript TypeScript theme={null}
const response = await fetch('https://staging-api.astral.global/compute/v0/area', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': 'your-api-key'
},
body: JSON.stringify({
chainId: 84532,
geometry: '0xpolygon...',
recipient: '0xdef456...'
})
});
const result = await response.json();
```
## Response
Returns a [NumericComputeResponse](/api-reference/types#numericcomputeresponse).
Area in square meters (e.g., `5432.10`).
Always `"square_meters"`.
Always `"area"`.
Unix timestamp of computation.
Array of input references.
Signed EAS attestation. See [AttestationData](/api-reference/types#attestationdata).
Signature for delegated onchain submission. See [DelegatedAttestationData](/api-reference/types#delegatedattestationdata).
## Example response
```json theme={null}
{
"result": 5432.10,
"units": "square_meters",
"operation": "area",
"timestamp": 1706400000,
"inputRefs": [
"0xpolygon..."
],
"attestation": {
"schema": "0x...",
"attester": "0x590fdb53ed3f0B52694876d42367192a5336700F",
"recipient": "0xdef456...",
"data": "0x...",
"revocable": true,
"refUID": "0x0000000000000000000000000000000000000000000000000000000000000000",
"signature": "0x..."
},
"delegatedAttestation": {
"signature": "0x...",
"attester": "0x590fdb53ed3f0B52694876d42367192a5336700F",
"deadline": 1706403600,
"nonce": 0
}
}
```
## Notes
* Uses PostGIS `ST_Area` with geodetic calculation
* Result is in square meters with square centimeter precision
* The onchain attested value is scaled to **square centimeters** (integer) for EVM compatibility
* Only valid for polygon geometries
See `astral.compute.area()`
# POST /compute/v0/contains
Source: https://docs.astral.global/api-reference/compute/contains
Check if a geometry is inside another geometry
**Research Preview** — This API is under development.
# Contains
Check if a geometry is inside a container geometry.
```
POST /compute/v0/contains
```
## Request body
Target chain ID (e.g., `84532` for Base Sepolia).
The containing geometry (typically a polygon). See [Input types](/api-reference/types#input) for accepted formats.
The geometry to check — is this inside the container?
EAS schema UID. The server uses a default schema if not provided.
Ethereum address to receive the attestation. Defaults to the zero address.
## Example request
```bash cURL theme={null}
curl -X POST https://staging-api.astral.global/compute/v0/contains \
-H "Content-Type: application/json" \
-H "X-API-Key: your-api-key" \
-d '{
"chainId": 84532,
"container": "0xpolygon...",
"containee": "0xpoint...",
"recipient": "0xdef456..."
}'
```
```typescript TypeScript theme={null}
const response = await fetch('https://staging-api.astral.global/compute/v0/contains', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': 'your-api-key'
},
body: JSON.stringify({
chainId: 84532,
container: '0xpolygon...',
containee: '0xpoint...',
recipient: '0xdef456...'
})
});
const result = await response.json();
```
## Response
Returns a [BooleanComputeResponse](/api-reference/types#booleancomputeresponse).
`true` if the containee is inside the container, `false` otherwise.
Always `"contains"`.
Unix timestamp of computation.
Array containing `[containerRef, containeeRef]`.
Signed EAS attestation. See [AttestationData](/api-reference/types#attestationdata).
Signature for delegated onchain submission. See [DelegatedAttestationData](/api-reference/types#delegatedattestationdata).
## Example response
```json theme={null}
{
"result": true,
"operation": "contains",
"timestamp": 1706400000,
"inputRefs": [
"0xpolygon...",
"0xpoint..."
],
"attestation": {
"schema": "0x...",
"attester": "0x590fdb53ed3f0B52694876d42367192a5336700F",
"recipient": "0xdef456...",
"data": "0x...",
"revocable": true,
"refUID": "0x0000000000000000000000000000000000000000000000000000000000000000",
"signature": "0x..."
},
"delegatedAttestation": {
"signature": "0x...",
"attester": "0x590fdb53ed3f0B52694876d42367192a5336700F",
"deadline": 1706403600,
"nonce": 0
}
}
```
## Notes
* Uses PostGIS `ST_Contains` function
* The container must completely contain the geometry for `true`
* Points on the boundary may return `false` — use [intersects](/api-reference/compute/intersects) for boundary cases
The field for the inner geometry is `containee`, not `geometry`. This naming distinguishes the two inputs unambiguously.
See `astral.compute.contains()`
# POST /compute/v0/distance
Source: https://docs.astral.global/api-reference/compute/distance
Calculate distance between two geometries
**Research Preview** — This API is under development.
# Distance
Calculate the distance between two geometries in meters.
```
POST /compute/v0/distance
```
## Request body
Target chain ID. All UIDs must exist on this chain.
First geometry. See [Input types](/api-reference/types#input) for accepted formats.
Second geometry.
EAS schema UID. The server uses a default schema if not provided.
Ethereum address to receive the attestation. Defaults to the zero address.
## Example request
```bash cURL theme={null}
curl -X POST https://staging-api.astral.global/compute/v0/distance \
-H "Content-Type: application/json" \
-H "X-API-Key: your-api-key" \
-d '{
"chainId": 84532,
"from": "0xabc123...",
"to": {
"type": "Point",
"coordinates": [2.2945, 48.8584]
},
"recipient": "0xdef456..."
}'
```
```typescript TypeScript theme={null}
const response = await fetch('https://staging-api.astral.global/compute/v0/distance', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': 'your-api-key'
},
body: JSON.stringify({
chainId: 84532,
from: '0xabc123...',
to: { type: 'Point', coordinates: [2.2945, 48.8584] },
recipient: '0xdef456...'
})
});
const result = await response.json();
```
## Response
Returns a [NumericComputeResponse](/api-reference/types#numericcomputeresponse).
Distance in meters (e.g., `523.45`).
Always `"meters"`.
Always `"distance"`.
Unix timestamp of computation.
Array of input references (UIDs or geometry hashes).
Signed EAS attestation. See [AttestationData](/api-reference/types#attestationdata).
Signature for delegated onchain submission. See [DelegatedAttestationData](/api-reference/types#delegatedattestationdata).
## Example response
```json theme={null}
{
"result": 523.45,
"units": "meters",
"operation": "distance",
"timestamp": 1706400000,
"inputRefs": [
"0xabc123...",
"0x7d3e8f..."
],
"attestation": {
"schema": "0x...",
"attester": "0x590fdb53ed3f0B52694876d42367192a5336700F",
"recipient": "0xdef456...",
"data": "0x...",
"revocable": true,
"refUID": "0x0000000000000000000000000000000000000000000000000000000000000000",
"signature": "0x..."
},
"delegatedAttestation": {
"signature": "0x...",
"attester": "0x590fdb53ed3f0B52694876d42367192a5336700F",
"deadline": 1706403600,
"nonce": 0
}
}
```
## Notes
* Distance is calculated using PostGIS `ST_Distance` with geodetic coordinates
* Result is in meters with centimeter precision
* The onchain attested value is scaled to **centimeters** (integer) for EVM compatibility
* For raw GeoJSON inputs, a keccak256 hash is used in `inputRefs`
See `astral.compute.distance()`
# POST /compute/v0/intersects
Source: https://docs.astral.global/api-reference/compute/intersects
Check if two geometries overlap
**Research Preview** — This API is under development.
# Intersects
Check if two geometries intersect (share any portion of space).
```
POST /compute/v0/intersects
```
## Request body
Target chain ID (e.g., `84532` for Base Sepolia).
First geometry to check. See [Input types](/api-reference/types#input) for accepted formats.
Second geometry to check.
EAS schema UID. The server uses a default schema if not provided.
Ethereum address to receive the attestation. Defaults to the zero address.
## Example request
```bash cURL theme={null}
curl -X POST https://staging-api.astral.global/compute/v0/intersects \
-H "Content-Type: application/json" \
-H "X-API-Key: your-api-key" \
-d '{
"chainId": 84532,
"geometry1": "0xpolygon1...",
"geometry2": "0xpolygon2...",
"recipient": "0xdef456..."
}'
```
```typescript TypeScript theme={null}
const response = await fetch('https://staging-api.astral.global/compute/v0/intersects', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': 'your-api-key'
},
body: JSON.stringify({
chainId: 84532,
geometry1: '0xpolygon1...',
geometry2: '0xpolygon2...',
recipient: '0xdef456...'
})
});
const result = await response.json();
```
## Response
Returns a [BooleanComputeResponse](/api-reference/types#booleancomputeresponse).
`true` if the geometries share any space.
Always `"intersects"`.
Unix timestamp of computation.
Array containing `[geometry1Ref, geometry2Ref]`.
Signed EAS attestation. See [AttestationData](/api-reference/types#attestationdata).
Signature for delegated onchain submission. See [DelegatedAttestationData](/api-reference/types#delegatedattestationdata).
## Example response
```json theme={null}
{
"result": true,
"operation": "intersects",
"timestamp": 1706400000,
"inputRefs": [
"0xpolygon1...",
"0xpolygon2..."
],
"attestation": {
"schema": "0x...",
"attester": "0x590fdb53ed3f0B52694876d42367192a5336700F",
"recipient": "0xdef456...",
"data": "0x...",
"revocable": true,
"refUID": "0x0000000000000000000000000000000000000000000000000000000000000000",
"signature": "0x..."
},
"delegatedAttestation": {
"signature": "0x...",
"attester": "0x590fdb53ed3f0B52694876d42367192a5336700F",
"deadline": 1706403600,
"nonce": 0
}
}
```
## Notes
* Uses PostGIS `ST_Intersects` function
* Returns `true` if geometries share any space (including boundaries)
* More permissive than [contains](/api-reference/compute/contains) — touching boundaries count as intersection
See `astral.compute.intersects()`
# POST /compute/v0/length
Source: https://docs.astral.global/api-reference/compute/length
Calculate length of a line
**Research Preview** — This API is under development.
# Length
Calculate the length of a line in meters.
```
POST /compute/v0/length
```
## Request body
Target chain ID (e.g., `84532` for Base Sepolia).
The line geometry to measure. See [Input types](/api-reference/types#input) for accepted formats.
EAS schema UID. The server uses a default schema if not provided.
Ethereum address to receive the attestation. Defaults to the zero address.
## Example request
```bash cURL theme={null}
curl -X POST https://staging-api.astral.global/compute/v0/length \
-H "Content-Type: application/json" \
-H "X-API-Key: your-api-key" \
-d '{
"chainId": 84532,
"geometry": "0xline...",
"recipient": "0xdef456..."
}'
```
```typescript TypeScript theme={null}
const response = await fetch('https://staging-api.astral.global/compute/v0/length', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': 'your-api-key'
},
body: JSON.stringify({
chainId: 84532,
geometry: '0xline...',
recipient: '0xdef456...'
})
});
const result = await response.json();
```
## Response
Returns a [NumericComputeResponse](/api-reference/types#numericcomputeresponse).
Length in meters (e.g., `2345.67`).
Always `"meters"`.
Always `"length"`.
Unix timestamp of computation.
Array of input references.
Signed EAS attestation. See [AttestationData](/api-reference/types#attestationdata).
Signature for delegated onchain submission. See [DelegatedAttestationData](/api-reference/types#delegatedattestationdata).
## Example response
```json theme={null}
{
"result": 2345.67,
"units": "meters",
"operation": "length",
"timestamp": 1706400000,
"inputRefs": [
"0xline..."
],
"attestation": {
"schema": "0x...",
"attester": "0x590fdb53ed3f0B52694876d42367192a5336700F",
"recipient": "0xdef456...",
"data": "0x...",
"revocable": true,
"refUID": "0x0000000000000000000000000000000000000000000000000000000000000000",
"signature": "0x..."
},
"delegatedAttestation": {
"signature": "0x...",
"attester": "0x590fdb53ed3f0B52694876d42367192a5336700F",
"deadline": 1706403600,
"nonce": 0
}
}
```
## Notes
* Uses PostGIS `ST_Length` with geodetic calculation
* Result is in meters with centimeter precision
* The onchain attested value is scaled to **centimeters** (integer) for EVM compatibility
* Valid for LineString and MultiLineString geometries
See `astral.compute.length()`
# POST /compute/v0/within
Source: https://docs.astral.global/api-reference/compute/within
Check if a geometry is within a radius of another
**Research Preview** — This API is under development.
# Within
Check if a geometry is within a specified radius of a target geometry.
```
POST /compute/v0/within
```
## Request body
Target chain ID (e.g., `84532` for Base Sepolia).
The geometry to check (typically a point). See [Input types](/api-reference/types#input) for accepted formats.
The target geometry to measure distance from.
Maximum distance in **meters**.
EAS schema UID. The server uses a default schema if not provided.
Ethereum address to receive the attestation. Defaults to the zero address.
## Example request
```bash cURL theme={null}
curl -X POST https://staging-api.astral.global/compute/v0/within \
-H "Content-Type: application/json" \
-H "X-API-Key: your-api-key" \
-d '{
"chainId": 84532,
"geometry": "0xpoint...",
"target": "0xlandmark...",
"radius": 500,
"recipient": "0xdef456..."
}'
```
```typescript TypeScript theme={null}
const response = await fetch('https://staging-api.astral.global/compute/v0/within', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': 'your-api-key'
},
body: JSON.stringify({
chainId: 84532,
geometry: '0xpoint...',
target: '0xlandmark...',
radius: 500,
recipient: '0xdef456...'
})
});
const result = await response.json();
```
## Response
Returns a [BooleanComputeResponse](/api-reference/types#booleancomputeresponse).
`true` if the geometry is within the radius of the target.
Encodes the radius in centimeters: `"within:RADIUS_CM"` (e.g., `"within:50000"` for a 500m radius).
Unix timestamp of computation.
Array containing `[geometryRef, targetRef]`.
Signed EAS attestation. See [AttestationData](/api-reference/types#attestationdata).
Signature for delegated onchain submission. See [DelegatedAttestationData](/api-reference/types#delegatedattestationdata).
## Example response
```json theme={null}
{
"result": true,
"operation": "within:50000",
"timestamp": 1706400000,
"inputRefs": [
"0xpoint...",
"0xlandmark..."
],
"attestation": {
"schema": "0x...",
"attester": "0x590fdb53ed3f0B52694876d42367192a5336700F",
"recipient": "0xdef456...",
"data": "0x...",
"revocable": true,
"refUID": "0x0000000000000000000000000000000000000000000000000000000000000000",
"signature": "0x..."
},
"delegatedAttestation": {
"signature": "0x...",
"attester": "0x590fdb53ed3f0B52694876d42367192a5336700F",
"deadline": 1706403600,
"nonce": 0
}
}
```
## Notes
* Uses PostGIS `ST_DWithin` function
* Radius is always in **meters** in the request (no unit conversion needed)
* The `operation` field encodes the radius in **centimeters**: `within:50000` means 500 meters. Resolver contracts should use prefix matching, not exact string comparison
* Returns `true` if the distance between geometries is less than or equal to the radius
See `astral.compute.within()`
# API Overview
Source: https://docs.astral.global/api-reference/overview
REST APIs for location proof verification and geospatial operations
**Research Preview** — This API specification is under development.
# API reference
Astral provides two REST APIs for working with location proofs and geospatial data:
| API | Purpose | Base URL |
| --------------- | --------------------------------- | ------------- |
| **Verify API** | Verify location proofs and stamps | `/verify/v0` |
| **Compute API** | Verifiable geospatial operations | `/compute/v0` |
The **Records API** (query location attestations across chains) is not yet integrated into the service. See the [roadmap](/resources/roadmap) for timeline.
Verify stamps, evaluate proofs, list plugins
Distance, containment, proximity checks with signed attestations
***
## Base URLs
```
https://staging-api.astral.global/verify/v0
https://staging-api.astral.global/compute/v0
```
## Authentication
The hosted staging service is currently open: the **public tier needs no API key**. Supplying an API key as a header raises your rate limit:
```bash theme={null}
# Either header works
-H "X-API-Key: your-api-key"
-H "Authorization: Bearer your-api-key"
```
### Rate limits
| Tier | Limit | How to get |
| ------------- | -------------------- | --------------------------------------------------------------------- |
| **Public** | 100 requests/hour | Default — no API key required |
| **Developer** | 1,000 requests/hour | Email [contact@astral.global](mailto:contact@astral.global) for a key |
| **Internal** | 10,000 requests/hour | Astral team only |
Rate-limit tiers are an evolving part of the Research Preview. The public tier is open today; authenticated tiers are being rolled out.
Rate limit headers are included in every response:
```
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 97
X-RateLimit-Reset: 1706403600
```
## Input types
All compute endpoints accept geometry inputs in these formats:
| Format | Description | Example |
| -------------- | ------------------------------------------ | ------------------------------------------------- |
| Onchain UID | Onchain attestation reference | `{"uid": "0xabc123..."}` |
| GeoJSON | Raw geometry | `{"type": "Point", "coordinates": [2.29, 48.85]}` |
| UID + URI | Offchain attestation (not yet implemented) | `{"uid": "0xabc...", "uri": "ipfs://Qm..."}` |
| Verified proof | Full verified proof response object | `{"verifiedProof": { ... }}` |
See [Input](/api-reference/types#input) for the full type definition.
## Response format
### Numeric operations (distance, area, length)
```json theme={null}
{
"result": 523.45,
"units": "meters",
"operation": "distance",
"timestamp": 1706400000,
"inputRefs": ["0x...", "0x..."],
"attestation": {
"schema": "0x...",
"attester": "0x...",
"recipient": "0x...",
"data": "0x...",
"revocable": true,
"refUID": "0x0000000000000000000000000000000000000000000000000000000000000000",
"signature": "0x..."
},
"delegatedAttestation": {
"signature": "0x...",
"attester": "0x...",
"deadline": 1706403600,
"nonce": 0
}
}
```
See [NumericComputeResponse](/api-reference/types#numericcomputeresponse) and [BooleanComputeResponse](/api-reference/types#booleancomputeresponse) for the full type definitions.
### Boolean operations (contains, within, intersects)
Same shape, but `result` is `true`/`false` and there is no `units` field.
## Error format
Errors follow [RFC 7807](https://tools.ietf.org/html/rfc7807) (Problem Details for HTTP APIs):
```json theme={null}
{
"type": "https://astral.global/errors/invalid-input",
"title": "Invalid Input",
"status": 400,
"detail": "geometry field is required"
}
```
### Error types
| Type | Status | Description |
| ----------------- | ------ | -------------------------------------------------- |
| `bad-request` | 400 | Malformed request body |
| `invalid-input` | 400 | Bad request data, missing fields, invalid geometry |
| `validation` | 400 | Input validation failed |
| `unauthorized` | 401 | Invalid or missing API key |
| `not-implemented` | 501 | Requested operation or plugin not yet supported |
| `internal` | 500 | Internal server error |
| `database` | 500 | Database operation failed |
| `rate-limited` | 429 | Too many requests — check rate limit headers |
## Chain configuration
The **Attester Address** is the address that signs delegated attestations. Resolver contracts must verify attestations come from this address.
| Chain | Chain ID | EAS | Schema Registry | Attester Address |
| ------------ | -------- | -------------------------------------------- | -------------------------------------------- | -------------------------------------------- |
| Base Sepolia | 84532 | `0x4200000000000000000000000000000000000021` | `0x4200000000000000000000000000000000000020` | `0x590fdb53ed3f0B52694876d42367192a5336700F` |
## Endpoints
### Verify API
Verify a single stamp's internal validity
Verify a proof with cross-correlation analysis
List available verification plugins
### Compute API
Distance between two geometries
Area of a polygon
Length of a line
Is geometry B inside geometry A?
Is point within radius of target?
Do geometries overlap?
# Data Types
Source: https://docs.astral.global/api-reference/types
Shared data types used across the Astral API
# Data types
This page defines all shared types referenced by the [Verify API](/api-reference/verify/stamp) and [Compute API](/api-reference/compute/distance) endpoints.
***
## Input
All compute endpoints accept geometry inputs as a discriminated union. The server resolves the input format automatically.
```typescript theme={null}
type Input =
| GeoJSON.Geometry // Raw GeoJSON geometry
| OnchainInput // Onchain attestation reference
| OffchainInput // Offchain attestation reference (not yet implemented)
| VerifiedProofInput; // Verified location proof
```
The SDK accepts bare UID strings (e.g., `"0xabc123..."`) and normalizes them to `OnchainInput` before sending to the API. When calling the API directly, always use the `{ uid: string }` object form.
### RawGeometryInput
A standard GeoJSON geometry object:
```json theme={null}
{
"type": "Point",
"coordinates": [2.2945, 48.8584]
}
```
### OnchainInput
A reference to an onchain EAS attestation:
```json theme={null}
{
"uid": "0xabc123..."
}
```
### OffchainInput
Not yet implemented. Planned for a future release.
A reference to an offchain attestation stored on IPFS or another URI-addressable store:
```json theme={null}
{
"uid": "0xabc...",
"uri": "ipfs://Qm..."
}
```
### VerifiedProofInput
A previously verified location proof, used to chain proof verification with compute operations. Pass the full `VerifiedLocationProofResponse` object returned by [POST /verify/v0/proof](/api-reference/verify/proof):
```typescript theme={null}
interface VerifiedProofInput {
verifiedProof: VerifiedLocationProofResponse;
}
```
```json theme={null}
{
"verifiedProof": {
"proof": { "..." : "..." },
"credibility": { "..." : "..." },
"evaluationMethod": "multifactor-v0",
"evaluatedAt": 1706961600
}
}
```
***
## LocationClaim
A claim that a subject was at a location during a time window. Extends [Location Protocol v0.2](https://github.com/DecentralizedGeo/location-protocol-spec).
```typescript theme={null}
interface LocationClaim {
lpVersion: string; // "0.2"
locationType: string; // "geojson-point", "h3-index", etc.
location: GeoJSON.Geometry; // The asserted location
srs: string; // Spatial reference system URI
subject: SubjectIdentifier; // Who is making the claim
radius: number; // Meters — required
time: {
start: number; // Unix timestamp (seconds)
end: number;
};
eventType?: string; // "presence", "transaction", "delivery"
}
```
***
## LocationStamp
Evidence from a single proof-of-location system — one observation. Uses LP format for the observed location.
```typescript theme={null}
interface LocationStamp {
lpVersion: string;
locationType: string;
location: GeoJSON.Geometry; // The OBSERVED location
srs: string;
temporalFootprint: {
start: number; // Unix timestamp (seconds)
end: number;
};
plugin: string; // "proofmode", "witnesschain", "mock"
pluginVersion: string;
signals: Record;
signatures: Signature[];
}
```
***
## LocationProof
A claim bundled with one or more stamps.
```typescript theme={null}
interface LocationProof {
claim: LocationClaim;
stamps: LocationStamp[];
}
```
***
## SubjectIdentifier
Identifies the subject of a claim. Follows a scheme/value pattern similar to [DIDs](https://www.w3.org/TR/did-core/).
```typescript theme={null}
interface SubjectIdentifier {
scheme: string; // "eth-address" | "device-pubkey" | "did:web"
value: string;
}
```
**Examples:**
```json theme={null}
{ "scheme": "eth-address", "value": "0x1234..." }
{ "scheme": "device-pubkey", "value": "0xabcd..." }
```
***
## Signature
A cryptographic signature on a stamp.
```typescript theme={null}
interface Signature {
signer: SubjectIdentifier;
algorithm: string; // "secp256k1" | "ed25519"
value: string; // Hex-encoded
timestamp: number; // Unix timestamp (seconds)
}
```
***
## CredibilityVector
Multidimensional assessment of how well stamps support a claim. Returned by [POST /verify/v0/proof](/api-reference/verify/proof).
```typescript theme={null}
interface CredibilityVector {
dimensions: {
spatial: {
meanDistanceMeters: number;
maxDistanceMeters: number;
withinRadiusFraction: number; // 0-1
};
temporal: {
meanOverlap: number; // 0-1
minOverlap: number; // 0-1
fullyOverlappingFraction: number; // 0-1
};
validity: {
signaturesValidFraction: number; // 0-1
structureValidFraction: number; // 0-1
signalsConsistentFraction: number; // 0-1
};
independence: {
uniquePluginRatio: number; // 0-1
spatialAgreement: number; // 0-1
pluginNames: string[];
};
};
stampResults: StampResult[];
meta: {
stampCount: number;
evaluatedAt: number; // Unix timestamp (seconds)
evaluationMode: 'local' | 'tee' | 'zk';
};
}
```
***
## StampResult
Per-stamp verification result within a `CredibilityVector`.
```typescript theme={null}
interface StampResult {
stampIndex: number;
plugin: string;
signaturesValid: boolean;
structureValid: boolean;
signalsConsistent: boolean;
distanceMeters: number; // Distance from stamp to claim location
temporalOverlap: number; // 0-1
withinRadius: boolean;
details: Record; // Plugin-specific
}
```
***
## AttestationData
EAS attestation data returned by compute endpoints.
```typescript theme={null}
interface AttestationData {
schema: string; // Schema UID
attester: string; // Attester address
recipient: string; // Recipient address
data: string; // ABI-encoded attestation data
revocable: boolean; // Whether the attestation can be revoked
refUID: string; // Referenced attestation UID (zero bytes if none)
signature: string; // Hex-encoded signature
}
```
The verify proof endpoint returns an extended attestation that also includes `uid`, `time`, `expirationTime`, and `revocationTime`. See [VerifiedLocationProofResponse](#verifiedlocationproofresponse) for details.
***
## DelegatedAttestationData
Signature and metadata for submitting a delegated attestation onchain.
```typescript theme={null}
interface DelegatedAttestationData {
signature: string; // Hex-encoded EIP-712 signature
attester: string; // Attester address
deadline: number; // Unix timestamp — signature expiry
nonce: number; // Attester nonce for replay protection
}
```
***
## NumericComputeResponse
Response shape for compute operations that return a number (distance, area, length).
```typescript theme={null}
interface NumericComputeResponse {
result: number;
units: string; // "meters", "square_meters"
operation: string;
timestamp: number;
inputRefs: string[];
attestation: AttestationData;
delegatedAttestation: DelegatedAttestationData;
}
```
***
## BooleanComputeResponse
Response shape for compute operations that return true/false (contains, within, intersects).
```typescript theme={null}
interface BooleanComputeResponse {
result: boolean;
operation: string;
timestamp: number;
inputRefs: string[];
attestation: AttestationData;
delegatedAttestation: DelegatedAttestationData;
}
```
***
## VerifiedLocationProofResponse
Response shape for [POST /verify/v0/proof](/api-reference/verify/proof).
```typescript theme={null}
interface VerifiedLocationProofResponse {
proof: LocationProof;
credibility: CredibilityVector;
evaluationMethod: string;
evaluatedAt: number; // Unix timestamp (seconds)
attestation?: {
uid: string; // Attestation UID
schema: string;
attester: string;
recipient: string;
data: string;
revocable: boolean;
refUID: string;
time: number; // Attestation creation time
expirationTime: number; // 0 = no expiration
revocationTime: number; // 0 = not revoked
signature?: string;
};
delegatedAttestation?: DelegatedAttestationData;
chainId?: number;
}
```
# GET /verify/v0/plugins
Source: https://docs.astral.global/api-reference/verify/plugins
List available verification plugins
**Research Preview** — This API is under development.
# List plugins
List the verification plugins available on the service. Each plugin represents a proof-of-location system that can produce [LocationStamps](/api-reference/types#locationstamp).
```
GET /verify/v0/plugins
```
## Example request
```bash cURL theme={null}
curl https://staging-api.astral.global/verify/v0/plugins \
-H "X-API-Key: your-api-key"
```
```typescript TypeScript theme={null}
const response = await fetch('https://staging-api.astral.global/verify/v0/plugins', {
headers: { 'X-API-Key': 'your-api-key' }
});
const { plugins } = await response.json();
```
## Response
Array of available plugins.
### Plugin object
| Field | Type | Description |
| -------------- | ---------- | --------------------------------------------------------- |
| `name` | `string` | Plugin identifier (e.g., `"proofmode"`, `"witnesschain"`) |
| `version` | `string` | Semver version string |
| `environments` | `string[]` | Runtime environments the plugin supports |
| `description` | `string` | Human-readable description |
**ProofMode is working today** — the Verify service implements ProofMode stamp verification. The other plugins listed (`witnesschain`, `gpsd`, `geoclue`, `wifi-mls`, `ip-geolocation`) are **experimental**: their interfaces are defined and early verification logic exists, but they aren't production yet. We're keen to develop new proof-of-location plugins with partners — [get in touch](mailto:contact@astral.global). On the client side, the SDK bundles `MockPlugin` for development; ProofMode evidence comes from the ProofMode app.
## Example response
```json theme={null}
{
"plugins": [
{
"name": "proofmode",
"version": "0.1.0",
"environments": ["mobile", "server"],
"description": "Device attestation with hardware-backed cryptographic signatures"
},
{
"name": "witnesschain",
"version": "0.1.0",
"environments": ["server", "node"],
"description": "WitnessChain proof-of-location via network latency triangulation"
},
{
"name": "gpsd",
"version": "0.1.0",
"environments": ["server", "node"],
"description": "GPS daemon (gpsd) location readings"
},
{
"name": "geoclue",
"version": "0.1.0",
"environments": ["server", "node"],
"description": "GeoClue geolocation service"
},
{
"name": "wifi-mls",
"version": "0.1.0",
"environments": ["server", "node"],
"description": "Wi-Fi positioning via Mozilla Location Service"
},
{
"name": "ip-geolocation",
"version": "0.1.0",
"environments": ["server", "node"],
"description": "IP-based geolocation lookup"
}
]
}
```
## Errors
| Type | Status | Description |
| -------------- | ------ | ------------------------------ |
| `internal` | 500 | Failed to retrieve plugin list |
| `rate-limited` | 429 | Too many requests |
| `unauthorized` | 401 | Invalid or missing API key |
See the SDK documentation for client-side usage
# POST /verify/v0/proof
Source: https://docs.astral.global/api-reference/verify/proof
Verify a location proof with cross-correlation analysis
**Research Preview** — This API is under development.
# Verify proof
Verify a location proof (a claim bundled with one or more stamps). This endpoint verifies each stamp individually, then cross-correlates them to produce a [CredibilityVector](/api-reference/types#credibilityvector) — a multidimensional assessment of how well the evidence supports the claim.
```
POST /verify/v0/proof
```
The Verify API does **not** return a single summary score. The `CredibilityVector` provides spatial, temporal, validity, and independence dimensions so your application can define its own trust model.
## Request body
The proof to verify. Contains a `claim` ([LocationClaim](/api-reference/types#locationclaim)) and an array of `stamps` ([LocationStamp](/api-reference/types#locationstamp)).
Chain ID for EAS attestation signing (e.g., `84532` for Base Sepolia).
Whether to submit the attestation onchain. Defaults to `false`.
## Example request
```bash cURL theme={null}
curl -X POST https://staging-api.astral.global/verify/v0/proof \
-H "Content-Type: application/json" \
-H "X-API-Key: your-api-key" \
-d '{
"proof": {
"claim": {
"lpVersion": "0.2",
"locationType": "geojson-point",
"location": { "type": "Point", "coordinates": [-122.4194, 37.7749] },
"srs": "http://www.opengis.net/def/crs/OGC/1.3/CRS84",
"subject": { "scheme": "eth-address", "value": "0x1234..." },
"radius": 100,
"time": { "start": 1706900000, "end": 1706903600 }
},
"stamps": [
{
"lpVersion": "0.2",
"locationType": "geojson-point",
"location": { "type": "Point", "coordinates": [-122.4195, 37.7750] },
"srs": "http://www.opengis.net/def/crs/OGC/1.3/CRS84",
"temporalFootprint": { "start": 1706901000, "end": 1706901060 },
"plugin": "proofmode",
"pluginVersion": "0.1.0",
"signals": {},
"signatures": [{ "signer": { "scheme": "device-pubkey", "value": "0x..." }, "algorithm": "secp256k1", "value": "0x...", "timestamp": 1706901030 }]
},
{
"lpVersion": "0.2",
"locationType": "geojson-point",
"location": { "type": "Point", "coordinates": [-122.4193, 37.7748] },
"srs": "http://www.opengis.net/def/crs/OGC/1.3/CRS84",
"temporalFootprint": { "start": 1706901000, "end": 1706901120 },
"plugin": "witnesschain",
"pluginVersion": "0.1.0",
"signals": {},
"signatures": [{ "signer": { "scheme": "eth-address", "value": "0x..." }, "algorithm": "secp256k1", "value": "0x...", "timestamp": 1706901060 }]
}
]
},
"options": {
"chainId": 84532
}
}'
```
```typescript TypeScript theme={null}
const response = await fetch('https://staging-api.astral.global/verify/v0/proof', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': 'your-api-key'
},
body: JSON.stringify({
proof: {
claim: {
lpVersion: '0.2',
locationType: 'geojson-point',
location: { type: 'Point', coordinates: [-122.4194, 37.7749] },
srs: 'http://www.opengis.net/def/crs/OGC/1.3/CRS84',
subject: { scheme: 'eth-address', value: '0x1234...' },
radius: 100,
time: { start: 1706900000, end: 1706903600 }
},
stamps: [
// ... stamps array
]
},
options: { chainId: 84532 }
})
});
```
## Response
The response contains the verified proof, a credibility vector, and optional EAS attestation data.
The verified proof object.
Multidimensional assessment of how well the stamps support the claim. See [CredibilityVector](/api-reference/types#credibilityvector) for the full type.
The evaluation method used (e.g., `"multifactor-v0"`).
Unix timestamp (seconds) of when the evaluation was performed.
EAS attestation data. Present when `chainId` is provided. See [AttestationData](/api-reference/types#attestationdata).
Delegated attestation for onchain submission. See [DelegatedAttestationData](/api-reference/types#delegatedattestationdata).
## Example response
**Illustrative example.** ProofMode verification is working today; the `witnesschain` stamp below is shown to illustrate multi-source cross-correlation, but WitnessChain (along with `gpsd`, `geoclue`, `wifi-mls`, `ip-geolocation`) is still experimental. Some `details` fields shown here (for example `certificateChainValid`) correspond to checks that are **planned but not yet performed in v0** — see the [ProofMode plugin](/plugins/proofmode) for exactly what verification does today. The credibility-vector structure is also still evolving.
```json theme={null}
{
"proof": {
"claim": { "..." : "..." },
"stamps": ["..."]
},
"credibility": {
"dimensions": {
"spatial": {
"meanDistanceMeters": 12.5,
"maxDistanceMeters": 18.3,
"withinRadiusFraction": 1.0
},
"temporal": {
"meanOverlap": 0.95,
"minOverlap": 0.90,
"fullyOverlappingFraction": 0.5
},
"validity": {
"signaturesValidFraction": 1.0,
"structureValidFraction": 1.0,
"signalsConsistentFraction": 1.0
},
"independence": {
"uniquePluginRatio": 1.0,
"spatialAgreement": 0.88,
"pluginNames": ["proofmode", "witnesschain"]
}
},
"stampResults": [
{
"stampIndex": 0,
"plugin": "proofmode",
"signaturesValid": true,
"structureValid": true,
"signalsConsistent": true,
"distanceMeters": 12.5,
"temporalOverlap": 0.95,
"withinRadius": true,
"details": {
"hashVerified": true,
"certificateChainValid": true
}
},
{
"stampIndex": 1,
"plugin": "witnesschain",
"signaturesValid": true,
"structureValid": true,
"signalsConsistent": true,
"distanceMeters": 18.3,
"temporalOverlap": 0.90,
"withinRadius": true,
"details": {
"latencyVerified": true,
"nodeCount": 3
}
}
],
"meta": {
"stampCount": 2,
"evaluatedAt": 1706961600,
"evaluationMode": "local"
}
},
"evaluationMethod": "multifactor-v0",
"evaluatedAt": 1706961600,
"attestation": {
"uid": "0xdef456...",
"schema": "0x...",
"attester": "0x590fdb53ed3f0B52694876d42367192a5336700F",
"recipient": "0x1234...",
"data": "0x...",
"revocable": true,
"refUID": "0x0000000000000000000000000000000000000000000000000000000000000000",
"signature": "0x..."
},
"delegatedAttestation": {
"signature": "0x...",
"attester": "0x590fdb53ed3f0B52694876d42367192a5336700F",
"deadline": 1706403600,
"nonce": 0
}
}
```
## Credibility vector
The `credibility` field is a `CredibilityVector` with four dimension groups:
### `dimensions.spatial`
How close each stamp's observed location is to the claimed location.
| Field | Type | Description |
| ---------------------- | -------- | ------------------------------------------------------------- |
| `meanDistanceMeters` | `number` | Average distance between stamp locations and claimed location |
| `maxDistanceMeters` | `number` | Largest distance from any stamp to the claim |
| `withinRadiusFraction` | `number` | Fraction of stamps within the claim's radius (0-1) |
### `dimensions.temporal`
How well each stamp's temporal footprint overlaps with the claimed time window.
| Field | Type | Description |
| -------------------------- | -------- | ------------------------------------------------------- |
| `meanOverlap` | `number` | Average temporal overlap ratio (0-1) |
| `minOverlap` | `number` | Smallest overlap of any stamp |
| `fullyOverlappingFraction` | `number` | Fraction of stamps fully within the claim's time window |
### `dimensions.validity`
Fraction of stamps passing each verification check.
| Field | Type | Description |
| --------------------------- | -------- | -------------------------------------- |
| `signaturesValidFraction` | `number` | Fraction with valid signatures (0-1) |
| `structureValidFraction` | `number` | Fraction with valid structure (0-1) |
| `signalsConsistentFraction` | `number` | Fraction with consistent signals (0-1) |
### `dimensions.independence`
How independent and corroborative the evidence sources are.
| Field | Type | Description |
| ------------------- | ---------- | --------------------------------------------- |
| `uniquePluginRatio` | `number` | Ratio of unique plugins to total stamps (0-1) |
| `spatialAgreement` | `number` | How well stamps agree spatially (0-1) |
| `pluginNames` | `string[]` | List of plugins that contributed stamps |
### `stampResults`
Per-stamp verification results. See [StampResult](/api-reference/types#stampresult) for the full type.
### `meta`
| Field | Type | Description |
| ---------------- | -------- | ---------------------------------------------- |
| `stampCount` | `number` | Number of stamps evaluated |
| `evaluatedAt` | `number` | Unix timestamp (seconds) |
| `evaluationMode` | `string` | Evaluation mode: `"local"`, `"tee"`, or `"zk"` |
## Errors
Errors follow [RFC 7807](https://tools.ietf.org/html/rfc7807):
| Type | Status | Description |
| ----------------- | ------ | ---------------------------------------------- |
| `invalid-input` | 400 | Malformed proof, missing claim fields |
| `validation` | 400 | Claim validation failed (e.g., missing radius) |
| `not-implemented` | 501 | Plugin not supported |
| `internal` | 500 | Internal verification error |
| `rate-limited` | 429 | Too many requests |
| `unauthorized` | 401 | Invalid or missing API key |
See the SDK documentation for client-side usage
# POST /verify/v0/stamp
Source: https://docs.astral.global/api-reference/verify/stamp
Verify a single location stamp's internal validity
**Research Preview** — This API is under development.
# Verify stamp
Verify a single location stamp's internal validity — checks cryptographic signatures, structure, and signal consistency. This endpoint does not assess how well the stamp supports a claim; use [verify proof](/api-reference/verify/proof) for that.
```
POST /verify/v0/stamp
```
## Request body
The location stamp to verify. See [LocationStamp](/api-reference/types#locationstamp) for the full type definition.
Key fields: `lpVersion`, `locationType`, `location`, `srs`, `temporalFootprint`, `plugin`, `pluginVersion`, `signals`, `signatures`.
## Example request
```bash cURL theme={null}
curl -X POST https://staging-api.astral.global/verify/v0/stamp \
-H "Content-Type: application/json" \
-H "X-API-Key: your-api-key" \
-d '{
"stamp": {
"lpVersion": "0.2",
"locationType": "geojson-point",
"location": { "type": "Point", "coordinates": [-122.4194, 37.7749] },
"srs": "http://www.opengis.net/def/crs/OGC/1.3/CRS84",
"temporalFootprint": {
"start": 1706901000,
"end": 1706901060
},
"plugin": "proofmode",
"pluginVersion": "0.1.0",
"signals": { "sensorData": "..." },
"signatures": [{
"signer": { "scheme": "device-pubkey", "value": "0xabcd..." },
"algorithm": "secp256k1",
"value": "0x...",
"timestamp": 1706901030
}]
}
}'
```
```typescript TypeScript theme={null}
const response = await fetch('https://staging-api.astral.global/verify/v0/stamp', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': 'your-api-key'
},
body: JSON.stringify({
stamp: {
lpVersion: '0.2',
locationType: 'geojson-point',
location: { type: 'Point', coordinates: [-122.4194, 37.7749] },
srs: 'http://www.opengis.net/def/crs/OGC/1.3/CRS84',
temporalFootprint: { start: 1706901000, end: 1706901060 },
plugin: 'proofmode',
pluginVersion: '0.1.0',
signals: { sensorData: '...' },
signatures: [{
signer: { scheme: 'device-pubkey', value: '0xabcd...' },
algorithm: 'secp256k1',
value: '0x...',
timestamp: 1706901030
}]
}
})
});
const result = await response.json();
```
## Response
Overall validity — `true` only if all checks pass.
Whether all cryptographic signatures verified successfully.
Whether the stamp conforms to the expected structure for its plugin.
Whether internal signals are self-consistent.
Plugin-specific verification details. Contents vary by plugin.
## Example response
Example only. `details` contents are plugin-specific, and some fields shown here (for example `certificateChainValid`) correspond to checks that are **planned but not yet performed in v0**. See the [ProofMode plugin](/plugins/proofmode) for exactly what stamp verification does today.
```json theme={null}
{
"valid": true,
"signaturesValid": true,
"structureValid": true,
"signalsConsistent": true,
"details": {
"plugin": "proofmode",
"hashVerified": true,
"certificateChainValid": true
}
}
```
## Errors
Errors follow [RFC 7807](https://tools.ietf.org/html/rfc7807):
```json theme={null}
{
"type": "https://astral.global/errors/invalid-input",
"title": "Invalid Input",
"status": 400,
"detail": "stamp.signatures is required"
}
```
| Type | Status | Description |
| ----------------- | ------ | ------------------------------------------ |
| `invalid-input` | 400 | Malformed stamp or missing required fields |
| `not-implemented` | 501 | Plugin not supported |
| `internal` | 500 | Internal verification error |
| `rate-limited` | 429 | Too many requests |
| `unauthorized` | 401 | Invalid or missing API key |
See the SDK documentation for client-side usage
# Astral Location Services
Source: https://docs.astral.global/concepts/astral-location-services
The hosted TEE service that runs verification and computation
**Research Preview** — APIs may change. [GitHub](https://github.com/AstralProtocol)
# Astral Location Services
Astral Location Services is the hosted service that performs [location proof verification](/concepts/verify) and [geospatial computation](/concepts/compute). It is designed to run inside a Trusted Execution Environment (TEE) — which, under attestation, is what makes results verifiable rather than merely signed.
**Deployment status.** Astral has run this service on real TEE hardware in test deployments, but does not currently fund continuous operation on attested hardware. The properties described below hold when the enclave runs under continuous remote attestation; on the hosted staging service today, a valid signature proves a key Astral controls produced the result, not yet that an independently attested enclave did. See [What you are trusting](/trust-model/what-you-are-trusting). To evaluate against real TEEs, reach out at [contact@astral.global](mailto:contact@astral.global).
The TEE makes the *computation* verifiable — that the attested code ran on the stated inputs. It does not make the *location inputs* truthful. Whether a location is real depends on the strength of the [location proof](/concepts/location-proofs) behind it, not on the TEE.
## What the Service Provides
Two endpoints, one TEE:
* **[Verify](/concepts/verify)** — Submit a location proof, get back a verified location proof: the original proof, a [credibility vector](/concepts/location-proof-evaluation#the-credibility-vector), and a signed EAS attestation
* **[Compute](/concepts/compute)** — Submit location data with geographic features and a specified spatial operation, get a signed result representing the computed relationship between those features
Both endpoints accept requests via the [Astral SDK](/sdk/overview) or directly through the [API](/api-reference/overview).
## Verifiability Properties
Under attestation, the TEE is designed to provide four properties that together make computation verifiable:
| Property | What it provides |
| ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Input verification** | Signatures on signed inputs are verified at the TEE boundary, and inputs are validated before computation begins. (Raw GeoJSON carries no signature and is accepted as unverified.) |
| **Deterministic computation** | Same inputs always produce the same result. PostGIS version is pinned, precision is fixed at centimeter level, and no persistent state exists between requests. |
| **Signed output** | Results are signed by a key held inside the TEE, intended to be non-extractable by the operator when the enclave is attested. |
| **TEE attestation** | The TEE is designed to provide hardware-generated attestation that specific code executed on specific inputs inside the enclave (currently via EigenCompute, though the design isn't tied to one provider). |
Together, under attestation: the code is attested, signed inputs are verified, the computation is deterministic, and the output is signed by a key the operator cannot access. An observer can then verify that the result came from the correct code running on the referenced inputs — without re-executing the computation. This is a statement about the *computation*, not about whether the input locations are truthful.
## Privacy Properties
**The current hosted service does not run in a TEE; nothing in the request path is encrypted.** It receives plaintext over HTTPS and processes it in an ordinary process, and there is no client-to-enclave encryption in the design. Astral has run the service on real TEE hardware in test deployments (which would provide the process isolation described here), but does not operate one continuously. See [Privacy](/concepts/privacy) and the [trust model](/trust-model/what-you-are-trusting) for the full picture.
Running the service inside an attested enclave would keep the *host operator* from inspecting the running process's memory — so raw input coordinates, exact geometries, and stamp signals would live only in enclave memory during computation. That is the privacy property a TEE provides, and it's narrow: it would **not** encrypt inputs end-to-end, and it would **not** stop the service from returning inputs in the result. True input and output privacy needs the affordances described in [Privacy](/concepts/privacy#privacy-modes-were-designing), not the TEE alone.
**v0 caveat.** Today, signed results may still include input data in plaintext (the full claim, stamps, and credibility vector travel with the result), so anyone who *receives* a result can read those inputs. A privacy-preserving output mode is planned. See [Privacy](/concepts/privacy) for the full picture.
Some information also leaks from the result itself (a `contains` answer of `true` tells you the point is inside the polygon), but that is inherent to the computation, not a limitation of the privacy model.
## TEE stack
Astral is **not tied to a specific TEE provider**. The architecture is a self-contained Docker container, so in principle it can run in any TEE that supports containerized workloads — assessing portability across providers is ongoing. The current deployment target is [EigenCompute](https://blog.eigencloud.xyz/eigencloud-brings-verifiable-ai-to-mass-market-with-eigenai-and-eigencompute-launches/) (part of the EigenCloud ecosystem):
```mermaid theme={null}
sequenceDiagram
participant C as Client
participant T as EigenCompute TEE
C->>T: Encrypted request
Note over T: Decrypt inside enclave
Note over T: Validate inputs
Note over T: PostGIS computation
Note over T: Sign result with TEE key
T->>C: Signed result
```
PostGIS runs **inside** the TEE container, not as an external service — no external dependencies means the entire execution environment is attested. The GEOS library under PostGIS is the same C++ geometry engine used by QGIS, GDAL, and most professional geospatial software.
## The Signing Key
The signing key is generated and provisioned inside the TEE. The design intent is that the operator cannot extract it — a property that holds when the enclave runs under remote attestation (see the deployment-status note above). All signed results are produced by this key, and downstream consumers (smart contracts, applications, agents) can verify that a result was signed by the Astral service by checking the signature against the known public key.
Signing key publication is not yet finalized — key management and rotation are still being worked out for production deployment. This page will be updated with the public key and verification instructions when available.
For key rotation and management details in smart contract integrations, see the [SDK: EAS module](/sdk/eas).
## Stateless Model
Each request brings all required inputs. There is no persistent state between requests. This ensures determinism — the same request always produces the same result, regardless of when it's submitted or what other requests have been processed.
## Future Directions
The current TEE-based approach is what makes computation verifiable under attestation. Directions we're exploring to reduce the trust surface further:
* **AVS consensus** — Multiple operators independently verify computations
* **ZK proofs** — Cryptographic proof of correct execution without trusted hardware
* **Decentralized signers** — Multi-party result signing
The verification endpoint in detail
***
**See also:**
* [API Reference](/api-reference/overview) — full endpoint documentation
# Compute
Source: https://docs.astral.global/concepts/compute
Geospatial operations inside the TEE — distance, containment, and more
**Research Preview** — APIs may change. [GitHub](https://github.com/AstralProtocol)
# Compute
The Compute endpoint runs geospatial operations on location data inside the [TEE](/concepts/astral-location-services) and returns [signed results](/concepts/signed-results). It answers spatial questions — how far apart are these locations? is this point inside that boundary? — and signs the answer so the *computation* can be verified independently. (Whether the *input* locations are truthful is a separate question — see [accepted inputs](#accepted-inputs) below.)
## Accepted Inputs
The compute endpoint accepts location data at any level of the [verifiability spectrum](/concepts/location-data#the-verifiability-spectrum):
| Input type | What it is | Trust properties |
| --------------------------- | ----------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
| **Raw GeoJSON** | Unsigned geometry | Proves computation was correct; does not prove input provenance |
| **Signed location record** | EAS attestation UID (onchain) or UID + URI (offchain) | Attribution + integrity + correct computation |
| **Verified location proof** | UID of a verified location proof | Evidence supporting physical correspondence (per the credibility vector) + correct computation |
You can mix input types in a single operation. A geofencing check might use raw GeoJSON for the boundary (a publicly known polygon) and a verified location proof for the user's position.
## Inside the TEE
The compute engine is PostGIS backed by GEOS — the same C++ library that powers QGIS, GDAL, and most professional geospatial software. PostGIS runs inside the Docker container within the TEE, not as an external service.
Computation is stateless and deterministic. Each request brings all required inputs. Results are rounded to centimeter precision before signing to ensure reproducibility. Same inputs always produce the same output.
## Available Operations
### Measurements (Numeric Results)
| Operation | What it computes | Unit | PostGIS function |
| ------------ | --------------------------------------- | ------------- | ---------------- |
| **Distance** | Nearest distance between two geometries | meters | `ST_Distance` |
| **Area** | Area of a polygon | square meters | `ST_Area` |
| **Length** | Length of a line | meters | `ST_Length` |
### Predicates (Boolean Results)
| Operation | What it checks | PostGIS function |
| -------------- | ------------------------------------------------ | ---------------- |
| **Contains** | Is geometry B entirely inside geometry A? | `ST_Contains` |
| **Within** | Is geometry within a specified radius of target? | `ST_DWithin` |
| **Intersects** | Do geometries share any space? | `ST_Intersects` |
All measurements use metric units. No unit conversion is provided — convert client-side if needed.
## Precision and Determinism
Results are stored with centimeter precision as scaled integers:
| Type | Precision | Scaling |
| ----------------- | --------- | --------------------------------- |
| Distance / Length | 0.01 m | 523.45 meters → stored as 52345 |
| Area | 0.0001 m² | 1234.5678 m² → stored as 12345678 |
This scaling ensures deterministic integer representation, which is important for smart contract integration where floating-point arithmetic isn't available.
## Output
Every compute operation returns a [signed result](/concepts/signed-results) containing:
* The computed answer (boolean or numeric)
* References to the specific inputs used (note: in v0 the result may also carry the full input data in plaintext — see [Privacy](/concepts/privacy))
* A timestamp
* The operation name
* A cryptographic signature from the TEE-held signing key
The signed result can be used offchain (in an agent, application, or database) or submitted onchain via EAS delegated attestation. EAS resolvers allow those attestations to trigger smart contract logic.
## What's Next
The current operation set covers the most common spatial questions, but PostGIS exposes a much larger surface. Areas we're exploring:
* **More predicates and measurements** — `disjoint`, `touches`, `crosses`, nearest-neighbor queries
* **Geometry transformations** — buffers, centroids, unions, intersections. These return **new geometries** rather than scalar or boolean values, which raises open design questions about signed result format and storage
* **Spatial selection queries** — operations over sets of geometries, like "which of these polygons contains this point?" or "find all zones within 1km of this location"
* **Compositional queries** — chaining multiple operations into a single verified request, so you could express something like "is this point within 500m of any geometry in this set that intersects this boundary?" without multiple round trips
The operation set is extensible. If you need a spatial operation that isn't listed here, [open an issue](https://github.com/AstralProtocol/astral-location-services/issues).
Output formats and how to use them
***
**See also:**
* [API: Compute endpoints](/api-reference/compute/distance) — endpoint reference for each operation
* [SDK: Compute module](/sdk/compute) — programmatic access to compute operations
* [API: Types](/api-reference/types) — result type reference
# Overview
Source: https://docs.astral.global/concepts/geocomputation
Processing location data with proof of correct execution
**Research Preview** — APIs may change. [GitHub](https://github.com/AstralProtocol)
# Geocomputation
Geocomputation is the processing of geospatial data to answer spatial questions: How far apart are these two locations? Is this point inside that boundary? Do these regions overlap? The answers seem simple, but computing them correctly — on a curved, non-Euclidean surface, with consistent precision and deterministic results — is harder than it appears.
## The computational challenge
Spatial computation involves real complexity:
* **Reference systems** — The Earth is not flat, and different coordinate systems represent it differently. Computing distance on a sphere vs. a projected plane vs. a geoid gives different results.
* **Computational geometry** — Operations like polygon containment, intersection detection, and area calculation require robust algorithms that handle edge cases (self-intersecting polygons, antipodal points, degenerate geometries).
* **Precision and determinism** — Floating-point arithmetic is not associative. The same computation can produce different results depending on operation order, hardware, or library version. For verifiable results, computation must be deterministic.
Professional geospatial software (QGIS, GDAL, PostGIS) has spent decades solving these problems. Astral builds on that foundation. (This is a pragmatic shift from our original plan to reimplement these [algorithms](https://github.com/DecentralizedGeo/spatial-sol) in Solidity.)
## Scope of geocomputation in Astral
Astral's geocomputation capabilities span three areas, at different stages of maturity:
| Capability | Status | What it does |
| --------------------------------------------------- | ---------------------------- | ------------------------------------------------- |
| **[Location proof verification](/concepts/verify)** | Available (Research Preview) | Evaluates location proof credibility |
| **[Geospatial operations](/concepts/compute)** | Available (Research Preview) | Distance, containment, intersection, area, length |
| **Geospatial AI/ML** | Planned | Spatial analysis, prediction, pattern detection |
## Verifiable geocomputation
Computation is only useful in adversarial contexts if you can trust the result. For most applications, that means running code on a server and trusting the operator. For applications where the spatial answer triggers real-world consequences that may carry an incentive to lie — a smart contract execution, a compliance determination, an autonomous agent decision — that trust model isn't sufficient.
Astral makes geocomputation verifiable through three approaches, at different stages of development:
**Trusted Execution Environments (v0).** Astral's Compute engine is designed to run inside a TEE, which provides hardware-level isolation. (It's a self-contained Docker container and isn't tied to a specific TEE provider; the current deployment target is [EigenCompute](https://blog.eigencloud.xyz/eigencloud-brings-verifiable-ai-to-mass-market-with-eigenai-and-eigencompute-launches/).) Under attestation, the TEE guarantees that the attested code executed on the attested inputs, and that the signing key never left the enclave. This is the foundation of [Astral Location Services](/concepts/astral-location-services), our hosted service. (Continuous attested operation is not yet funded — see the [trust model](/trust-model/what-you-are-trusting) for current status.)
**Zero-knowledge circuits (research).** ZK proofs would allow verification of correct computation without any trusted hardware — a verifier could confirm the result was computed correctly without re-executing the computation or trusting a TEE manufacturer. This is an active research direction, not yet implemented.
**Smart contract verification (limited).** Some spatial operations could theoretically run onchain, but gas costs and computational limitations make this impractical for most geospatial operations today.
The hosted TEE service that runs verification and computation
***
**See also:**
* [API Reference](/api-reference/overview) — full endpoint documentation
# GeoJSON
Source: https://docs.astral.global/concepts/geojson
Raw unsigned geospatial data — the simplest input format
**Research Preview** — APIs may change. [GitHub](https://github.com/AstralProtocol)
# GeoJSON
GeoJSON is raw, unsigned geospatial data. It's the simplest way to pass location data into Astral — and the least verifiable.
## The format
GeoJSON ([RFC 7946](https://datatracker.ietf.org/doc/html/rfc7946)) is a standard format for encoding geographic data structures. The full spec defines three object types — Geometry, Feature (geometry + properties), and FeatureCollection — but **Astral v0 works with bare Geometry objects only**, not Feature or FeatureCollection wrappers.
A Geometry object has two keys:
* **`type`** — the geometry type: `Point`, `LineString`, `Polygon`, `MultiPoint`, `MultiLineString`, `MultiPolygon`, or `GeometryCollection`
* **`coordinates`** — nested arrays of `[longitude, latitude]` positions
Geometries are built from coordinate arrays. A Point is a single `[lon, lat]` pair. A LineString is an array of positions. A Polygon is an array of linear rings — where each ring is an array of positions and the **first and last position must be identical** (closing the ring to form a valid area).
```json theme={null}
{
"type": "Polygon",
"coordinates": [[
[-122.42, 37.78],
[-122.42, 37.76],
[-122.40, 37.76],
[-122.40, 37.78],
[-122.42, 37.78]
]]
}
```
Astral APIs accept **bare Geometry objects** — pass the geometry directly, not wrapped in a Feature. If you're exporting from tools like geojson.io, extract the `geometry` field from the Feature before passing it to Astral.
## What GeoJSON does not provide
* **Attribution** — No record of who created it
* **Integrity** — No way to detect if it's been modified
* **Correspondence** — No evidence that it reflects reality
A signed result that uses raw GeoJSON as input proves "Astral computed the relationship between geometry A and geometry B" — but it does not prove who provided those geometries or whether they correspond to anything in the physical world.
For inputs where provenance matters, use [signed location records](/concepts/location-records) or [location proofs](/concepts/location-proofs).
## Coordinate conventions
| Convention | Value |
| -------------------- | ------------------------------------------------------- |
| **Format** | GeoJSON (RFC 7946) |
| **CRS** | WGS84 (`http://www.opengis.net/def/crs/OGC/1.3/CRS84`) |
| **Coordinate order** | `[longitude, latitude]` |
| **Altitude** | Optional third coordinate, meters above WGS84 ellipsoid |
Many mapping APIs use `[latitude, longitude]` order. GeoJSON uses `[longitude, latitude]`. Mixing these up is a common source of bugs — your point ends up in the wrong hemisphere.
## Tools for working with GeoJSON
* **[geojson.io](https://geojson.io)** — Draw, edit, and export GeoJSON on a map. The standard quick-start tool.
* **[Placemark Play](https://play.placemark.io)** — Open-source geodata editor with drawing and algorithmic operations (buffering, simplification).
* **[MapShaper](https://mapshaper.org)** — Simplify, convert, and inspect vector data. Useful for reducing file size and format conversion.
Signed, verifiable location data
***
**See also:**
* [API: Types — Input](/api-reference/types) — how raw GeoJSON is passed to API endpoints
# Location Claims
Source: https://docs.astral.global/concepts/location-claims
Assertions about the timing and location of an event
**Research Preview** — APIs may change. [GitHub](https://github.com/AstralProtocol)
# Location Claims
A location claim is an assertion about where and when an event occurred. It's the "I was here" statement that [location stamps](/concepts/location-stamps) provide evidence for or against.
## What a Location Claim Contains
A location claim extends the [Location Protocol v0.2](https://github.com/DecentralizedGeo/location-protocol-spec/tree/v0.2-draft) specification with verification-specific fields:
**Asserted location.** The location being claimed — a GeoJSON geometry with a spatial reference system. This is where the subject claims the event occurred.
**Spatial uncertainty (radius).** How precisely the claim defines the location, in meters. This field is required — you cannot claim presence at an exact point. Both physical measurement and honest reporting involve uncertainty, and pretending otherwise produces misleading verification results.
**Temporal bounds.** A time range (start and end) during which the event is claimed to have occurred. Like spatial uncertainty, temporal bounds acknowledge that events have duration.
**Subject.** Who or what was at the location. This could be a person (identified by an Ethereum address, DID, or other scheme), a device, an asset, or an organization.
**Event type.** What kind of event is being claimed. The event could be:
* **Presence** — a person or device was at a location
* **Transaction origin** — a transaction originated from a location
* **Asset location** — a physical asset was at a location
* **Delivery** — a delivery occurred at a location
## The Uncertainty Tradeoff
Location claims involve a fundamental tension between precision and confidence:
* "Somewhere in California during 2024" — easy to verify with high confidence, but not very useful for many use cases
* "Within 10 meters at 14:32:07" — precise and useful, but harder to verify confidently
Broader claims are easier to verify because the evidence just needs to fall anywhere within a larger target. Narrower claims require more precise evidence. Applications decide what precision/confidence balance they need.
This tradeoff also has privacy implications. Broader spatial and temporal bounds reveal less about exact location, which can be a feature rather than a limitation for privacy-sensitive applications.
Ultimately, spatial and temporal precision, forgery cost, privacy, latency and other factors are application-specific — different combinations of location stamps will be useful in different contexts.
## Relationship to Location Stamps
A location claim is an assertion. A [location stamp](/concepts/location-stamps) is evidence. They use the same Location Protocol format for their location data, but represent different things:
| | Location claim | Location stamp |
| -------------- | ------------------------------------------------ | --------------------------------------------------- |
| **Location** | Asserted — where the subject claims to have been | Observed — where evidence indicates the subject was |
| **Created by** | The claimant | A proof-of-location system |
| **Purpose** | States what needs to be verified | Provides evidence for verification |
[Evaluation](/concepts/location-proof-evaluation) compares the two: does the observed location (from location stamps) support the asserted location (from the location claim)?
Bundling location stamps with a location claim
***
**See also:**
* [API: Types](/api-reference/types) — LocationClaim type reference
# Overview
Source: https://docs.astral.global/concepts/location-data
Spatial data in Astral — from raw coordinates to verifiable records
**Research Preview** — APIs may change. [GitHub](https://github.com/AstralProtocol)
# Location Data
Location data is what Astral operates on. Every API call starts with spatial data — points, polygons, lines, or references to previously recorded locations. But not all location data is created equal. The provenance of a piece of location data determines how much trust you can place in it.
## The Verifiability Spectrum
Astral works with location data at three levels of verifiability:
| Level | What it is | What you know |
| -------------------------- | ---------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
| **Raw GeoJSON** | Unsigned geospatial data | The vector geometry itself — nothing about who created it or whether it's been tampered with |
| **Signed location record** | EAS attestation with cryptographic signature | Who attested to this location, plus assurances that data hasn't been changed |
| **Location proof** | Signed location record + evidence from proof-of-location systems | All of the above, plus independently evaluable evidence about a claim about the location of an event |
Each level adds verifiability. Raw GeoJSON is fine for some use cases; signed location records add attribution and integrity. Location proofs add evidence of physical correspondence — the hardest part.
Astral doesn't take a stance on what's trustworthy — that's not our role. Instead, we provide a system to organize, harmonize, and evaluate location data and evidence — to make spatial decision parameters legible – so that applications can make informed, risk-appropriate choices about what to trust and how much.
**v0 supports vector geospatial data only.** Raster support (GeoTIFFs, imagery analysis) is planned.
## Geospatial data sources
Location data comes from many sources, each with different properties:
* **Device sensors** — GPS, Wi-Fi, cellular positioning. Ubiquitous but often spoofable.
* **Network infrastructure** — Cell tower triangulation, IP geolocation, internet latency measurements. Coarse but hard to manipulate without network access.
* **Hardware attestation** — Secure enclave readings, hardware keystores. Harder to forge but still device-dependent.
* **Institutional records** — Land registries, shipping manifests, IoT telemetry. Trusted because of the institution, not cryptography.
* **Reference data** — Official boundaries, standard geometries, public datasets. Trusted because they're publicly auditable.
No single source is sufficient for high-stakes applications. [Location proofs](/concepts/location-proofs) address this through defense-in-depth: combining evidence from multiple independent sources to raise the cost of forgery.
## Coordinate system
All location data in v0 of the Astral Protocol uses [GeoJSON](https://datatracker.ietf.org/doc/html/rfc7946) as the geometry format. Coordinates follow the GeoJSON standard:
* **Default Coordinate Reference System (CRS):** WGS84 (`http://www.opengis.net/def/crs/OGC/1.3/CRS84`)
* **Coordinate order:** `[longitude, latitude]` — note that this is the opposite of what many mapping APIs use (a common challenge in GIS systems — [here's why](https://macwright.com/lonlat/).)
Raw unsigned geospatial data
***
**See also:**
* [SDK: Location module](/sdk/location) — creating and fetching location records
* [API: Types](/api-reference/types) — input format reference
# Location Proof Evaluation
Source: https://docs.astral.global/concepts/location-proof-evaluation
How Astral assesses the credibility of a location proof
**Research Preview** — APIs may change. [GitHub](https://github.com/AstralProtocol)
# Location Proof Evaluation
When a [location proof](/concepts/location-proof-structure) is submitted for verification, Astral evaluates it through a three-phase process. The output is a **credibility vector** — a structured assessment describing how strongly the evidence supports the claim across multiple dimensions.
Why not a binary yes/no? Different applications value different properties — a logistics platform cares most about spatial precision, while a compliance system may prioritize source independence. The credibility vector provides a structured, multidimensional quantification of the evidence so applications can apply their own weighting.
## The Three Evaluation Phases
### Phase 1: Stamp Checks
Each [location stamp](/concepts/location-stamps) is evaluated independently for internal validity:
* **Signatures** — Are the cryptographic signatures valid? Do they chain to a trusted source for this [proof-of-location system](/concepts/pol-systems)?
* **Structure** — Does the location stamp conform to the expected format for its [plugin](/plugins/overview)?
* **Signal consistency** — Are the raw signals internally consistent? Do multiple sensor readings within a single location stamp agree with each other?
A location stamp that fails these checks provides no useful evidence — its results are reported but won't contribute positively to the overall credibility.
### Phase 2: Correlation Checks (Multi-Stamp Location Proofs)
For location proofs with multiple location stamps from independent proof-of-location systems, the evaluator cross-correlates the evidence:
* **Independence** — Are the location stamps from genuinely independent sources? Two location stamps from the same proof-of-location system or the same device aren't independent. Independence is assessed based on plugin type, device identity, and trust model.
* **Agreement** — Do the independent location stamps agree on location and timing? Evidence from unrelated sources that converges on the same location is substantially more convincing than any single source.
This phase is what makes multi-factor location proofs powerful. Agreement between independent sources is hard to forge because an attacker would need to compromise multiple unrelated systems simultaneously.
### Phase 3: Claim Assessment
The evaluator compares the observed evidence (from the location stamps) against the asserted claim:
* **Spatial consistency** — Does the observed location from the location stamps fall within the claimed location and radius?
* **Temporal consistency** — Does the temporal footprint of the evidence overlap with the claimed time range?
* **Overall support** — Given the strength of the evidence (phases 1 and 2), how well does it support this specific claim?
## The Credibility Vector
**The structure of the credibility vector is an open research question.** The dimensions, metrics, and naming shown below are a working proposal under active development — not a finalized schema. Expect them to change. Treat the example below as illustrative, and don't hard-code dependencies on a fixed structure yet.
The output of evaluation is a credibility vector — currently modeled with four dimensions, each a structured object containing multiple metrics rather than a single score.
| Dimension | What it measures | Example metrics |
| ---------------- | ------------------------------------------------------------ | -------------------------------------------------------------------------------------- |
| **Spatial** | How well observed locations support the claimed location | Mean distance from claim center, fraction of location stamps within claimed radius |
| **Temporal** | How well observation times align with the claimed time range | Mean overlap between location stamp and claim time windows, fraction with full overlap |
| **Validity** | Internal validity of the location stamps | Fraction with valid signatures, valid structure, consistent signals |
| **Independence** | How independent and corroborating the evidence sources are | Ratio of unique plugins to total location stamps, spatial agreement across sources |
There is no single overall score. This is deliberate — collapsing a multidimensional assessment into one number requires value judgments about which dimensions matter most, and that's the application's call, not ours.
These dimensions and their constituent metrics are an active area of [research](https://github.com/AstralProtocol/research). We expect them to evolve as we learn more about what's useful in practice.
## Interpreting the Vector
The credibility vector quantifies the strength of the evidence, not the probability that the claim is true. Strong metrics across all four dimensions mean the evidence is internally valid, spatially and temporally consistent with the claim, and drawn from independent sources. Weak metrics tell you *where* the evidence falls short.
What the vector cannot tell you: whether the proof-of-location systems themselves are trustworthy for your use case. A credibility vector with strong metrics from a single device attestation reflects different underlying assurance than one with equally strong metrics from three independent proof-of-location systems. The per-dimension breakdown — especially independence — makes this visible.
## Application-Level Decisions
Astral evaluates and reports. Applications decide.
The credibility vector gives applications enough information to make risk-appropriate decisions. A social check-in app might accept weak independence metrics. A compliance system might require strong validity and spatial metrics from at least two independent sources. A land title registry might require the strongest available assurance across all dimensions.
The threshold is always the application's choice — Astral does not impose minimum requirements.
Spatial operations with proof of correct execution
***
**See also:**
* [SDK: Location proofs](/sdk/location-proofs) — verifying location proofs programmatically
* [API: Verify proof](/api-reference/verify/proof) — verification endpoint reference
* [API: Types](/api-reference/types) — CredibilityVector type reference
# Composing Location Proofs
Source: https://docs.astral.global/concepts/location-proof-structure
Bundling location stamps with a location claim into a verifiable artifact
**Research Preview** — APIs may change. [GitHub](https://github.com/AstralProtocol)
# Composing Location Proofs
A location proof bundles one or more [location stamps](/concepts/location-stamps) with a [location claim](/concepts/location-claims). It's the verifiable artifact — a claim paired with its supporting evidence.
## Structure
A location proof contains:
* **One location claim** — the assertion being made (where, when, who, what event)
* **One or more location stamps** — evidence from proof-of-location systems
That's it. The simplicity is intentional. A location proof is a container that brings together the assertion and the evidence so they can be [evaluated](/concepts/location-proof-evaluation) together.
## Single-Stamp vs. Multi-Stamp Location Proofs
A location proof with a single location stamp is valid. It represents evidence from one proof-of-location system supporting one location claim. The evaluation can still assess the location stamp's internal validity and how well it supports the claim.
Multi-stamp location proofs from independent proof-of-location systems are stronger. When evidence from unrelated sources agrees, the cost of forgery rises sharply — an attacker would need to simultaneously compromise multiple independent systems. This is where cross-correlation adds value: the evaluation assesses how independent the evidence sources actually are.
Redundant location stamps from the *same* proof-of-location system don't meaningfully increase confidence. Cross-correlation looks for independence between sources.
We are building a taxonomy of proof-of-location systems and quantifying these dimensions as an area of active [research](https://github.com/AstralProtocol/research).
## Location Proof Plugins
**Location proof plugins** are the abstraction layer that makes composition practical. Each plugin wraps a [proof-of-location system](/concepts/pol-systems) behind a common interface with five standard methods:
1. **Collect** — Gather raw signals from the proof-of-location system
2. **Create** — Process signals into an unsigned location stamp
3. **Sign** — Cryptographically sign the location stamp
4. **Verify** — Check a location stamp's internal validity
5. **Evaluate** — Assess how well a location stamp supports a location claim
This common interface means a developer can compose evidence from multiple proof-of-location systems — and any future system — using the same SDK patterns. The plugin handles the system-specific details; the SDK handles composition and orchestration.
Plugins and their status:
| Plugin | Proof-of-location system | Environment | Status |
| ------------------------------------- | ------------------------------------ | ------------------------------------- | ------------------------------------------------ |
| [ProofMode](/plugins/proofmode) | Device attestation + sensor fusion | iOS, Android, React Native | Working — verification implemented (alpha) |
| [WitnessChain](/plugins/witnesschain) | Infrastructure latency triangulation | Node.js, any HTTP client | Experimental — interface defined, early verifier |
| [Mock](/plugins/mock) | Configurable test evidence | Any JavaScript/TypeScript environment | Available for testing (client) |
The Verify service also includes experimental stamp-verification logic for `gpsd`, `geoclue`, `wifi-mls`, and `ip-geolocation`.
Different devices and environments suit different plugins. A mobile app might use ProofMode for on-device attestation, while a backend service could use a network-based source like WitnessChain. ProofMode is working today; the others are experimental with interfaces defined — we're keen to develop new ones with partners. See [Plugins](/plugins/overview) for details on each.
## Composing From Multiple Plugins
To create a multi-stamp location proof, collect location stamps from each plugin independently, then bundle them with a single location claim. The location stamps don't need to know about each other — they just provide independent evidence that the evaluation process cross-correlates.
How Astral assesses the credibility of a location proof
***
**See also:**
* [SDK: Location proofs](/sdk/location-proofs) — creating and composing location proofs
* [Plugins overview](/plugins/overview) — the full plugin ecosystem
* [Build a custom plugin](/plugins/custom) — implementing the plugin interface
# Overview
Source: https://docs.astral.global/concepts/location-proofs
Composable, multi-factor evidence that something was somewhere
**Research Preview** — APIs may change. [GitHub](https://github.com/AstralProtocol)
# Location Proofs
A location proof couples a **location claim** with **location evidence** supporting that claim. It provides independently evaluable evidence about how well the claim corresponds to physical reality — not a guarantee that it does.
A location proof is a verifiable digital artifact that represents a claim about the timing and position of some event, plus one or more pieces of corroborating evidence that would require technical manipulation, collusion, or fraud to forge.
The key aim is to **raise the cost of lying about where things happen**, so we can build more interesting applications on more credible spatial facts.
Astral's Location Proof Framework provides a library of [location proof plugins](/plugins/overview) that collect evidence from different systems and compose them in a way that can be parsed and verified. We are building to support the widest possible range of ways to verify location — we don't want to make assumptions about what forms of evidence system designers will or won't accept.
One of the framework's central ideas is composability: multi-factor location proofs that bundle evidence collected from independent [proof-of-location systems](/concepts/pol-systems) into a single verifiable artifact.
## Why Location Proofs Matter
Most location data today is self-reported. A device says "I'm at these coordinates" and the receiving system takes it on faith. This works until the stakes are high enough to justify forgery — and the cost of forging GPS coordinates is essentially zero.
Location proofs change the economics. By requiring evidence from independent proof-of-location systems, they raise the cost of forgery. The goal is not absolute certainty (which we believe is not achievable for physical location), but making the cost of a convincing forgery exceed the value of the fraud.
## The Certainty Spectrum
Not all location proofs are equally strong. The level of assurance depends on the number, diversity, and quality of the evidence sources.
To reason about the relative security of location proofs, we are developing a 5-level **certainty spectrum**:
| Level | Description |
| ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **L0** | No evidence — bare self-reported claim |
| **L1** | One externally sourced piece of evidence (e.g. IP-based or single-sensor) |
| **L2** | One substantive strategy or 2–3 lighter-weight independent sources |
| **L3** | Multiple independent sources, at least one substantive |
| **L4** | Diverse high-integrity signals with strong cryptographic backing and cross-correlation |
| **L5** | Resistance to well-resourced (e.g. nation-state-level) adversaries — advanced cryptographic proofs with multiple cross-domain endorsements (research frontier; aspirational) |
Most real-world applications today operate at L1 or L2. Higher levels require proof-of-location systems that don't yet exist at scale — this is an active area of research.
## Design Principles
**No one-size-fits-all.** Different applications need different levels of assurance. A social check-in app has different requirements than a land title registry. Location proofs are designed to be composable so applications can choose the right level for their use case.
**Neutral framework.** Astral provides the structure for collecting, composing, and evaluating location evidence — not opinions about which evidence sources to trust. Some applications will accept government-issued attestations as sufficient; others will give them no weight. The framework accommodates both without preference.
**Probabilistic, not deterministic.** Location proofs produce credibility scores, not binary verdicts. The evidence either supports the claim strongly, weakly, or not at all — and applications decide what threshold they need.
**Compose for the use case.** A single location stamp from one proof-of-location system coupled with a claim is a valid location proof. Multiple location stamps from independent systems are stronger. The architecture supports both without requiring a minimum.
**Information monotonicity.** Adding valid, independent evidence may push confidence in the claim up or down, but it should always sharpen the picture: you end up more sure of where the claim really stands, even when that turns out to be unfavorable. More evidence should never leave you more confused.
**Verifiability throughout.** Location stamps should be cryptographically signed, evidence bundles should maintain clear provenance, and evaluation functions should be identifiable. Every layer of the location proof should be independently checkable.
**Forgery cost principle.** The cost of forging a location proof should exceed the economic value of the transaction it underpins. A \$10 check-in reward needs less forgery resistance than a \$10M land title transfer.
## The Location Proof Pipeline
Location proofs are built from a series of concepts that compose together:
1. **[Proof-of-location systems](/concepts/pol-systems)** — The technical and social systems that produce location evidence
2. **[Location stamps](/concepts/location-stamps)** — Individual pieces of evidence from a single proof-of-location system
3. **[Location claims](/concepts/location-claims)** — Assertions about where and when an event occurred
4. **[Composing location proofs](/concepts/location-proof-structure)** — Bundling location stamps with a location claim into a verifiable artifact
5. **[Location proof evaluation](/concepts/location-proof-evaluation)** — How Astral assesses the credibility of a location proof
Each of the following pages covers one of these concepts in detail.
The technical and social systems that produce location evidence
***
**See also:**
* [SDK: Location proofs](/sdk/location-proofs) — creating, signing, and verifying location proofs
* [Plugins overview](/plugins/overview) — the location proof plugin ecosystem
# Location Records
Source: https://docs.astral.global/concepts/location-records
Signed, verifiable location data with attribution and integrity
**Research Preview** — APIs may change. [GitHub](https://github.com/AstralProtocol)
# Location records
A location record is signed, verifiable location data artifact. It takes raw geospatial data and adds what [GeoJSON](/concepts/geojson) alone cannot provide: attribution (who created it) and integrity (proof it hasn't been tampered with).
In the Astral ecosystem, location records conform to v0.2 of the [Location Protocol](https://spec.decentralizedgeo.org) specification. The Location Protocol provides a lightweight data schema that wraps spatial data in any format, to improve interoperability so that spatial data can be parsed and interpreted across distributed systems.
## What signing adds
| Property | Raw GeoJSON | Signed location record |
| ------------------------ | --------------------------------- | --------------------------------------------------------------------------------------------------- |
| **Geometry** | Yes | Yes |
| **Attribution** | No — anyone could have created it | Yes — cryptographic signature ties it to a specific identity |
| **Integrity** | No — could be modified silently | Yes — any modification invalidates the signature |
| **Verifiable timestamp** | No | Sometimes — if the attestation was anchored or registered on a blockchain or other timestamp server |
| **Correspondence** | No | No — signing proves who *claimed* this location, not that they *were* there |
The last row is important. A signed location record proves that a specific identity attested to a specific location at a specific time. It does not prove the attester was physically present. That's what [location proofs](/concepts/location-proofs) add.
We use the term "correspondence" to refer to how well a digital spatial data record *corresponds* to physical reality.
## EAS attestations
Astral's v0 implementation uses [Ethereum Attestation Service (EAS)](https://attest.org/) for signed location records. EAS provides:
* **Onchain attestations** — stored directly on EAS contracts, referenced by UID (unique identifier), permanent and immutable
* **Offchain attestations** — stored on user devices, centralized servers, IPFS or other infrastructure, referenced by UID + URI, no gas cost, EIP-712 signed
Both carry the same cryptographic guarantees. The difference is storage: onchain attestations live on the blockchain; offchain attestations live wherever you put them but are still cryptographically verifiable.
Other implementations — for example, on ATProto — are in development, and we plan to add support in time.
## Location Protocol v0.2 schema
Location records conform to the [Location Protocol v0.2](https://github.com/DecentralizedGeo/location-protocol-spec/tree/v0.2-draft) schema:
| Field | Type | Description |
| --------------- | ------ | --------------------------------------- |
| `lp_version` | string | Protocol version (e.g., "0.2") |
| `location_type` | string | Type of location data (e.g., "GeoJSON") |
| `location` | bytes | Encoded location data |
| `srs` | string | Spatial reference system as OGC URI |
We are actively verifying that deployed schemas conform to Location Protocol v0.2. There may be inconsistencies between the documentation, deployed schemas, and the spec. See [GitHub issue #11](https://github.com/AstralProtocol/astral-location-services/issues/11) for status.
## Storage options
* Stored on EAS contracts
* Referenced by chain ID + UID
* Higher gas cost
* Permanent, immutable
* Held on user devices or stored on IPFS, servers, etc.
* Referenced by UID + URI
* No gas cost to create
* EIP-712 signed
For offchain attestations, the UID is deterministically derived from the attestation data. Even when fetching from HTTPS (not content-addressed), Astral verifies that the fetched attestation produces the expected UID. Mismatch means rejection.
Adding evidence of physical correspondence
***
**See also:**
* [SDK: Location module](/sdk/location) — creating, fetching, and querying location records
* [API: Types](/api-reference/types) — input format reference
# Location Stamps
Source: https://docs.astral.global/concepts/location-stamps
Evidence from a single proof-of-location system about an observed location
**Research Preview** — APIs may change. [GitHub](https://github.com/AstralProtocol)
# Location Stamps
A location stamp is a signed piece of evidence from a single [proof-of-location system](/concepts/pol-systems) about an observed location. It's the atomic unit of location evidence in Astral — the building block from which the [location evidence](/concepts/location-proof-structure) component of a location proof is composed.
## Location Stamp Contents
Conceptually, a location stamp carries four things:
**Observed location.** Where the proof-of-location system's evidence indicates the subject was. This uses the same [Location Protocol v0.2](https://github.com/DecentralizedGeo/location-protocol-spec/tree/v0.2-draft) format as location records — a geometry with a spatial reference system.
**Temporal footprint.** When the observation was made. Both location stamps and location claims use time ranges (start and end), not single instants — observations take time, and evidence may span a window.
**Plugin identification.** Which [location proof plugin](/concepts/location-proof-structure#location-proof-plugins) + version was used to create this evidence. This is necessary for verification — the verifier needs to know what kind of evidence to expect and how to evaluate it.
**Signals and signatures.** The raw evidence data (signals) collected by the proof-of-location system, plus cryptographic signatures that bind the evidence to its source. The signals are plugin-specific — each proof-of-location system produces different kinds of raw data (sensor readings, latency measurements, attestation tokens, etc.).
## Independence From Claims
This is a key design decision. A location stamp says "here is evidence about an observed location" — it does not say "this evidence supports a particular claim." The separation matters because:
* The same location stamp could be evaluated against different location claims
* Location stamps can be collected before a location claim is formulated
* Verification can assess each location stamp on its own merits before comparing it to the claim
The connection between location stamps and location claims happens at the [location proof](/concepts/location-proof-structure) level, where they're bundled together. The [evaluation](/concepts/location-proof-evaluation) process then assesses whether the observed evidence (from location stamps) support the asserted location (from the location claim).
## Internal validity
Each location stamp carries enough information to verify its own internal validity — independent of any location claim. Stamp verification checks:
* **Signatures** — Are the cryptographic signatures valid? Do they chain back to a trusted source?
* **Structure** — Does the location stamp conform to the expected format for its plugin?
* **Signal consistency** — Are the raw signals internally consistent? (e.g., do multiple sensor readings agree?)
This is the first phase of [location proof evaluation](/concepts/location-proof-evaluation) — checking each location stamp before assessing how well it supports the claim.
Assertions about where and when an event occurred
***
**See also:**
* [SDK: Stamps](/sdk/stamps) — collecting, creating, and signing location stamps
* [API: Verify stamp](/api-reference/verify/stamp) — verifying individual location stamps
* [API: Types](/api-reference/types) — LocationStamp type reference
# Proof-of-Location Systems
Source: https://docs.astral.global/concepts/pol-systems
Strategies and technologies for producing location evidence
**Research Preview** — APIs may change. [GitHub](https://github.com/AstralProtocol)
# Proof-of-Location Systems
A proof-of-location system is any system that produces evidence about physical location. These systems are the foundation of location proofs — each [location stamp](/concepts/location-stamps) comes from a proof-of-location system, wrapped in a [location proof plugin](/concepts/location-proof-structure#location-proof-plugins).
Location verification is already everywhere online, but it's usually ad hoc and hard to check: GeoIP lookups, scanning a QR code, entering a passphrase, checking in with an event host, even submitting a bank statement as proof of address. The Location Proof framework is an attempt to make these techniques more transparent and legible — not to replace them.
## Categories of proof-of-location systems
Proof-of-location systems span a wide range of approaches, from hardware-based measurement to social attestation:
| Category | Mechanism | Example | Strengths | Limitations |
| ---------------------- | ------------------------------------------------------- | ---------------------------------------------- | --------------------------------------------------- | ------------------------------------------------------------------------------ |
| **Near-field machine** | Physical proximity verification via short-range signals | RFID, NFC, Bluetooth beacons | Hard to forge without physical presence | Short range; requires infrastructure |
| **Network machine** | Position derived from network measurements | Time of Flight, TDOA, latency triangulation | Independent of device; based on physics | Requires distributed infrastructure |
| **Sensor data** | Location inferred from environmental readings | Magnetometer signatures, image/audio analysis | Rich contextual evidence | Computationally expensive to verify, difficult to detect generated sensor data |
| **Delegated** | Trusted third party attests to location | Notarized presence, institutional witness | Leverages existing trust relationships | Only as trustworthy as the delegate |
| **Social** | Peer confirmations of co-location | Mutual attestation, group check-in | Distributed trust; no infrastructure needed | Collusion risk |
| **Authority-based** | Authorized entity confirms location | Government agency, licensed surveyor | High institutional trust | Centralized; requires authority access |
| **Legal** | Location established through legal process | Court records, notarized documents, affidavits | Strong evidentiary weight, legal liability of fraud | Slow; expensive; not real-time |
## Current state of the field
Honesty matters here: few "hard" proof-of-location systems exist at scale today, and even fewer are decentralized. Most deployed location infrastructure (GPS, Wi-Fi positioning, cell tower triangulation) was designed for navigation, not proof. These systems tell you where you are but don't produce cryptographically verifiable evidence that you were there.
The proof-of-location systems that do exist with meaningful cryptographic properties — hardware attestation, network latency triangulation, secure enclave readings, [Galileo's OSNMA authentication feature](https://www.gsc-europa.eu/galileo/services/galileo-open-service-navigation-message-authentication-osnma) — are still maturing. Each has real limitations and known attack vectors.
That said, significant value comes from "softer" proof-of-location systems too. Even a single device attestation with sensor readings, while not unbreakable, raises the cost of forgery substantially compared to self-reported GPS. And combining multiple independent sources — even individually weak ones — creates meaningful assurance through cross-correlation. Our vision is to build a community ecosystem of location proof plugins, and over time enhance our capability to create location proofs further and further up the certainty spectrum.
## Available plugins
v0 of Astral connects to proof-of-location systems through [location proof plugins](/plugins/overview). **ProofMode is working today** — its stamps can be verified end to end. The other plugins are experimental, with interfaces defined and early verification logic in place. The two highlighted below are the documented examples:
Device attestation + sensor fusion. Uses iOS Secure Enclave and Android hardware keystore to attest to device sensor readings (GPS, Wi-Fi, cellular, magnetometer). Trust derives from device hardware integrity. This is the one proof-of-location plugin available today (alpha).
Infrastructure verification. The design uses UDP latency triangulation across a distributed challenger network, where trust derives from the speed of light — you can't fake being close to many geographically distributed nodes at once. The interface is defined and an experimental server-side verification logic exists; it's not production yet.
The Verify service also includes experimental stamp-verification logic for `gpsd`, `geoclue`, `wifi-mls`, and `ip-geolocation`. The [Mock plugin](/plugins/mock) is available for development and testing on the client.
ProofMode is working today; the others are experimental, with interfaces defined and early verification logic in place. We're actively interested in developing new proof-of-location plugins with partners — if you work on a proof-of-location system, [get in touch](mailto:contact@astral.global).
## Building new plugins
The location proof plugin interface is extensible by design. If you have a proof-of-location system that isn't covered by existing plugins, you can [build a custom plugin](/plugins/custom) that implements the standard interface.
Evidence from a single proof-of-location system
***
**See also:**
* [Plugins overview](/plugins/overview) — the full plugin ecosystem
* [ProofMode plugin](/plugins/proofmode) — device attestation details
* [WitnessChain plugin](/plugins/witnesschain) — network verification details
# Privacy
Source: https://docs.astral.global/concepts/privacy
Privacy properties of verification and computation
**Research Preview** — APIs may change. [GitHub](https://github.com/AstralProtocol)
# Privacy
Privacy is a core design goal for Astral — but almost all of it is ahead of us, not behind us. The current hosted service does **not** run in a TEE and does **no** input encryption: it receives inputs as plaintext, and signed results can carry those inputs back in plaintext (see below). So Astral does not provide input or output privacy today. (Astral *has* run the service on real TEE hardware in test deployments — which isolates process memory from the host operator — but does not operate one continuously, and a TEE on its own would not add the privacy affordances below.) Private inputs and private/shielded outputs are on our **near-term development roadmap**; continuous attested operation and zero-knowledge approaches are the directions we're pursuing. See the [trust model](/trust-model/what-you-are-trusting) for current deployment status.
## What the TEE does and doesn't protect
**The current hosted service does not run in a TEE, and nothing in the request path is encrypted.** It receives plaintext JSON over HTTPS, computes in an ordinary process (and PostGIS), and — for proof inputs — echoes inputs back in the response. Astral has run the service on real TEE hardware in test deployments (which would provide the process isolation below), but does not operate one continuously. See the [trust model](/trust-model/what-you-are-trusting).
A TEE provides one specific thing: **process isolation**. Running the service inside an attested enclave would stop the *host operator* from inspecting the running process's memory. That's real, but narrow — on its own it would **not** encrypt your inputs end-to-end (there is no client-to-enclave encryption in the design; inputs arrive as plaintext), stop the service from returning your inputs in the response, or hide the reference geometry and evaluation function. Those need the privacy affordances we're [designing](#privacy-modes-were-designing), not just a TEE.
**What an attested deployment is *intended* to keep from the host operator** (a goal, not a guarantee today): raw input coordinates, exact geometries, and location stamp signals would live only in enclave memory during computation.
**What the service returns today:** the signed result (a boolean, numeric value, or credibility vector), the operation type, and input references (`inputRefs` — hashes or UIDs). The service is stateless and does not persist or log raw inputs — but "not persisted" is not the same as "private."
However, in v0 the signed results **do include input data in plaintext**. Compute results can carry the full location claim and credibility vector via `proofInputs`, and verified location proofs include the original proof with all stamps and claim data. This means anyone who receives a signed result can see the raw location inputs.
v0 does not strip input data from signed results. A privacy-preserving output mode — returning only the answer, operation type, and hashed input references — is planned. See [astral-location-services#57](https://github.com/AstralProtocol/astral-location-services/issues/57) for progress.
## Privacy modes we're designing
To be clear: Astral does not offer private inputs or private outputs as features today. They're on our **near-term development roadmap** — the design feels feasible on this architecture, and we'd welcome input from anyone who needs them:
* **Private input coordinates** — encrypted lat/lng, decrypted only inside the enclave and never echoed in the result.
* **Private reference geometries** — keep the comparison geometry (a geofence or boundary) hidden, so a `contains`/`within` check doesn't reveal it.
* **Private evaluation functions** — keep a verifier's evaluation/weighting logic confidential.
* **Shielded outputs** — return a policy decision (trigger / don't trigger) carrying no identifying detail about who or where.
* **Encrypted outputs** — results encrypted so only a specified counterparty can read them.
The output-stripping piece is tracked in [astral-location-services#57](https://github.com/AstralProtocol/astral-location-services/issues/57); the broader design is collected in [astral-location-services#65](https://github.com/AstralProtocol/astral-location-services/issues/65). If any of this matters to you, [get in touch](mailto:contact@astral.global).
## Information Leakage From Results
The result itself may reveal information about the inputs. This is inherent to the computation, not a limitation of the privacy model:
| Operation | What the result reveals |
| ------------------------ | ------------------------------------------- |
| `contains` (true) | The point is somewhere inside the polygon |
| `within` (true, 500m) | The point is within 500m of the target |
| `distance` (exact value) | The precise distance between two geometries |
More specific operations leak more. A `contains` check against a country-sized polygon reveals less than a `within` check with a 10-meter radius.
## Spatial and Temporal Uncertainty as Privacy Tools
The [uncertainty tradeoff](/concepts/location-claims#the-uncertainty-tradeoff) in location claims has a privacy dimension. Broader spatial bounds (larger radius) and wider temporal bounds reveal less about exact location and timing. Applications that want to preserve user privacy can intentionally use coarser claims — "was this user in San Francisco sometime today?" rather than "was this user within 5m 37.7749°N 122.4194°W at 14:32:07?"
This isn't a hack — it's a principled privacy-preserving approach. If the application only needs to know "roughly where, roughly when," there's no reason to collect or process exact coordinates.
## ZK Location Proofs (Research)
Zero-knowledge proofs would allow verification of location claims without revealing the underlying location data to anyone — including the verifier. A ZK location proof could prove "I was inside this boundary" without revealing where inside the boundary, or even what the boundary was.
| Property | TEE (attested deployment) | ZK (future) |
| ------------------------------------ | ---------------------------------------------------------- | ------------------------- |
| Raw inputs hidden from host operator | Only under a real attested deployment — **not live today** | Yes |
| Raw inputs hidden from verifier | No | Yes |
| Inputs encrypted client-to-enclave | No (not in the design) | Yes |
| No trusted hardware required | No | Yes |
| Verification without re-execution | No | Yes |
| Maturity | Not live — demonstrated in test deployments only | Not yet — active research |
The hard part isn't only the geometry. ZK *computational geometry* circuits — polygon containment, distance — are expensive but tractable; [zkMaps](https://github.com/zkMaps/zkMaps), a project Astral has supported, has done some benchmarking here. But ZK geometry on its own isn't very useful: a location proof's value comes from *evaluating evidence*, not from computing a bare predicate. The frontier we care about is **ZK evaluation functions combined with ZK computational geometry** — proving privately that a credible body of evidence supports a claim. That's research, not a current capability.
## TEE Limitations
The TEE provides strong but not absolute privacy:
* **Hardware trust** — You are trusting that the TEE hardware (Intel SGX / AMD SEV) correctly isolates the enclave. Side-channel attacks on TEEs are an active area of security research.
* **Result leakage** — As described above, the result itself carries information about the inputs.
* **Input reference hashes** — Hashed input references are visible. If an observer knows the possible input space, they could attempt to match hashes (though this is computationally expensive for arbitrary geometries).
Walk through common workflows step by step
***
**See also:**
* [Trust model](/trust-model/architecture) — what's verified vs. what you're trusting
* [Astral Location Services](/concepts/astral-location-services) — TEE architecture details
# Signed Results
Source: https://docs.astral.global/concepts/signed-results
Output formats from verification and computation — and how to use them
**Research Preview** — APIs may change. [GitHub](https://github.com/AstralProtocol)
# Signed Results
Both the [Verify](/concepts/verify) and [Compute](/concepts/compute) endpoints return signed results — cryptographically signed records of what was computed, what inputs were used, and what the answer was. The signature comes from the Astral signing key, held inside the [TEE](/concepts/astral-location-services).
## What a Signed Result Proves
Under attestation (see deployment status below), a signed result is intended to prove three things:
1. **Correct execution** — The computation was performed by attested code inside the TEE
2. **Specific inputs** — The result references the exact inputs that were used (via hashes or UIDs)
3. **Authentic output** — The signing key that produced the signature is held inside the TEE and, under attestation, cannot be extracted by the operator
Any downstream consumer can verify these properties by checking the signature against the known Astral public key — without re-executing the computation or trusting an intermediary.
**Deployment status.** Astral has run these services on real TEE hardware in test deployments but does not currently fund continuous attested operation. Today a valid signature proves a key Astral controls produced the result; binding it to an independently attested enclave (point 1 above) is target-state. See [What you are trusting](/trust-model/what-you-are-trusting).
## Result Types
The signed result format depends on the operation:
| Operation type | Result schema | Result value |
| --------------------------------------------- | ------------- | ------------------------------------------ |
| **Predicates** (contains, within, intersects) | Boolean | `true` or `false` |
| **Measurements** (distance, area, length) | Numeric | Scaled integer (centimeters or cm²) |
| **Verification** | Credibility | Credibility vector with dimensional scores |
Each result also includes: input references (UIDs or hashes), a timestamp, the operation name, and the Astral signing key's signature.
In v0, signed results may also carry the full input data in plaintext (for example, the complete claim and stamps for a verification), not just hashed references. A privacy-preserving output mode that returns only the answer and hashed references is planned — see [Privacy](/concepts/privacy).
## Using Signed Results Offchain
A signed result is immediately usable in any application:
* **Agent workflows** — An autonomous agent branches on the spatial answer. The signed result is the audit trail.
* **Backend storage** — Store the signed result as evidence alongside the business action it triggered.
* **Compliance reports** — The signed result proves what was computed and when, with cryptographic backing.
* **Peer-to-peer** — Share the signed result with a counterparty who can verify it independently.
A verified location proof (from the verify endpoint) is valuable on its own — it doesn't need to flow into a compute operation. Many applications only need to know "how credible is this location claim?" and can act on that directly.
## Using Signed Results Onchain
Signed results can be submitted onchain via EAS (Ethereum Attestation Service) delegated attestations. The flow:
1. **Astral signs** the result inside the TEE
2. **The developer submits** the signed result onchain with Astral's signature (developer pays gas)
3. **EAS verifies** the signature and records Astral as the attester
4. **Resolver contracts** execute business logic based on the result
This is the **delegated attestation pattern** — Astral produces the attestation offchain, and the developer submits it when ready. The signed result includes a deadline for submission; after the deadline, the signature expires.
Resolver contracts can verify that the result came from Astral by checking `attestation.attester == astralSigner`, and can inspect the input references to confirm the expected locations were checked.
## Not Every Result Needs to Go Onchain
The onchain path is available but not required. Most applications will use signed results offchain — the cryptographic signature provides verifiability regardless of whether the result is submitted to a blockchain.
Privacy properties of the system
***
**See also:**
* [SDK: EAS module](/sdk/eas) — submitting delegated attestations onchain
* [SDK: Compute module](/sdk/compute) — working with compute results
* [API: Types](/api-reference/types) — result type schemas
# Verify
Source: https://docs.astral.global/concepts/verify
The verification endpoint — submit a location proof, get a verified location proof
**Research Preview** — APIs may change. [GitHub](https://github.com/AstralProtocol)
# Verify
The Verify service accepts a [location proof](/concepts/location-proof-structure) and returns a **verified location proof** — the original proof, a [credibility vector](/concepts/location-proof-evaluation#the-credibility-vector), and a signed result that can be recorded onchain as an EAS attestation. It's a hosted implementation of the [evaluation function](/concepts/location-proof-evaluation). This page describes *how the service works*; the evaluation page describes *what is checked and why*.
## How It Works
When a location proof is submitted to the verify endpoint:
1. **Plugin resolution** — The service identifies which [location proof plugins](/plugins/overview) produced each [location stamp](/concepts/location-stamps) in the proof and loads the appropriate verification logic.
2. **Stamp verification** — Each location stamp is verified independently: signature validation, structural checks, and signal consistency assessment. Verification is plugin-specific — each plugin knows how to check its own evidence.
3. **Cross-correlation** (multi-stamp location proofs) — For location proofs with multiple location stamps from independent proof-of-location systems, the service assesses independence and agreement across sources.
4. **Claim assessment** — The service evaluates how well the evidence from the location stamps supports the [location claim](/concepts/location-claims): spatial overlap, temporal overlap, and overall consistency.
5. **Credibility vector generation** — The per-stamp results, correlation analysis, and claim assessment are combined into a structured credibility vector. It currently spans spatial, temporal, validity, and independence dimensions, though [that structure is an open research question](/concepts/location-proof-evaluation#the-credibility-vector) and will change.
All of this is designed to happen inside the [TEE](/concepts/astral-location-services), so that — under attestation — the evaluation logic is attested and the signing key is protected. See the [trust model](/trust-model/what-you-are-trusting) for current deployment status.
## Two Endpoints
| Endpoint | Input | Output | Use case |
| ----------------------------------------------- | ------------------------------------ | ----------------------- | ------------------------------------------------------------------- |
| **[Verify proof](/api-reference/verify/proof)** | Full location proof (claim + stamps) | Verified location proof | Full evaluation — the primary verification path |
| **[Verify stamp](/api-reference/verify/stamp)** | Single location stamp | Stamp validity result | Quick check of a location stamp's internal validity without a claim |
The stamp endpoint is useful for confirming that a location stamp's signatures are valid and its structure is correct — without the overhead of claim assessment and cross-correlation.
## What Each Endpoint Returns
**Verify proof** returns a verified location proof containing:
* **The original proof** — the location claim and location stamps as submitted
* **Credibility vector** — structured assessment across (currently) spatial, temporal, validity, and independence dimensions, with per-stamp detail; the dimension structure is still evolving
* **Signed result** — the verification result signed by the TEE key, independently verifiable by any downstream consumer and optionally recorded onchain as an EAS attestation
**Verify stamp** returns a validity result: whether signatures, structure, and signals pass their checks.
A verified location proof is valuable on its own. It doesn't need to flow into a [Compute](/concepts/compute) operation — many applications only need to know "how credible is this location claim?" without asking any further spatial questions.
Geospatial operations inside the TEE
***
**See also:**
* [API: Verify proof](/api-reference/verify/proof) — endpoint reference
* [API: Verify stamp](/api-reference/verify/stamp) — stamp verification endpoint
* [SDK: Location proofs](/sdk/location-proofs) — programmatic verification
# Agent Integration
Source: https://docs.astral.global/guides/agent-integration
Using Astral as a spatial oracle in agent workflows
**Research Preview** — APIs may change. [GitHub](https://github.com/AstralProtocol)
# Agent integration
Autonomous agents need spatial reasoning — is this delivery within the geofence? How far is the nearest facility? Does this route intersect a restricted zone? Astral provides verifiable spatial answers that agents can use in decision-making.
The examples on this page pass **raw coordinates** to the compute API. A signed result proves the *computation* was done correctly on those coordinates — it does **not** prove the coordinates are truthful (raw GPS is spoofable, and an agent could supply any position). To make the input trustworthy too, feed in a verified [location proof](/concepts/location-proofs) rather than bare coordinates. Keep the distinction clear when an agent's decision carries real consequences.
## The pattern
The integration pattern is straightforward: the agent needs a spatial answer, calls Astral, gets a signed result, and uses the result in its decision logic.
```mermaid theme={null}
graph LR
A[Agent needs spatial answer] --> B[Calls Astral API]
B --> C[Gets signed result]
C --> D[Uses result in decision]
```
The signed result provides an audit trail. Anyone reviewing the agent's decisions can verify that the spatial answer was computed correctly on the stated inputs, rather than fabricated or hallucinated by the agent. (Whether those inputs are themselves truthful is a separate question — see the note above.)
## Raw HTTP from any framework
Astral is a REST API, so any language or framework that can make HTTP requests works. No SDK required.
### Python
```python theme={null}
import httpx
response = httpx.post("https://staging-api.astral.global/compute/v0/distance", json={
"from": {"type": "Point", "coordinates": [2.2945, 48.8584]},
"to": {"type": "Point", "coordinates": [2.3522, 48.8566]},
"chainId": 84532
})
result = response.json()
distance_meters = result["result"]
```
### TypeScript
```typescript theme={null}
const response = await fetch('https://staging-api.astral.global/compute/v0/distance', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
from: { type: 'Point', coordinates: [2.2945, 48.8584] },
to: { type: 'Point', coordinates: [2.3522, 48.8566] },
chainId: 84532
})
});
const result = await response.json();
const distanceMeters = result.result;
```
## Using the result in decisions
Once the agent has a signed spatial answer, it can branch on the result:
```python theme={null}
result = response.json()
if result["result"] < 500:
# Courier is within 500 meters of the delivery address
proceed_with_delivery_confirmation(result)
else:
# Too far — wait or reroute
schedule_retry(result["result"])
```
For boolean operations like containment or intersection, the result is `true` or `false`:
```python theme={null}
response = httpx.post("https://staging-api.astral.global/compute/v0/contains", json={
"container": geofence_polygon,
"containee": {"type": "Point", "coordinates": agent_location},
"chainId": 84532
})
result = response.json()
if result["result"]:
# Agent is inside the geofence
execute_geofenced_action()
else:
# Agent is outside — restricted
log_boundary_violation()
```
## Example: delivery verification agent
Here is a complete agent workflow that checks whether a courier has arrived at the delivery address. The agent polls the courier's location and confirms delivery when the courier is close enough.
```python theme={null}
import httpx
import time
ASTRAL_URL = "https://staging-api.astral.global"
DELIVERY_RADIUS_METERS = 100
def check_delivery_proximity(
courier_coords: list[float],
destination_coords: list[float],
chain_id: int = 84532
) -> dict:
"""Check if courier is within delivery radius of destination."""
response = httpx.post(f"{ASTRAL_URL}/compute/v0/distance", json={
"from": {"type": "Point", "coordinates": courier_coords},
"to": {"type": "Point", "coordinates": destination_coords},
"chainId": chain_id
})
response.raise_for_status()
return response.json()
def delivery_agent(
courier_coords: list[float],
destination_coords: list[float]
) -> dict:
"""
Delivery verification agent.
Checks proximity and returns a decision with the signed result.
"""
result = check_delivery_proximity(courier_coords, destination_coords)
distance = result["result"]
if distance <= DELIVERY_RADIUS_METERS:
return {
"decision": "confirm_delivery",
"distance_meters": distance,
"signed_result": result,
"reason": f"Courier is {distance:.1f}m from destination (within {DELIVERY_RADIUS_METERS}m threshold)"
}
return {
"decision": "wait",
"distance_meters": distance,
"signed_result": result,
"reason": f"Courier is {distance:.1f}m from destination (need to be within {DELIVERY_RADIUS_METERS}m)"
}
# Run the agent
decision = delivery_agent(
courier_coords=[2.3522, 48.8566],
destination_coords=[2.3525, 48.8568]
)
print(f"Decision: {decision['decision']}")
print(f"Reason: {decision['reason']}")
```
The `signed_result` in the decision object contains the full Astral response, including the cryptographic signature. This means the decision is auditable — anyone can verify the spatial computation that led to it.
## Why verified answers matter for agents
When an agent makes a decision based on spatial data, the signed result provides an audit trail. Anyone can verify that the agent's spatial reasoning was based on correctly computed data.
This matters for several reasons:
* **Accountability** — if an agent approves a delivery payout based on location proximity, the signed result proves the distance was computed correctly on the stated inputs.
* **Dispute resolution** — when a decision is challenged, the signed result is independent evidence. It does not depend on trusting the agent's own logs.
* **Composability** — signed results can be submitted onchain to trigger smart contract logic, bridging the agent's offchain reasoning with onchain actions.
Without verified computation, an agent's spatial claims are self-reported. With Astral, they are independently verifiable.
## Next steps
Full details on request format and error handling
Submit agent decisions onchain via EAS
# Blockchain Integration
Source: https://docs.astral.global/guides/blockchain-integration
Submit verified spatial results onchain via EAS
**Research Preview** — Smart contract patterns need audit. [GitHub](https://github.com/AstralProtocol)
# Blockchain integration
Astral's signed results can be submitted onchain as EAS attestations, making them available to smart contracts. This guide covers the full blockchain integration flow.
## What is EAS?
The [Ethereum Attestation Service](https://docs.attest.org/) (EAS) is an open protocol for making onchain and offchain attestations. Attestations are structured, signed claims — "entity X attests that Y is true."
EAS supports two storage modes:
* **Onchain attestations** — stored directly in EAS contracts, referenced by UID
* **Offchain attestations** — signed with EIP-712, stored on IPFS or other storage, verifiable without gas costs
Astral uses EAS to package signed spatial results as attestations that smart contracts can consume and act on.
## The pattern
The core flow: compute a spatial result, sign it, submit it onchain, and let a resolver contract react.
```mermaid theme={null}
sequenceDiagram
participant User
participant SDK as Astral SDK
participant Engine as Geospatial Policy Engine
participant EAS as EAS Contracts
participant Resolver as Your Resolver
User->>SDK: compute.within(...)
SDK->>Engine: Request computation
Engine-->>SDK: Signed attestation
SDK->>EAS: Submit delegated attestation
EAS->>Resolver: onAttest() callback
Resolver->>Resolver: Verify + Execute logic
Resolver-->>EAS: return true/false
```
## Delegated attestation flow
Astral uses EAS's **delegated attestation** pattern to separate signing from submission:
```mermaid theme={null}
sequenceDiagram
participant Dev as Developer App
participant SDK as Astral SDK
participant Svc as Compute Service
participant EAS as EAS Contracts
participant Res as Resolver
Dev->>SDK: compute.within(...)
SDK->>Svc: POST /compute/within
Svc->>Svc: Execute PostGIS operation
Svc->>Svc: Sign attestation
Svc-->>SDK: Signed attestation + delegated sig
SDK->>EAS: Submit with Astral's signature
EAS->>Res: onAttest() callback
Res->>Res: Execute business logic
```
The delegated attestation pattern means:
* **Astral signs** the attestation data offchain (inside the TEE)
* **Developer submits** with Astral's signature (pays gas)
* **EAS verifies** the signature and records Astral as attester
* **Resolver contracts** can verify `attestation.attester == astralSigner`
```typescript theme={null}
// Compute args are positional: (geometry, target, radius, options)
const result = await astral.compute.within(
uid1,
uid2,
500,
{ schema: RESOLVER_SCHEMA_UID, recipient: userAddress }
);
// Submit to EAS — triggers your resolver
const { uid } = await astral.compute.submit(result.delegatedAttestation);
```
The `delegatedAttestation.deadline` indicates when the signature expires. Submissions after the deadline will fail:
```typescript theme={null}
if (Date.now() / 1000 < result.delegatedAttestation.deadline) {
await astral.compute.submit(result.delegatedAttestation);
}
```
## Writing a resolver contract
In EAS, a **resolver** is a smart contract that gets called whenever an attestation is made against a specific schema. This enables:
* **Validation** — accept or reject attestations based on custom logic
* **Side effects** — execute actions atomically with attestation creation
* **Composability** — combine attestations with any onchain logic
### Basic resolver (LocationGatedAction)
**About the Astral signer**: The `astralSigner` address is the key that signs attestations inside the TEE. Signer management (multisig, key rotation, etc.) is on the roadmap. See [Security](/trust-model/security) for more details.
```solidity theme={null}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@eas/contracts/resolver/SchemaResolver.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract LocationGatedAction is SchemaResolver, Ownable {
address public astralSigner;
constructor(IEAS eas, address _astralSigner) SchemaResolver(eas) {
astralSigner = _astralSigner;
}
function onAttest(
Attestation calldata attestation,
uint256 /* value */
) internal override returns (bool) {
// 1. Verify from Astral
require(attestation.attester == astralSigner, "Not from Astral");
// 2. Decode policy result (BooleanPolicyAttestation)
(
bool result,
bytes32[] memory inputRefs,
uint64 timestamp,
string memory operation
) = abi.decode(
attestation.data,
(bool, bytes32[], uint64, string)
);
// 3. Execute business logic
require(result, "Policy check failed");
_executeAction(attestation.recipient);
return true;
}
function onRevoke(Attestation calldata, uint256)
internal pure override returns (bool)
{
return false; // Don't allow revocation
}
function _executeAction(address recipient) internal virtual {
// Override in child contracts
}
// Owner-controlled signer update for key rotation
function updateAstralSigner(address newSigner) external onlyOwner {
astralSigner = newSigner;
}
}
```
### Common patterns
#### NFT minting
```solidity theme={null}
contract LocationNFT is LocationGatedAction, ERC721 {
mapping(address => bool) public hasMinted;
uint256 public nextTokenId = 1;
function _executeAction(address recipient) internal override {
require(!hasMinted[recipient], "Already minted");
hasMinted[recipient] = true;
_mint(recipient, nextTokenId++);
}
}
```
#### Token distribution
```solidity theme={null}
contract LocationAirdrop is LocationGatedAction {
IERC20 public token;
uint256 public amount;
function _executeAction(address recipient) internal override {
token.transfer(recipient, amount);
}
}
```
#### Access control
```solidity theme={null}
contract LocationGate is LocationGatedAction {
mapping(address => bool) public hasAccess;
function _executeAction(address recipient) internal override {
hasAccess[recipient] = true;
}
modifier onlyVerified() {
require(hasAccess[msg.sender], "Location not verified");
_;
}
}
```
#### Numeric policies (distance-based)
Use numeric attestations (distance, area, length) for more sophisticated logic like [spatial demurrage](https://www.johnx.co/notes/spatial-demurrage) — where transfer fees vary based on distance from a target location.
```solidity theme={null}
// Decode numeric policy attestation
(
uint256 distanceCm,
string memory units,
bytes32[] memory inputRefs,
uint64 timestamp,
string memory operation
) = abi.decode(attestation.data, (uint256, string, bytes32[], uint64, string));
// Apply distance-based fee (example: 0.1% per km from target)
uint256 distanceKm = distanceCm / 100000;
uint256 fee = (amount * distanceKm) / 1000;
```
### Decoding attestation data
#### Boolean policies
```solidity theme={null}
(
bool result,
bytes32[] memory inputRefs,
uint64 timestamp,
string memory operation
) = abi.decode(
attestation.data,
(bool, bytes32[], uint64, string)
);
```
#### Numeric policies
```solidity theme={null}
(
uint256 result, // Scaled integer (centimeters)
string memory units, // "meters" or "square_meters"
bytes32[] memory inputRefs,
uint64 timestamp,
string memory operation
) = abi.decode(
attestation.data,
(uint256, string, bytes32[], uint64, string)
);
// Convert back to meters
uint256 meters = result / 100;
```
## Registering your schema
Before submitting attestations, register your schema with EAS and point it at your resolver:
```typescript theme={null}
import { SchemaRegistry } from '@ethereum-attestation-service/eas-sdk';
const schemaRegistry = new SchemaRegistry(SCHEMA_REGISTRY_ADDRESS);
// Boolean policy schema
const boolSchema = "bool result,bytes32[] inputRefs,uint64 timestamp,string operation";
const tx = await schemaRegistry.connect(signer).register({
schema: boolSchema,
resolverAddress: yourResolver.address,
revocable: false
});
const receipt = await tx.wait();
const schemaUID = receipt.logs[0].args.uid;
```
## Onchain vs offchain location records
Location attestations — the spatial inputs to computations — can be stored onchain or offchain:
* Stored on EAS contracts
* Referenced by UID alone
* Higher gas cost
* Permanent, immutable
* Stored on IPFS, servers, etc.
* Referenced by UID + URI
* No gas cost to create
* EIP-712 signed
### Onchain
```typescript theme={null}
// Create onchain attestation
const location = await astral.location.onchain.create({ location: geojson });
// Reference by UID only
await astral.compute.distance(location.uid, otherUID, { schema: SCHEMA_UID });
```
### Offchain
```typescript theme={null}
// Create offchain attestation
const location = await astral.location.offchain.create({ location: geojson });
// Reference by UID + URI
await astral.compute.distance(
{ uid: location.uid, uri: location.uri },
otherUID,
{ schema: SCHEMA_UID }
);
```
## Chain configuration
Astral supports EAS on multiple EVM-compatible chains. See [Schema registry](/resources/schemas) for schema UIDs by chain.
## Verification best practices
Always check `attestation.attester == astralSigner`:
```solidity theme={null}
require(attestation.attester == astralSigner, "Not from Astral");
```
Prevent replay of old attestations:
```solidity theme={null}
require(timestamp > block.timestamp - 1 hours, "Attestation too old");
```
Ensure the right locations were checked:
```solidity theme={null}
require(inputRefs[1] == EXPECTED_LANDMARK_UID, "Wrong location");
```
Prevent reuse of attestations:
```solidity theme={null}
mapping(bytes32 => bool) public usedAttestations;
function onAttest(...) {
bytes32 attUid = keccak256(abi.encode(attestation));
require(!usedAttestations[attUid], "Already used");
usedAttestations[attUid] = true;
}
```
## Key rotation
Resolver contracts should support updating the Astral signer address. For the Research Preview, a simple owner-controlled approach works:
```solidity theme={null}
function updateAstralSigner(address newSigner) external onlyOwner {
emit SignerUpdated(astralSigner, newSigner);
astralSigner = newSigner;
}
```
For production deployments, you may want the owner to be a multisig. We plan to provide more graceful key rotation mechanisms in future releases.
# Building Verification Plugins
Source: https://docs.astral.global/guides/building-plugins
Connect a new proof-of-location system to Astral
**Research Preview** — APIs may change. [GitHub](https://github.com/AstralProtocol)
# Building verification plugins
A plugin connects a proof-of-location (PoL) system to Astral's verification framework. Plugins collect signals from a PoL system, produce location stamps, and verify those stamps.
The plugin interface is under active development. The patterns shown here reflect the current design direction, but specifics may change as we iterate on the verification framework.
## What is a plugin?
Proof-of-location systems vary widely — hardware attestation, network triangulation, sensor fusion, institutional records. A plugin is a standardized adapter that translates a specific PoL system's output into the common stamp format that Astral can verify and cross-correlate.
Each plugin handles three responsibilities:
1. **Collect signals** from the PoL system (GPS readings, network measurements, device attestations)
2. **Create stamps** from those signals (structured evidence artifacts)
3. **Verify stamps** for authenticity and structural integrity
## Plugin interface
```typescript theme={null}
type Runtime = 'react-native' | 'node' | 'browser';
interface LocationProofPlugin {
name: string;
version: string;
runtimes: Runtime[];
requiredCapabilities: string[];
description: string;
// Each method is optional — a plugin implements the stages it supports
collect?(options?: CollectOptions): Promise;
create?(signals: RawSignals): Promise;
sign?(stamp: UnsignedLocationStamp, signer?: StampSigner): Promise;
verify?(stamp: LocationStamp): Promise;
}
interface RawSignals {
plugin: string;
timestamp: number; // Unix seconds
data: Record; // plugin-specific signal data
}
interface StampVerificationResult {
valid: boolean;
signaturesValid: boolean;
structureValid: boolean;
signalsConsistent: boolean;
details: Record;
}
```
## How stamps work
A stamp is a signed artifact from a PoL system. It encodes the system's conclusion about where and when an event occurred, along with the raw signals that support that conclusion.
Stamps follow the [Location Protocol](https://github.com/DecentralizedGeo/location-protocol-spec) format:
```typescript theme={null}
interface LocationStamp {
// Location data (LP v0.2)
lpVersion: string;
locationType: string;
location: LocationData; // Where evidence indicates the subject was
srs: string;
// Temporal footprint
temporalFootprint: { start: number; end: number };
// Plugin identification
plugin: string; // "proofmode", "witnesschain", etc.
pluginVersion: string;
// Evidence and signatures
signals: Record;
signatures: Signature[];
}
```
The distinction between a stamp's location and a claim's location is important. The stamp records where the PoL system *observed* the subject. The claim records where the subject *asserts* they were. Verification compares the two.
## Implementation guide
Here is a step-by-step walkthrough for building a hypothetical plugin that uses Wi-Fi access point data.
### Step 1: Define signal collection
```typescript theme={null}
import type {
LocationProofPlugin,
RawSignals,
UnsignedLocationStamp,
LocationStamp,
StampVerificationResult,
} from '@decentralized-geo/astral-sdk';
const wifiPlugin: LocationProofPlugin = {
name: 'wifi-triangulation',
version: '0.1.0',
runtimes: ['node', 'browser'],
requiredCapabilities: ['wifi-scan'],
description: 'Wi-Fi triangulation via nearby access points',
// `collect` returns a single RawSignals object: { plugin, timestamp, data }
async collect(): Promise {
const networks = await scanNearbyNetworks();
return {
plugin: 'wifi-triangulation',
timestamp: Date.now() / 1000,
data: {
accessPoints: networks.map((network) => ({
bssid: network.bssid,
ssid: network.ssid,
rssi: network.signalStrength,
frequency: network.frequency,
})),
},
};
},
// ... continued below
};
```
### Step 2: Create stamps from signals
```typescript theme={null}
// `create` transforms RawSignals into an unsigned stamp
async create(signals: RawSignals): Promise {
const accessPoints = signals.data.accessPoints as WifiAccessPoint[];
// Triangulate position from Wi-Fi signals
const position = triangulateFromAccessPoints(accessPoints);
return {
lpVersion: '0.2',
locationType: 'geojson-point',
location: {
type: 'Point',
coordinates: [position.longitude, position.latitude]
},
srs: 'http://www.opengis.net/def/crs/OGC/1.3/CRS84',
temporalFootprint: { start: signals.timestamp, end: signals.timestamp },
plugin: 'wifi-triangulation',
pluginVersion: '0.1.0',
signals: {
accessPoints,
triangulationMethod: 'weighted-centroid'
}
};
},
```
### Step 3: Implement stamp verification
```typescript theme={null}
// `verify` returns a StampVerificationResult:
// { valid, signaturesValid, structureValid, signalsConsistent, details }
async verify(stamp: LocationStamp): Promise {
const signals = stamp.signals as { accessPoints?: unknown[] };
const accessPointCount = signals.accessPoints?.length ?? 0;
// Structure: Wi-Fi triangulation needs at least 3 access points
const structureValid =
stamp.plugin === 'wifi-triangulation' && accessPointCount >= 3;
// Signal consistency: collection window should be tight (≤ 60s)
const duration = stamp.temporalFootprint.end - stamp.temporalFootprint.start;
const signalsConsistent = duration <= 60;
// Signatures
const signaturesValid = await verifyStampSignatures(stamp);
return {
valid: structureValid && signalsConsistent && signaturesValid,
signaturesValid,
structureValid,
signalsConsistent,
details: {
accessPointCount,
collectionDuration: duration,
signaturesChecked: stamp.signatures.length
}
};
}
```
## Registration
Register your plugin with the Astral SDK so it can be used in stamp collection and verification:
```typescript theme={null}
import { AstralSDK } from '@decentralized-geo/astral-sdk';
const astral = new AstralSDK({ chainId: 84532 });
astral.plugins.register(wifiPlugin);
// Now you can collect stamps using your plugin (collect returns an array)
const signals = await astral.stamps.collect({ plugins: ['wifi-triangulation'] });
const unsigned = await astral.stamps.create(
{ plugin: 'wifi-triangulation' },
signals[0]
);
const stamp = await astral.stamps.sign({ plugin: 'wifi-triangulation' }, unsigned, signer);
```
## Testing
Test each plugin responsibility independently:
```typescript theme={null}
import { describe, it, expect } from 'vitest';
describe('wifi-triangulation plugin', () => {
it('collects signals from nearby networks', async () => {
const signals = await wifiPlugin.collect();
expect(signals.plugin).toBe('wifi-triangulation');
expect(signals.timestamp).toBeGreaterThan(0);
expect((signals.data.accessPoints as unknown[]).length).toBeGreaterThan(0);
});
it('creates a valid stamp from signals', async () => {
const signals = mockWifiSignals(5);
const stamp = await wifiPlugin.create(signals);
expect(stamp.lpVersion).toBe('0.2');
expect(stamp.plugin).toBe('wifi-triangulation');
expect(stamp.location.type).toBe('Point');
expect(stamp.location.coordinates).toHaveLength(2);
});
it('rejects stamps with fewer than 3 access points', async () => {
const stamp = createMockStamp({ accessPointCount: 2 });
const result = await wifiPlugin.verify(stamp);
expect(result.valid).toBe(false);
expect(result.structureValid).toBe(false);
});
});
```
## Existing plugins
ProofMode is the plugin that's working end to end today. The Verify service also includes experimental stamp-verification logic — `witnesschain`, `gpsd`, `geoclue`, `wifi-mls`, and `ip-geolocation` — with interfaces defined. We're keen to develop new ones with partners. Two are highlighted here:
Device attestation and sensor fusion. Uses Secure Enclave (iOS) or hardware keystore (Android) to sign location observations.
Infrastructure verification using UDP latency triangulation and a challenger network. Trust is intended to derive from speed-of-light constraints and cryptoeconomic incentives.
Each plugin documents its own threat model and trust assumptions. When building a new plugin, you should clearly document what an attacker would need to do to forge a stamp from your system.
## Next steps
Understand how stamps combine into verifiable proofs
Submit proofs and understand credibility scores
# Calling the API
Source: https://docs.astral.global/guides/calling-the-api
Request format, authentication, and response handling
**Research Preview** — APIs may change. [GitHub](https://github.com/AstralProtocol)
# Calling the API
This guide covers the mechanics of making requests to Astral — how to format inputs, what comes back, and how to handle errors.
## Base URL
| Environment | URL |
| ----------------- | ----------------------------------- |
| Staging (testnet) | `https://staging-api.astral.global` |
| Local development | `http://localhost:3004` |
See [Local development](/guides/local-development) for setup instructions.
## Request format
All compute endpoints accept `POST` requests with a JSON body. The required fields depend on the operation, but every request needs a `chainId` to identify which chain to sign for.
### Distance, length
```json theme={null}
{
"from": { "type": "Point", "coordinates": [2.2945, 48.8584] },
"to": { "type": "Point", "coordinates": [2.3522, 48.8566] },
"chainId": 84532
}
```
### Contains, intersects
```json theme={null}
{
"container": {
"type": "Polygon",
"coordinates": [[[2.28, 48.85], [2.30, 48.85], [2.30, 48.87], [2.28, 48.87], [2.28, 48.85]]]
},
"geometry": { "type": "Point", "coordinates": [2.2945, 48.8584] },
"chainId": 84532
}
```
### Optional fields
| Field | Description |
| ----------- | ------------------------------------------------------------------ |
| `schema` | EAS schema UID — required if you plan to submit the result onchain |
| `recipient` | Ethereum address to set as the attestation recipient |
## Geographic feature inputs
You can pass geographic features in four formats:
### Raw GeoJSON
A GeoJSON geometry object directly in the request:
```json theme={null}
{
"from": { "type": "Point", "coordinates": [2.2945, 48.8584] }
}
```
### UID string
A reference to an onchain location attestation:
```json theme={null}
{
"from": "0xabc123...def456"
}
```
### UID + URI (offchain attestation)
A reference to an offchain attestation stored on IPFS or another location:
```json theme={null}
{
"from": { "uid": "0xabc123...", "uri": "ipfs://Qm..." }
}
```
### Inline signed attestation
A full offchain attestation object:
```json theme={null}
{
"from": { "attestation": { "uid": "0x...", "schema": "0x...", "data": "0x..." } }
}
```
## Authentication
No authentication is currently required. Rate limits apply by IP address (100 requests/hour for unauthenticated clients). Wallet-based authentication with higher limits is planned.
## Response format
A successful response includes the computed result, proof of computation, and a pre-signed attestation for optional onchain submission.
```json theme={null}
{
"result": 4520.37,
"units": "meters",
"operation": "distance",
"timestamp": 1706400000,
"inputRefs": [
"0xabc123...input_a_hash",
"0xdef456...input_b_hash"
],
"attestation": {
"uid": "0x...",
"schema": "0x...",
"attester": "0x...",
"recipient": "0x...",
"data": "0x...",
"signature": "0x..."
},
"delegatedAttestation": {
"signature": "0x7f8e9d...tee_signature",
"attester": "0x590fdb53ed3f0B52694876d42367192a5336700F",
"deadline": 1706403600
}
}
```
Here is what each field means:
* **`result`** — The computed answer. A number for distance/area/length, a boolean for contains/within/intersects.
* **`units`** — Unit of measurement (for numeric results). `"meters"` or `"square_meters"`.
* **`operation`** — The spatial operation that was performed.
* **`timestamp`** — Unix timestamp of when the computation ran.
* **`inputRefs`** — Hashes of the input geographic features. These let you verify which inputs were used in the computation.
* **`attestation`** — The full EAS attestation data, including the TEE's cryptographic signature.
* **`delegatedAttestation`** — A pre-signed attestation ready for onchain submission via EAS. The `deadline` indicates when the signature expires.
## Error handling
Errors follow [RFC 7807](https://tools.ietf.org/html/rfc7807) (Problem Details for HTTP APIs):
```json theme={null}
{
"type": "https://staging-api.astral.global/errors/invalid-input",
"title": "Invalid Input",
"status": 400,
"detail": "Field 'from' must be a valid GeoJSON geometry or attestation UID"
}
```
### Common error types
| Type | Status | What it means |
| ----------------------- | ------ | ----------------------------------------------------- |
| `invalid-input` | 400 | Bad request data, missing fields, or invalid geometry |
| `attestation-not-found` | 404 | The UID does not exist on the specified chain |
| `verification-failed` | 401 | Signature verification failed |
| `computation-error` | 500 | The PostGIS operation failed |
| `rate-limited` | 429 | Too many requests — wait and retry |
### Handling errors in code
```typescript theme={null}
const response = await fetch('https://staging-api.astral.global/compute/v0/distance', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
from: { type: 'Point', coordinates: [2.2945, 48.8584] },
to: { type: 'Point', coordinates: [2.3522, 48.8566] },
chainId: 84532
})
});
if (!response.ok) {
const error = await response.json();
console.error(`${error.title}: ${error.detail}`);
return;
}
const result = await response.json();
console.log(`Distance: ${result.result} ${result.units}`);
```
## Full example: curl
```bash theme={null}
curl -X POST https://staging-api.astral.global/compute/v0/contains \
-H "Content-Type: application/json" \
-d '{
"container": {
"type": "Polygon",
"coordinates": [[[2.28, 48.85], [2.30, 48.85], [2.30, 48.87], [2.28, 48.87], [2.28, 48.85]]]
},
"containee": { "type": "Point", "coordinates": [2.2945, 48.8584] },
"chainId": 84532
}'
```
## Next steps
Use Astral as a spatial oracle in agent workflows
Submit signed results onchain via EAS
Full endpoint documentation
# Local Currency
Source: https://docs.astral.global/guides/geofenced-token
Create a token that can only be traded within a geographic region
**Research Preview** — Code snippets need testing against actual implementation.
# Build a Local Currency
Create a local currency that can only be traded by people physically in a specific region — enabling hyperlocal economies.
**About location verification**: This guide uses GPS coordinates as input. GPS is spoofable. We're working on [Location Proof plugins](https://collective.flashbots.net/t/towards-stronger-location-proofs/5323) that will replace `getCurrentLocation` for stronger verification — these are still in development.
## Concept
Imagine a "Berlin Token" (BLN) that:
* Can only be minted by people present in Berlin
* Can only be transferred to people currently in Berlin
* Creates a true local currency for the city
For more on the economic theory behind spatially-restricted currencies, see [Spatial Demurrage](https://www.johnx.co/notes/spatial-demurrage).
***
## Step 1: Define the Region
```typescript theme={null}
import { AstralSDK } from '@decentralized-geo/astral-sdk';
const astral = new AstralSDK({ chainId: 84532, signer: wallet });
// Berlin city boundary (simplified)
const berlinBoundary = {
type: 'Polygon',
coordinates: [[[
[13.088, 52.338],
[13.761, 52.338],
[13.761, 52.675],
[13.088, 52.675],
[13.088, 52.338]
]]]
};
// Create region attestation
const region = await astral.location.onchain.create({
location: berlinBoundary,
memo: "Berlin city boundary for local currency"
});
console.log('Region UID:', region.uid);
```
***
## Step 2: The Token Contract
```solidity theme={null}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@eas/contracts/resolver/SchemaResolver.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract GeofencedToken is SchemaResolver, ERC20, Ownable {
address public astralSigner;
bytes32 public regionUID;
// Track pending operations
mapping(bytes32 => PendingMint) public pendingMints;
mapping(bytes32 => PendingTransfer) public pendingTransfers;
struct PendingMint {
address recipient;
uint256 amount;
bool exists;
}
struct PendingTransfer {
address from;
address to;
uint256 amount;
bool exists;
}
event MintRequested(bytes32 indexed requestId, address recipient, uint256 amount);
event TransferRequested(bytes32 indexed requestId, address from, address to, uint256 amount);
constructor(
IEAS eas,
address _astralSigner,
bytes32 _regionUID
)
SchemaResolver(eas)
ERC20("Berlin Token", "BLN")
Ownable(msg.sender)
{
astralSigner = _astralSigner;
regionUID = _regionUID;
}
// Request a mint - user must then prove they're in the region
function requestMint(uint256 amount) external returns (bytes32 requestId) {
requestId = keccak256(abi.encode(msg.sender, amount, block.timestamp));
pendingMints[requestId] = PendingMint({
recipient: msg.sender,
amount: amount,
exists: true
});
emit MintRequested(requestId, msg.sender, amount);
}
// Request a transfer - recipient must prove they're in the region
function requestTransfer(address to, uint256 amount) external returns (bytes32 requestId) {
require(balanceOf(msg.sender) >= amount, "Insufficient balance");
requestId = keccak256(abi.encode(msg.sender, to, amount, block.timestamp));
pendingTransfers[requestId] = PendingTransfer({
from: msg.sender,
to: to,
amount: amount,
exists: true
});
// Lock tokens during pending period
_transfer(msg.sender, address(this), amount);
emit TransferRequested(requestId, msg.sender, to, amount);
}
function onAttest(
Attestation calldata attestation,
uint256 /*value*/
) internal override returns (bool) {
require(attestation.attester == astralSigner, "Not from Astral");
// Decode policy attestation
(
bool inRegion,
bytes32[] memory inputRefs,
uint64 timestamp,
string memory operation
) = abi.decode(
attestation.data,
(bool, bytes32[], uint64, string)
);
require(inRegion, "Not in region");
require(inputRefs[0] == regionUID, "Wrong region");
require(timestamp > block.timestamp - 1 hours, "Too old");
// Extract request ID from recipient field (encoded)
bytes32 requestId = bytes32(uint256(uint160(attestation.recipient)));
// Check if this is a mint or transfer
if (pendingMints[requestId].exists) {
_executeMint(requestId);
} else if (pendingTransfers[requestId].exists) {
_executeTransfer(requestId);
} else {
revert("No pending operation");
}
return true;
}
function _executeMint(bytes32 requestId) internal {
PendingMint memory pending = pendingMints[requestId];
delete pendingMints[requestId];
_mint(pending.recipient, pending.amount);
}
function _executeTransfer(bytes32 requestId) internal {
PendingTransfer memory pending = pendingTransfers[requestId];
delete pendingTransfers[requestId];
_transfer(address(this), pending.to, pending.amount);
}
// Cancel pending operations
function cancelMint(bytes32 requestId) external {
require(pendingMints[requestId].recipient == msg.sender, "Not your request");
delete pendingMints[requestId];
}
function cancelTransfer(bytes32 requestId) external {
PendingTransfer memory pending = pendingTransfers[requestId];
require(pending.from == msg.sender, "Not your request");
delete pendingTransfers[requestId];
_transfer(address(this), pending.from, pending.amount);
}
function onRevoke(Attestation calldata, uint256) internal pure override returns (bool) {
return false;
}
}
```
***
## Step 3: SDK Integration
```typescript theme={null}
async function mintLocalToken(amount: bigint, wallet: Signer) {
const astral = new AstralSDK({ chainId: 84532, signer: wallet });
const tokenContract = new Contract(TOKEN_ADDRESS, TOKEN_ABI, wallet);
// 1. Request mint
const tx1 = await tokenContract.requestMint(amount);
const receipt = await tx1.wait();
const requestId = receipt.logs[0].args.requestId;
// 2. Get user's current location
const coords = await getCurrentLocation();
// 3. Create location attestation
const userLocation = await astral.location.onchain.create({
location: { type: 'Point', coordinates: coords }
});
// 4. Prove they're in the region
const proof = await astral.compute.contains(
REGION_UID,
userLocation.uid,
{
schema: SCHEMA_UID,
recipient: requestId // Pass request ID as recipient
}
);
if (!proof.result) {
await tokenContract.cancelMint(requestId);
throw new Error('Not in the required region');
}
// 5. Submit proof → triggers mint
const tx2 = await astral.compute.submit(proof.delegatedAttestation);
await tx2.wait();
return { transactionHash: tx2.hash };
}
```
***
## Alternative: Simpler Transfer Gate
For a simpler implementation, gate transfers directly:
```solidity theme={null}
contract SimpleGeofencedToken is ERC20 {
IEAS public eas;
address public astralSigner;
bytes32 public regionUID;
bytes32 public schemaUID;
function transferWithProof(
address to,
uint256 amount,
bytes32 attestationUID
) external {
// Fetch attestation from EAS
Attestation memory att = eas.getAttestation(attestationUID);
// Verify
require(att.attester == astralSigner, "Invalid attester");
require(att.recipient == to, "Wrong recipient");
(bool inRegion, bytes32[] memory inputs, , ) = abi.decode(
att.data,
(bool, bytes32[], uint64, string)
);
require(inRegion, "Recipient not in region");
require(inputs[0] == regionUID, "Wrong region");
// Execute transfer
_transfer(msg.sender, to, amount);
}
}
```
***
## Use Cases
Local money that stays in the community
Tokens only spendable within city limits
Pegged currencies for specific regions
Festival/conference currency
Build an escrow with location verification
# Local Development
Source: https://docs.astral.global/guides/local-development
Run Astral locally for development and testing
**Research Preview** — APIs may change. [GitHub](https://github.com/AstralProtocol)
# Local development
This guide walks you through running Astral on your machine. By the end, you will have a working local instance that responds to geocomputation requests.
## Prerequisites
* **Node.js 20+** — the service uses the `--env-file` flag, which requires Node 20
* **Docker** — for PostgreSQL with PostGIS
* **pnpm** — the monorepo uses pnpm workspaces
## Clone and install
```bash theme={null}
git clone https://github.com/AstralProtocol/astral-location-services.git
cd astral-location-services
pnpm install
```
## Start PostgreSQL + PostGIS
The repo includes a development Docker Compose file that runs PostgreSQL with PostGIS:
```bash theme={null}
docker compose -f docker-compose.dev.yml up -d
```
Verify the database is running:
```bash theme={null}
docker compose -f docker-compose.dev.yml ps
```
### Port conflicts
If port 5432 is already in use by another PostgreSQL instance, create a `docker-compose.override.yml` that maps to a different port (e.g., 5434:5432) and update your `DATABASE_URL` accordingly:
```yaml theme={null}
# docker-compose.override.yml
services:
postgres:
ports:
- "5434:5432"
```
Then set `DATABASE_URL=postgresql://postgres:postgres@localhost:5434/astral` in your environment file.
## Environment setup
```bash theme={null}
cp packages/astral-service/.env.example packages/astral-service/.env.local
```
Edit `.env.local` with your configuration. You will need a `SIGNER_PRIVATE_KEY` — any Ethereum private key works for development. You can generate one with `openssl rand -hex 32`.
The important fields:
| Variable | Description | Example |
| -------------------- | ---------------------------------------- | ------------------------------------------------------ |
| `DATABASE_URL` | PostgreSQL connection string | `postgresql://postgres:postgres@localhost:5432/astral` |
| `PORT` | HTTP server port | `3004` |
| `SIGNER_PRIVATE_KEY` | Ethereum private key for signing results | `0xac0974bec...` |
| `CHAIN_ID` | Default chain ID | `84532` (Base Sepolia) |
## Start the service
The service does not use dotenv. You must pass the env file explicitly using Node's `--env-file` flag.
```bash theme={null}
node --env-file=packages/astral-service/.env.local --import tsx packages/astral-service/src/index.ts
```
The `npm run dev` script may not work reliably. The command above is the most reliable way to start the service.
You should see output indicating the server is listening on the configured port.
## Health check
Confirm the service is running:
```bash theme={null}
curl http://localhost:3004/health
```
A successful response means the service is up and connected to the database.
## Smoke test
Run a distance computation between the Eiffel Tower and a point across the Seine:
```bash theme={null}
curl -X POST http://localhost:3004/compute/v0/distance \
-H "Content-Type: application/json" \
-d '{
"from": { "type": "Point", "coordinates": [2.2945, 48.8584] },
"to": { "type": "Point", "coordinates": [2.3522, 48.8566] },
"chainId": 84532
}'
```
The response includes the distance in meters, a cryptographic signature, and input references. If you see a `result` field with a numeric value, everything is working.
## Platform notes
PostGIS Docker images may need an explicit platform flag. If the container fails to start, add `platform: linux/amd64` to the postgres service in your compose file:
```yaml theme={null}
services:
postgres:
platform: linux/amd64
image: postgis/postgis:16-3.4
```
This runs under Rosetta emulation, which is slower but functional.
These ports may collide with other local services:
| Port | Used by | Common conflict |
| --------- | ----------------------- | ------------------------------------------- |
| 5432 | PostgreSQL | Other Postgres instances, Homebrew Postgres |
| 3004 | Astral service | Other dev servers |
| 3000-3003 | Other monorepo packages | Next.js, React dev servers |
Use the `docker-compose.override.yml` approach for database port conflicts, and change `PORT` in `.env.local` for service port conflicts.
## Next steps
Learn the request format and response structure
Your first verified spatial computation
# Location-Gated NFT
Source: https://docs.astral.global/guides/location-gated-nft
Build an NFT that requires physical presence to mint
**Research Preview** — Code snippets need testing against actual implementation.
# Build a Location-Gated NFT
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](https://collective.flashbots.net/t/towards-stronger-location-proofs/5323) that will replace `navigator.geolocation` for stronger verification — these are still in development.
## Overview
This guide walks through:
1. Setting up a reference location
2. Creating the resolver contract
3. Building the frontend
4. Handling the mint flow
***
## Step 1: Set Up the Reference Location
First, create a location attestation for the target location. This could be a permanent landmark, or a dynamic location that changes.
```typescript theme={null}
import { AstralSDK } from '@decentralized-geo/astral-sdk';
const astral = new AstralSDK({ chainId: 84532, signer: wallet });
// Create the landmark location
const landmark = await astral.location.onchain.create({
location: { type: 'Point', coordinates: [-122.4194, 37.7749] }, // San Francisco
memo: "SF Visitor Center — San Francisco Welcome NFT location"
});
console.log('Landmark UID:', landmark.uid);
// Store this UID for your contract
```
***
## Step 2: Deploy the Resolver Contract
```solidity theme={null}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@eas/contracts/resolver/SchemaResolver.sol";
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract LocationGatedNFT is SchemaResolver, ERC721, Ownable {
address public astralSigner;
bytes32 public landmarkUID;
uint256 public radius; // in centimeters (for precision)
uint256 public nextTokenId = 1;
mapping(address => bool) public hasMinted;
mapping(bytes32 => bool) public usedAttestations;
error NotFromAstral();
error AlreadyUsed();
error WrongOperation();
error InvalidInputs();
error WrongLocation();
error AttestationTooOld();
error NotCloseEnough();
error AlreadyMinted();
event NFTMinted(address indexed recipient, uint256 tokenId, bytes32 attestationUID);
constructor(
IEAS eas,
address _astralSigner,
bytes32 _landmarkUID,
uint256 _radiusMeters
)
SchemaResolver(eas)
ERC721("SF Visitor", "SFVISIT")
Ownable(msg.sender)
{
astralSigner = _astralSigner;
landmarkUID = _landmarkUID;
radius = _radiusMeters * 100; // Convert to cm
}
/// @dev Check if a string starts with a given prefix
/// @notice Operation strings include parameters (e.g., "within:500" not "within")
function _startsWith(string memory str, string memory prefix) internal pure returns (bool) {
bytes memory strBytes = bytes(str);
bytes memory prefixBytes = bytes(prefix);
if (strBytes.length < prefixBytes.length) return false;
for (uint256 i = 0; i < prefixBytes.length; i++) {
if (strBytes[i] != prefixBytes[i]) return false;
}
return true;
}
function onAttest(
Attestation calldata attestation,
uint256 /*value*/
) internal override returns (bool) {
// 1. Verify from Astral's TEE signer
if (attestation.attester != astralSigner) revert NotFromAstral();
// 2. Prevent replay
if (usedAttestations[attestation.uid]) revert AlreadyUsed();
usedAttestations[attestation.uid] = true;
// 3. Decode policy attestation (BooleanPolicy for 'within')
(
bool policyPassed,
bytes32[] memory inputRefs,
uint64 timestamp,
string memory operation
) = abi.decode(
attestation.data,
(bool, bytes32[], uint64, string)
);
// 4. Verify correct operation (uses prefix match - operation is "within:RADIUS")
if (!_startsWith(operation, "within")) revert WrongOperation();
// 5. Verify correct landmark was checked
// Note: inputRefs are content hashes when using raw GeoJSON, or UIDs when using attestations
if (inputRefs.length < 2) revert InvalidInputs();
if (inputRefs[1] != landmarkUID) revert WrongLocation();
// 6. Verify timestamp is recent (within 1 hour)
if (timestamp < block.timestamp - 1 hours) revert AttestationTooOld();
// 7. Verify policy passed (user is within radius)
if (!policyPassed) revert NotCloseEnough();
// 8. One mint per address
if (hasMinted[attestation.recipient]) revert AlreadyMinted();
hasMinted[attestation.recipient] = true;
// 9. Mint NFT
uint256 tokenId = nextTokenId++;
_mint(attestation.recipient, tokenId);
emit NFTMinted(attestation.recipient, tokenId, attestation.uid);
return true;
}
function onRevoke(Attestation calldata, uint256)
internal pure override returns (bool)
{
return false;
}
// Admin functions
function updateAstralSigner(address _signer) external onlyOwner {
astralSigner = _signer;
}
function updateRadius(uint256 _radiusMeters) external onlyOwner {
radius = _radiusMeters * 100;
}
}
```
***
## Step 3: Register the Schema
```typescript theme={null}
import { SchemaRegistry } from '@ethereum-attestation-service/eas-sdk';
const schemaRegistry = new SchemaRegistry(SCHEMA_REGISTRY_ADDRESS);
// Boolean policy schema for 'within' operation
const 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.
***
## Step 4: Frontend Integration
**Location source**: The `navigator.geolocation` API provides GPS coordinates which are spoofable. In production, replace with [Location Proof plugins](https://collective.flashbots.net/t/towards-stronger-location-proofs/5323) as they become available.
```typescript theme={null}
import { AstralSDK } from '@decentralized-geo/astral-sdk';
import * as turf from '@turf/turf';
const LANDMARK_UID = '0x...'; // Your reference location
const SCHEMA_UID = '0x...'; // Your schema
const 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.onchain.create({
location: { type: 'Point', coordinates: userCoords }
});
// 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 { uid } = await astral.compute.submit(result.delegatedAttestation);
return {
attestationUID: uid
};
}
```
***
## Step 5: React Component
```tsx theme={null}
import { useState, useEffect } from 'react';
import { useAccount, useSigner } from 'wagmi';
function MintButton() {
const { address } = useAccount();
const { data: signer } = useSigner();
const [status, setStatus] = useState<'checking' | 'eligible' | 'minting' | 'done'>('checking');
const [message, setMessage] = useState('');
useEffect(() => {
// Get user's location
navigator.geolocation.getCurrentPosition(async (position) => {
const coords: [number, number] = [
position.coords.longitude,
position.coords.latitude
];
const eligibility = await checkEligibility(coords);
setMessage(eligibility.message);
setStatus(eligibility.eligible ? 'eligible' : 'checking');
});
}, []);
const handleMint = async () => {
if (!signer) return;
setStatus('minting');
try {
navigator.geolocation.getCurrentPosition(async (position) => {
const coords: [number, number] = [
position.coords.longitude,
position.coords.latitude
];
const result = await mintNFT(coords, signer);
setStatus('done');
setMessage(`NFT minted! TX: ${result.transactionHash}`);
});
} catch (error) {
setStatus('eligible');
setMessage(`Error: ${error.message}`);
}
};
return (
{message}
);
}
```
***
## Understanding inputRefs
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.
***
## Common Errors
| Error | Selector | Cause | Solution |
| --------------------- | ------------ | ------------------------------------------------------------------------------- | ----------------------------------------------------------------------- |
| `InvalidSignature()` | `0x8baa579f` | `revocable` mismatch between signed data and submission | Ensure you submit with `revocable: true` |
| `Irrevocable()` | `0x157bd4c3` | Schema registered with `revocable: false` but attestation has `revocable: true` | Re-register schema with `revocable: true` |
| `WrongOperation()` | Custom | Checking `operation == "within"` but API returns `within:500` | Use prefix matching with `_startsWith()` |
| `WrongLocation()` | Custom | `inputRefs[1]` doesn't match expected landmark | Verify landmark UID; check if using raw GeoJSON (content hashes differ) |
| `NotFromAstral()` | Custom | Attestation not signed by Astral's TEE | Check `astralSigner` matches chain's TEE address |
| `AttestationTooOld()` | Custom | Timestamp older than 1 hour | User needs to generate a fresh attestation |
***
## Security Considerations
1. **Timestamp validation**: Contract requires attestation \< 1 hour old
2. **Replay prevention**: Track used attestation UIDs
3. **Input verification**: Check that the expected landmark was used
4. **One mint per address**: Prevent farming
Build a token with geographic restrictions
# Verifying Location Proofs
Source: https://docs.astral.global/guides/verifying-location-proofs
Submit location proofs and understand credibility scores
**Research Preview** — APIs may change. [GitHub](https://github.com/AstralProtocol)
# Verifying location proofs
A location proof is a location claim bundled with evidence — stamps from proof-of-location systems. The proof carries everything needed to assess the claim's credibility.
This guide walks you through creating a claim, collecting stamps, bundling them into a proof, and interpreting the verification result.
## What is a location proof?
A location proof has two parts:
* **Claim** — an assertion that a subject was at a location during a time window
* **Stamps** — evidence from one or more proof-of-location systems that support (or contradict) the claim
The verification process evaluates the stamps against the claim and produces a credibility assessment — not a simple yes/no, but a structured evaluation of how strong the evidence is.
## Creating a location claim
A claim follows the [Location Protocol](https://github.com/DecentralizedGeo/location-protocol-spec) format and includes the asserted location, time bounds, and spatial uncertainty:
```typescript theme={null}
const claim = {
lpVersion: '0.2',
locationType: 'geojson-point',
location: { type: 'Point', coordinates: [-122.4194, 37.7749] },
srs: 'http://www.opengis.net/def/crs/OGC/1.3/CRS84',
subject: { scheme: 'eth-address', value: '0x1234...abcd' },
radius: 100,
time: { start: Date.now() / 1000 - 60, end: Date.now() / 1000 },
eventType: 'presence'
};
```
Key fields:
| Field | Description |
| ----------- | --------------------------------------------------------------------------- |
| `location` | GeoJSON geometry — where the subject claims to have been |
| `subject` | Identifier for the entity making the claim (Ethereum address, DID, etc.) |
| `radius` | Spatial uncertainty in meters — you cannot claim presence at an exact point |
| `time` | Temporal bounds as Unix timestamps (start and end) |
| `eventType` | What kind of event: `"presence"`, `"transaction"`, `"delivery"` |
The `radius` field is required. Every location claim involves spatial uncertainty. Claiming a smaller radius requires stronger evidence to achieve the same credibility score.
## Collecting stamps
Stamps are evidence from proof-of-location plugins. Each plugin collects signals from its PoL system and produces a stamp:
```typescript theme={null}
import { AstralSDK, MockPlugin } from '@decentralized-geo/astral-sdk';
const astral = new AstralSDK({ chainId: 84532 });
// Register a plugin and collect signals. The SDK ships MockPlugin for
// local development; on a real device, evidence comes from a source like
// the ProofMode app. stamps.collect returns an array.
astral.plugins.register(new MockPlugin({ name: 'mock-1', lat: 37.7749, lon: -122.4194 }));
const signals = await astral.stamps.collect({ plugins: ['mock-1'] });
const unsignedStamp = await astral.stamps.create({ plugin: 'mock-1' }, signals[0]);
const stamp1 = await astral.stamps.sign({ plugin: 'mock-1' }, unsignedStamp, deviceSigner);
```
For stronger verification, collect stamps from multiple **independent** systems — independence is what cross-correlation rewards, so two stamps from the same source add little. Real independence means genuinely different proof-of-location systems (for example the ProofMode app plus a network-based plugin). Those client plugins are still being built; the shape is the same:
```typescript theme={null}
// A second stamp from an independent system (illustrated with a second
// mock instance; in practice this would be a different PoL system).
astral.plugins.register(new MockPlugin({ name: 'mock-2', lat: 37.7750, lon: -122.4193 }));
const witnessSignals = await astral.stamps.collect({ plugins: ['mock-2'] });
const unsignedStamp2 = await astral.stamps.create({ plugin: 'mock-2' }, witnessSignals[0]);
const stamp2 = await astral.stamps.sign({ plugin: 'mock-2' }, unsignedStamp2, nodeSigner);
```
## Bundling into a proof
Combine the claim and stamps into a location proof:
```typescript theme={null}
const proof = astral.proofs.create(claim, [stamp1, stamp2]);
```
A single-stamp proof is valid. Multiple stamps from independent systems enable cross-correlation, which increases confidence.
## Submitting to the verify API
Submit the proof for verification:
```typescript theme={null}
// `mode: 'tee'` routes to the hosted service; the default 'local' mode
// evaluates in-process and returns the credibility vector directly.
const result = await astral.proofs.verify(proof, { mode: 'tee', chainId: 84532 });
```
Or via raw HTTP:
```bash theme={null}
curl -X POST https://staging-api.astral.global/verify/v0/proof \
-H "Content-Type: application/json" \
-d '{
"claim": {
"lpVersion": "0.2",
"locationType": "geojson-point",
"location": { "type": "Point", "coordinates": [-122.4194, 37.7749] },
"srs": "http://www.opengis.net/def/crs/OGC/1.3/CRS84",
"subject": { "scheme": "eth-address", "value": "0x1234...abcd" },
"radius": 100,
"time": { "start": 1706399940, "end": 1706400000 },
"eventType": "presence"
},
"stamps": [ ... ]
}'
```
## Understanding credibility scores
The verification result is a structured credibility assessment, not a simple pass/fail:
The exact structure of the credibility vector is an [open research question](/concepts/location-proof-evaluation) and will change. The fields below are illustrative of the current shape, not a stable contract.
```json theme={null}
{
"credibility": {
"dimensions": {
"spatial": {
"meanDistanceMeters": 12.5,
"maxDistanceMeters": 18.3,
"withinRadiusFraction": 1.0
},
"temporal": {
"meanOverlap": 0.95,
"minOverlap": 0.90,
"fullyOverlappingFraction": 0.5
},
"validity": {
"signaturesValidFraction": 1.0,
"structureValidFraction": 1.0,
"signalsConsistentFraction": 1.0
},
"independence": {
"uniquePluginRatio": 1.0,
"spatialAgreement": 0.88,
"pluginNames": ["mock-1", "mock-2"]
}
},
"stampResults": [ "..." ],
"meta": { "stampCount": 2, "evaluatedAt": 1706400000, "evaluationMode": "tee" }
},
"evaluationMethod": "multifactor-v0",
"evaluatedAt": 1706400000,
"attestation": {
"uid": "0xabc123...",
"attester": "0x590fdb53..."
}
}
```
### Credibility dimensions
These dimensions are a **preliminary sketch** — their exact structure and metrics are an [active research area](/concepts/location-proof-evaluation) and will change. The credibility vector is a multidimensional assessment grouped into four dimensions — each an object of metrics, not a single score:
| Dimension | What it measures |
| ---------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| **Spatial** | How closely the stamps' observed locations match the claimed location (mean/max distance, fraction within the claimed radius) |
| **Temporal** | How well the stamps' time windows overlap the claimed time window |
| **Validity** | Fraction of stamps with valid signatures, structure, and consistent signals |
| **Independence** | How independent and corroborating the sources are (unique-plugin ratio, spatial agreement) |
### No single score
There is **no top-level `confidence` field**. Collapsing the vector into one number requires deciding which dimensions matter most — that judgment belongs to your application, not to Astral. The SDK ships an `exampleWeighting()` helper you can use as a starting point, but you're expected to define your own.
**A credibility score is not a probability.** Strong metrics mean the evidence is strong — not "an X% chance the claim is true." Calibrating these to true probabilities is future work.
### Cross-correlation
When a proof includes multiple stamps, the verification engine analyzes their relationship:
* **Independence** — are the stamps from truly independent systems? Two stamps from the same underlying data source do not add much.
* **Agreement** — do the stamps agree on location and time? Independent stamps that corroborate each other significantly boost confidence.
## Multi-factor proofs
Multiple stamps from independent systems increase confidence because an attacker would need to compromise multiple unrelated systems simultaneously:
```typescript theme={null}
// Single stamp
const singleResult = await astral.proofs.verify(
astral.proofs.create(claim, [stamp1]),
{ mode: 'tee', chainId: 84532 }
);
// Multi-stamp from independent systems: the independence dimension reflects
// that the evidence is corroborated rather than redundant
const multiResult = await astral.proofs.verify(
astral.proofs.create(claim, [stamp1, stamp2]),
{ mode: 'tee', chainId: 84532 }
);
// multiResult.credibility.dimensions.independence.uniquePluginRatio → higher
// multiResult.credibility.dimensions.independence.spatialAgreement → higher
```
The improvement comes from source independence. Redundant stamps from the same system do not meaningfully strengthen the assessment, but they do not weaken it either.
### Choosing the right level of evidence
The level of evidence you need depends on the value of the transaction the proof underpins:
* **Low-stakes** (check-in rewards, social proof) — a single stamp from a device attestation plugin may be sufficient.
* **Medium-stakes** (delivery verification, access control) — two independent stamps provide meaningful forgery resistance.
* **High-stakes** (insurance payouts, land records) — multiple independent stamps with high forgery resistance, plus onchain submission for an immutable audit trail.
## Using verified proofs as compute inputs
Verified location proofs can serve as trusted inputs to geocomputation operations. This connects the verification pipeline to the spatial reasoning pipeline:
```typescript theme={null}
const verifiedProof = await astral.proofs.verify(proof, { mode: 'tee', chainId: 84532 });
// Use the verified proof as input to a spatial operation. Compute args are
// positional; a verified proof is passed as { verifiedProof } (the whole object).
const result = await astral.compute.contains(
approvedZonePolygonUID, // container
{ verifiedProof }, // containee — the verified location proof
{ schema: SCHEMA_UID }
);
```
## Next steps
Deeper dive into claims, stamps, and the verification model
Connect a new proof-of-location system to Astral
# How It Works
Source: https://docs.astral.global/how-it-works
The full pipeline from location evidence to signed results
**Research Preview** — APIs may change. [GitHub](https://github.com/AstralProtocol)
# How it works
Location data is easy to fake and hard to verify. GPS can be spoofed with a \$20 app. IP geolocation is trivially manipulated. Self-reported coordinates carry no proof of origin. When a delivery platform confirms a drop-off, a compliance system checks a geofence, or an autonomous agent makes a spatial decision — the location data underneath is taken on faith.
Astral exists to change that. The system provides a pipeline that collects location evidence, bundles it into verifiable artifacts, evaluates its credibility, runs spatial computations on it, and delivers signed results that any downstream system can independently verify.
## The Pipeline
### 1. Collect location evidence
Devices collect signals from **proof-of-location systems** — independent systems that produce evidence about where something is. A phone's secure enclave can attest to sensor readings. A network of infrastructure nodes can triangulate position via latency measurements. Each system has different strengths, weaknesses, and trust properties.
Each [location proof plugin](/plugins/overview) connects to a proof-of-location system, collects signals, and processes them to produce a [location stamp](/concepts/location-stamps) — an individual piece of evidence about the location of a device, person, asset or event.
**Why this step matters:** Self-reported location is trivially spoofable. Composing evidence from independent proof-of-location systems raises the cost of forgery — which is the real goal, since absolute certainty about physical location is not achievable.
### 2. Create a location proof
A **location stamp** is a piece of signed, structured evidence from a single proof-of-location system about an observed location. Each location stamp carries enough information to verify its internal validity: signatures, temporal footprint, and plugin identification.
One or more location stamps bundle with a **location claim** — an assertion about where and when an event occurred — to form a **location proof**. The location proof is the verifiable artifact: a claim paired with its supporting evidence.
`proof = {claim, [stamps...]}`
**Why this step matters:** Attaching structured, composable evidence to claims give people and systems receiving location proofs what they need to verify and assess credibility. A location proof separates the assertion ("I was here") from the evidence ("here's why you should believe me"), which makes both independently evaluable.
### 3. Verify the location proof
Submit the location proof to Astral's Verify endpoint. Inside a Trusted Execution Environment (TEE), the system evaluates the location stamp — checking signatures, structure, and signal consistency — then assesses how well the evidence supports the location claim. For multi-stamp location proofs, it also cross-correlates across independent sources.
The output is a **credibility vector**: structured scores quantifying how well the evidence supports the claim across multiple dimensions (spatial, temporal, validity, independence, and more — this is an active research area). Location proof verification does not output a binary yes/no — application developers decide what dimensions they value and the threshold they need to meet.
**Why this step matters:** Independent, structured evaluation of location evidence. The credibility vector gives applications enough information to make risk-appropriate decisions — a \$10 check-in reward can accept lower confidence than a \$10M physical asset verification.
### 4. Compute spatial relationships
Run geospatial operations — distance, containment, intersection, area, length — on location data inside the Astral TEE. The compute endpoints accept raw GeoJSON, signed location records, or verified location proofs as inputs. [PostGIS](https://postgis.net/) (backed by the [GEOS](https://libgeos.org/) library, the same computational geometry engine used by QGIS and other open-source geospatial tools) performs the computation; the TEE signs the result.
**Why this step matters:** Verifiable spatial answers. The signed result proves not just "what was the answer" but "the answer was computed correctly by trusted code on these specific inputs."
### 5. Use the signed result
The signed result goes wherever it needs to — an autonomous agent's decision loop, a backend database, a compliance report, or a smart contract via an EAS attestation. It carries its own proof of correctness, so any downstream consumer can verify it independently without re-executing the computation or trusting the intermediary.
**Why this step matters:** Portable, independently verifiable spatial facts. The result is useful whether it stays offchain or goes onchain.
## Architecture
```mermaid theme={null}
graph LR
subgraph Client["🔵 Client"]
D[Device / Sensor] --> P1[Plugin A]
D --> P2[Plugin B]
P1 --> S1[Stamp]
P2 --> S2[Stamp]
S1 --> LP[Location Proof]
S2 --> LP
CL[Claim] --> LP
end
subgraph TEE["🟡 Astral TEE"]
LP --> VE[Verify]
VE --> CV[Credibility Vector]
LD[Location Data] --> CO[Compute]
CV -.-> CO
CO --> SR[Signed Result]
end
SR --> AG[Agent]
SR --> AP[Application]
SR --> SC[Smart Contract]
style Client fill:#e8f4fd,stroke:#4a90d9,color:#000
style TEE fill:#fef9e7,stroke:#d4a63a,color:#000
```
Evidence collection → composition → verification. Produces a credibility vector that tells you how much to trust the location claim. The verified location proof can then flow into the Compute service, or be used on its own.
Location data (raw, signed, or verified) → spatial operation → signed result. Produces a cryptographically signed spatial answer. The dashed arrow from the credibility vector to Compute indicates that verified proofs *can* feed into computation, but don't have to.
The two capabilities compose but don't require each other. A verified location proof is valuable on its own — it doesn't need to flow into the Compute service. And a compute operation can run on any location data, not just verified location proofs.
## What's verified vs. what's trusted
The TEE guarantees that computation executes correctly — the code that ran is the code that was attested, inputs weren't tampered with, and the signing key never leaves the enclave. That's the "verifiable" part.
The truthfulness of location inputs is a separate question. It depends on the strength of the location proof: how many independent proof-of-location systems contributed evidence, how resistant those systems are to forgery, and whether the evidence is consistent. Astral evaluates this and reports it honestly via the credibility vector — but it cannot make weak evidence strong.
For a detailed accounting of what exactly is verified and what trust assumptions remain, see the [Trust Model](/trust-model/architecture).
How Astral represents and verifies spatial data
# ERC-8004 + Astral
Source: https://docs.astral.global/integrations/erc-8004
Adding verifiable location to autonomous agent validation
**Research Preview** — This integration is under active development. Interfaces may change.
# ERC-8004 + Astral
[ERC-8004](https://ethereum-magicians.org/t/erc-8004-autonomous-agents) gives autonomous agents identity, reputation, task delegation, and on-chain validation. It answers *who* an agent is, *what* it can do, and *whether* it did it correctly.
It doesn't answer **where the agent is**.
A growing class of agent tasks — deliveries, inspections, environmental monitoring, data residency, compute jobs anchored to physical infrastructure — require verifiable proof of location. Without it, an agent's claim to be "at the delivery address" or "at the inspection site" is self-reported and unverifiable. ERC-8004 validators can check computational correctness, but they can't check physical presence.
Astral fills that gap. By combining Astral's verifiable location infrastructure with ERC-8004's agent framework, agents can prove *where* they are — not just *what* they did.
## The three layers
| Layer | Responsibility | Provider |
| ------------- | --------------------------------------------- | --------------- |
| **Agent** | Identity, task execution, reputation | ERC-8004 |
| **Location** | Location proofs, verification, geocomputation | Astral Protocol |
| **Consensus** | On-chain validation and settlement | EVM chain |
## How it works
[Astral Location Services](/concepts/astral-location-services) is a verifiable geospatial computation service that runs inside a Trusted Execution Environment. It exposes two modules:
* **[Verify](/concepts/verify)** — Verifies location proofs: checks each stamp's signatures, structure, and signal consistency, cross-correlates evidence from independent sources, and produces a [credibility vector](/concepts/location-proof-evaluation).
* **[Compute](/concepts/compute)** — Computes spatial relationships between geographic features: distance, containment, intersection, area. This is how spatial constraints in tasks get checked — e.g., "is this verified location within the required geofence?"
Both endpoints are designed to run inside the TEE, and both produce cryptographically signed [results](/concepts/signed-results) that can be recorded on-chain as EAS attestations.
```mermaid theme={null}
graph LR
subgraph ERC8004["ERC-8004 Agent"]
T[Task assigned] --> E[Execute task]
E --> S[Submit result]
end
subgraph Astral["Astral Location Services"]
C[Collect evidence] --> V[Verify]
V --> CR[Credibility vector]
CR --> CO[Compute]
CO --> A[Signed attestations]
end
T --> C
A --> S
S --> Val[On-chain validation]
```
1. **Task assignment** — An ERC-8004 task includes a spatial constraint (e.g., "must be within 50m of 52.3676°N, 4.9041°E")
2. **Evidence collection** — The agent uses [location proof plugins](/plugins/overview) to collect location evidence. The design supports many independent signal sources — GPS hardware, WiFi geolocation, IP lookup, device attestation, infrastructure triangulation — behind a common interface. ProofMode is working today; the others (WitnessChain, gpsd, GeoClue, Wi-Fi/MLS, IP geolocation) are experimental, with interfaces defined and early verification logic in the service. We're keen to develop new plugins with partners.
3. **Verification** — The agent submits the location proof to Astral's [Verify](/concepts/verify) endpoint. It checks each [location stamp's](/concepts/location-stamps) internal validity, cross-correlates evidence from independent sources, and evaluates how well the evidence supports the claim. The output is a [credibility vector](/concepts/location-proof-evaluation) — a multi-dimensional assessment across spatial, temporal, validity, and independence dimensions.
4. **Constraint check** — The agent uses Astral's [Compute](/concepts/compute) endpoint to check the verified location against the task's spatial constraint — e.g., a containment check against a geofence, or a distance check from a target point. Compute compares geographic features and returns a signed result.
5. **Signed attestations** — Both the credibility vector and the constraint check result are signed and can be recorded on-chain as [EAS attestations](/concepts/signed-results). The signing key lives inside the TEE; under attestation, a valid signature proves the result came from Astral's attested code. (Continuous attested operation is not yet funded — see the [trust model](/trust-model/what-you-are-trusting).)
6. **Result submission** — The signed attestations are bundled with the agent's task result and submitted on-chain. ERC-8004 validators check the Astral signatures to verify both the location evidence and the spatial constraint were evaluated correctly.
## What Astral adds to ERC-8004
### Verifiable location evaluation
Astral's Verify endpoint is designed to run inside a TEE. Under attestation, this provides hardware attestation that the verification code ran as deployed, the credibility vector was computed correctly, and the signed output hasn't been tampered with — and the signing key cannot be extracted by the operator. Today a valid signature proves a key Astral controls produced the result; binding it to an independently attested enclave is target-state (see the [trust model](/trust-model/what-you-are-trusting)).
The strength of the underlying evidence depends on the [plugins](/plugins/overview) used. Each plugin connects with a proof-of-location system that has its own trust properties — from hardware-rooted device attestation to lightweight IP geolocation. The [credibility vector](/concepts/location-proof-evaluation) surfaces these differences so applications can make informed decisions. The exact structure and dimensions of the credibility vector are an active area of [research](https://github.com/AstralProtocol/research).
### Verifiable geocomputation
Astral's [Compute](/concepts/compute) endpoint compares and computes relationships between geographic features — distances, containment, intersections, areas — inside the TEE. This is what makes spatial constraints enforceable: a task says "agent must be within this geofence," and Compute produces a signed attestation confirming whether the condition is met.
### Signed attestations on-chain
Every result from Astral Location Services — whether from Verify or Compute — is a cryptographically signed [result](/concepts/signed-results) that can be recorded on-chain as an EAS attestation. These signatures are verifiable on-chain, making them native inputs to ERC-8004 validator contracts.
## Use cases unlocked
Courier produces a multi-factor location proof within 30m of the delivery point. Payment can release when the credibility vector meets the task's threshold — with the evidence on record, not just a self-reported GPS ping.
Each sensor reading is bound to a verified location. Coverage gaps are detectable. A single agent can't fake data for multiple stations.
Agents dispatched to inspect properties must prove physical presence at the site before submitting reports.
Compute jobs and data storage tasks can require verifiable proof that the agent is operating from a specific jurisdiction.
## Trust model
| Claim | Verification method | Trust root |
| ---------------------------------------- | --------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
| Evidence was evaluated correctly | TEE-attested computation | TEE hardware attestation + Astral signing key |
| Spatial constraint was checked correctly | TEE-attested geocomputation | TEE hardware attestation + Astral signing key |
| Location stamps are internally valid | Plugin-level verification (signatures, structure, signal consistency) | Per-plugin — depends on the proof-of-location system |
| Location evidence is authentic at source | Plugin-specific | Varies: hardware secure elements, infrastructure attestation, cryptographic proofs |
Each [plugin](/plugins/overview) has its own trust properties. The credibility vector's dimensions — especially validity and independence — help applications distinguish between evidence backed by strong guarantees and lighter-weight signals.
For a detailed discussion, see the [Trust Model](/trust-model/architecture).
## Open questions
These are active areas of research and design:
1. **On-chain proof size** — Full credibility vectors with TEE attestations can be large. Should only the hash go on-chain, with full results on IPFS/Arweave?
2. **Proof freshness** — How recent must a location proof be? Should tasks include a `maxProofAge` parameter?
3. **Privacy** — Can Astral produce zero-knowledge spatial proofs? ("Agent is within the geofence" without revealing exact coordinates.)
4. **Agent collusion** — Colluding agents could co-attest false locations. How does the credibility model handle spatial Sybil attacks?
5. **Standard extension** — Should location verification become a formal ERC-8004 extension (EIP), or remain an integration-level pattern?
What the system verifies vs. what it assumes
# Introduction
Source: https://docs.astral.global/introduction
Verifiable location infrastructure for agents, applications, and smart contracts
**Research Preview** — The Astral Protocol is under active development and not yet production-ready. APIs may change. We're building in public and welcome feedback.
Location data is everywhere, but trust in location data is very brittle. GPS is spoofable. VPNs manipulate IP addresses. In most cases, location data can be edited freely or forged easily. When an agent, an application, or a smart contract needs to answer a spatial question — was this device actually there? Is this point inside that boundary? — there is no way to independently verify the claim or the computation. You're taking someone's word for it.
Location data is becoming more and more important across a range of use cases — so we built the Astral Protocol to introduce verifiable location-based services.
The system rests on two core capabilities:
Verify where a device, user, or event actually was — using multi-factor evidence from independent proof-of-location systems
Ask spatial questions — distance, containment, intersection, area — and get back signed, cryptographically verifiable answers
Both produce **signed results** — cryptographic artifacts that any downstream system can verify independently. The result is the same whether it ends up in an autonomous agent's decision loop, a compliance report, a backend database, or a smart contract.
## Two Capabilities, One Pipeline
```mermaid theme={null}
graph LR
subgraph Inputs
direction TB
RG[Raw GeoJSON]
SL[Signed Location Record]
LP[Location Proof]
end
subgraph Astral Hosted TEE
direction TB
C[Geospatial Compute]
V[Verify]
end
RG --> C
SL --> C
LP --> V
V --> C
C --> SR[Signed Result]
SR --> A1[Agent]
SR --> A2[Application]
SR --> A3[Smart Contract]
```
1. **Collect + Compose** — Gather location evidence from various sources and compose it into a single, verifiable location proof.
2. **Verify** — Submit location proofs for evidence-based verification. The system evaluates stamps from independent proof-of-location systems and returns credibility scores.
3. **Compute** — Run spatial operations (distance, containment, intersection, area, length) inside a Trusted Execution Environment. PostGIS computes; the TEE signs.
4. **Use the results anywhere** — The signed results carry their own proof of correctness. Use them peer-to-peer, on centralized servers, or in smart contracts.
## Quick Example
Create a location proof, verify it, then check if the verified position is inside a geofence:
```typescript theme={null}
import { AstralSDK, MockPlugin } from '@decentralized-geo/astral-sdk';
const astral = new AstralSDK({
chainId: 84532,
signer: wallet,
apiUrl: 'https://staging-api.astral.global'
});
// 1. Register a proof-of-location plugin. The SDK ships MockPlugin for local
// development; real device evidence comes from sources like the ProofMode app.
astral.plugins.register(new MockPlugin({ name: 'mock-1', lat: 37.7749, lon: -122.4194 }));
// 2. Collect signals, then create and sign a stamp
const signals = await astral.stamps.collect({ plugins: ['mock-1'] });
const unsigned = await astral.stamps.create({ plugin: 'mock-1' }, signals[0]);
const stamp = await astral.stamps.sign({ plugin: 'mock-1' }, unsigned, deviceSigner);
// 3. Compose a location proof: a claim ("I was here") bundled with evidence
const claim = {
lpVersion: '0.2',
locationType: 'geojson-point',
location: { type: 'Point', coordinates: [-122.4194, 37.7749] },
srs: 'http://www.opengis.net/def/crs/OGC/1.3/CRS84',
subject: { scheme: 'eth-address', value: '0x1234...' },
radius: 100,
time: { start: Date.now() / 1000 - 60, end: Date.now() / 1000 },
};
const proof = astral.proofs.create(
claim,
[stamp] // multi-stamp location proofs are supported, and more secure
);
// 4. Verify against the hosted service. The result includes a credibility
// vector — multidimensional, not a single score.
const verified = await astral.proofs.verify(proof, { mode: 'tee', chainId: 84532 });
console.log(verified.credibility); // CredibilityVector — you decide how to weight it
const locationUID = verified.attestation.uid;
// 5. Compute — use the verified location in a spatial operation.
// Compute arguments are positional; options carry the EAS schema UID.
const inside = await astral.compute.contains(
geofencePolygon, // container
locationUID, // containee
{ schema: SCHEMA_UID }
);
console.log(`Inside geofence: ${inside.result}`);
```
The response includes the computation result, a cryptographic signature from the TEE, and references to the inputs — everything needed to verify the answer independently.
The signed result is portable. Use it directly in your application:
```typescript theme={null}
// In an agent workflow — branch on the verified spatial answer
if (inside.result) {
confirmDelivery(inside); // the signed result is the audit trail
}
// In a backend — store the signed result as evidence
await db.insert({ delivery_id, proof: inside });
```
Or submit it onchain to trigger smart contract logic:
```typescript theme={null}
// Submit onchain via EAS — triggers your resolver contract
await astral.compute.submit({
attestation: inside.attestation,
delegatedAttestation: inside.delegatedAttestation,
});
```
## What You Can Build
Verified location data and verifiable spatial computation support applications that have, until now, depended on blind trust in self-reported location:
Confirm a courier arrived at the right location — with evidence, not just GPS
Give AI agents verifiable spatial reasoning with an auditable trail
Prove an asset stayed within an approved corridor
Trigger policies based on verified proximity to events
Gate features, content, or actions on verified presence
Submit signed results to smart contracts via EAS attestations
Your first verified spatial computation in 5 minutes
# Custom Plugins
Source: https://docs.astral.global/plugins/custom
Build your own proof-of-location plugin for the Astral SDK
# Building a custom plugin
Any proof-of-location system can integrate with the Astral SDK by implementing the `LocationProofPlugin` interface. This guide walks through the interface contract, which methods to implement, and how to test your plugin.
## The interface
```typescript theme={null}
import type {
LocationProofPlugin,
Runtime,
CollectOptions,
RawSignals,
UnsignedLocationStamp,
LocationStamp,
StampSigner,
StampVerificationResult
} from '@decentralized-geo/astral-sdk';
```
```typescript theme={null}
interface LocationProofPlugin {
readonly name: string; // Unique plugin identifier
readonly version: string; // Semver version
readonly runtimes: Runtime[]; // ['react-native' | 'node' | 'browser']
readonly requiredCapabilities: string[]; // e.g., ['gps', 'network']
readonly description: string; // Human-readable description
collect?(options?: CollectOptions): Promise;
create?(signals: RawSignals): Promise;
sign?(stamp: UnsignedLocationStamp, signer?: StampSigner): Promise;
verify?(stamp: LocationStamp): Promise;
}
```
All four methods are optional. Implement what makes sense for your system.
***
## Step 1: Implement the interface
```typescript theme={null}
import type {
LocationProofPlugin,
Runtime,
CollectOptions,
RawSignals,
UnsignedLocationStamp,
LocationStamp,
StampSigner,
StampVerificationResult
} from '@decentralized-geo/astral-sdk';
export class MyLocationPlugin implements LocationProofPlugin {
readonly name = 'my-location-service';
readonly version = '0.1.0';
readonly runtimes: Runtime[] = ['node', 'browser'];
readonly requiredCapabilities: string[] = [];
readonly description = 'Location proofs from My Location Service';
constructor(private config: { apiUrl: string; apiKey: string }) {}
async collect(options?: CollectOptions): Promise {
const response = await fetch(`${this.config.apiUrl}/evidence`, {
headers: { Authorization: `Bearer ${this.config.apiKey}` }
});
const data = await response.json();
return {
plugin: this.name,
timestamp: Math.floor(Date.now() / 1000),
data
};
}
async create(signals: RawSignals): Promise {
return {
lpVersion: '0.2',
locationType: 'geojson-point',
location: {
type: 'Point',
coordinates: [signals.data.longitude, signals.data.latitude]
},
srs: 'EPSG:4326',
temporalFootprint: {
start: signals.timestamp,
end: signals.timestamp + 60
},
plugin: this.name,
pluginVersion: this.version,
signals: signals.data
};
}
async verify(stamp: LocationStamp): Promise {
const structureValid =
stamp.lpVersion === '0.2' &&
stamp.plugin === this.name &&
stamp.location != null &&
stamp.temporalFootprint != null;
const signaturesValid =
stamp.signatures.length > 0 &&
stamp.signatures.every(s => s.value && s.signer);
// Add your own signal consistency checks
const signalsConsistent = validateMySignals(stamp.signals);
return {
valid: structureValid && signaturesValid && signalsConsistent,
structureValid,
signaturesValid,
signalsConsistent,
details: {}
};
}
}
```
***
## Step 2: Register with the SDK
```typescript theme={null}
import { AstralSDK } from '@decentralized-geo/astral-sdk';
import { MyLocationPlugin } from './my-location-plugin';
const astral = new AstralSDK({ chainId: 84532, signer: wallet });
astral.plugins.register(new MyLocationPlugin({
apiUrl: 'https://api.my-service.com',
apiKey: process.env.MY_SERVICE_API_KEY
}));
```
The registry validates that the current runtime is in the plugin's `runtimes` array. If not, `register()` throws.
***
## Step 3: Use through the SDK
Once registered, your plugin works with the standard stamps and proofs pipeline:
```typescript theme={null}
// Collect signals
const signals = await astral.stamps.collect({
plugins: ['my-location-service']
});
// Create stamp
const unsigned = await astral.stamps.create(
{ plugin: 'my-location-service' },
signals[0]
);
// Sign stamp
const stamp = await astral.stamps.sign(
{ plugin: 'my-location-service' },
unsigned,
signer
);
// Verify stamp
const result = await astral.stamps.verify(stamp);
// Use in a proof
const proof = astral.proofs.create(claim, [stamp]);
const vector = await astral.proofs.verify(proof);
```
***
## Which methods to implement
| Method | Implement when... |
| ----------- | -------------------------------------------------------------------------------------------- |
| `collect()` | Your system can actively gather evidence (API calls, sensor reads) |
| `create()` | You need to parse raw data into Location Protocol v0.2 format |
| `sign()` | Your system has its own signing mechanism (most plugins skip this — the SDK handles signing) |
| `verify()` | You can validate stamps from your system (signature checks, signal consistency) |
Common patterns:
* **API-based service** (like WitnessChain): implement `collect()`, `create()`, `verify()`
* **Mobile app export** (like ProofMode): implement `create()`, `verify()` (collection happens on-device)
* **Full control**: implement all four methods
***
## Runtime compatibility
Declare which environments your plugin supports:
```typescript theme={null}
readonly runtimes: Runtime[] = ['node']; // Server only
readonly runtimes: Runtime[] = ['browser']; // Browser only
readonly runtimes: Runtime[] = ['react-native']; // Mobile only
readonly runtimes: Runtime[] = ['node', 'browser']; // Both
readonly runtimes: Runtime[] = ['react-native', 'node', 'browser']; // All
```
The SDK detects the current runtime automatically and rejects plugins that don't support it.
***
## Location Protocol v0.2 compliance
Stamps must conform to LP v0.2. Key requirements for `UnsignedLocationStamp`:
| Field | Type | Description |
| ------------------- | -------------------------------- | ------------------------------------------------- |
| `lpVersion` | `string` | Must be `'0.2'` |
| `locationType` | `string` | e.g., `'geojson-point'`, `'h3-index'` |
| `location` | `LocationData` | GeoJSON geometry or string |
| `srs` | `string` | Spatial reference system, typically `'EPSG:4326'` |
| `temporalFootprint` | `{ start: number; end: number }` | Unix seconds |
| `plugin` | `string` | Your plugin name |
| `pluginVersion` | `string` | Semver |
| `signals` | `Record` | Plugin-specific data |
***
## Testing with MockPlugin
Use the MockPlugin as a reference implementation and for testing alongside your plugin:
```typescript theme={null}
import { AstralSDK, MockPlugin } from '@decentralized-geo/astral-sdk';
import { MyLocationPlugin } from './my-location-plugin';
const astral = new AstralSDK({ chainId: 84532, signer: wallet });
// Register both
astral.plugins.register(new MockPlugin({ lat: 37.7749, lon: -122.4194 }));
astral.plugins.register(new MyLocationPlugin({ apiUrl: '...', apiKey: '...' }));
// Collect from both — test multi-stamp proofs
const signals = await astral.stamps.collect();
// Returns signals from both plugins
// Build multi-stamp proof for cross-correlation testing
const mockStamp = /* ... */;
const myStamp = /* ... */;
const proof = astral.proofs.create(claim, [mockStamp, myStamp]);
const vector = await astral.proofs.verify(proof);
// independence.uniquePluginRatio should be 1.0
console.log(vector.dimensions.independence.uniquePluginRatio);
```
# MockPlugin
Source: https://docs.astral.global/plugins/mock
Built-in test plugin for development and testing
# MockPlugin
The MockPlugin is built into the Astral SDK and implements all four plugin methods (collect, create, sign, verify). It generates deterministic location data for testing and development. Runs in all environments: Node.js, browser, and React Native.
## Installation
No separate installation needed — MockPlugin ships with the SDK:
```typescript theme={null}
import { MockPlugin } from '@decentralized-geo/astral-sdk';
```
## Configuration
```typescript theme={null}
interface MockPluginOptions {
name?: string; // Must start with 'mock-' (default: 'mock')
lat?: number; // Latitude (default: 40.7484 — Empire State Building)
lon?: number; // Longitude (default: -73.9857)
jitterMeters?: number; // Random offset in meters (default: 0)
accuracy?: number; // Accuracy in meters (default: 10)
timestamp?: number; // Unix seconds (default: current time)
durationSeconds?: number; // Temporal footprint duration (default: 60)
privateKey?: string; // Deterministic signing key (optional)
}
```
```typescript theme={null}
const mock = new MockPlugin({
lat: 37.7749,
lon: -122.4194,
accuracy: 10,
jitterMeters: 5,
durationSeconds: 120
});
```
## Plugin properties
```typescript theme={null}
mock.name // 'mock'
mock.version // '0.1.0'
mock.runtimes // ['react-native', 'node', 'browser']
mock.description // 'Mock location proof plugin for testing'
```
***
## collect()
Returns simulated GPS-like signals.
```typescript theme={null}
const signals = await mock.collect();
```
Returns `RawSignals`:
```typescript theme={null}
{
plugin: 'mock',
timestamp: 1700000000,
data: {
latitude: 37.7749,
longitude: -122.4194,
accuracy: 10,
altitude: 0,
provider: 'mock',
speed: 0,
bearing: 0
}
}
```
When `jitterMeters` is set, coordinates will vary randomly within the specified radius on each call.
***
## create()
Converts signals into an `UnsignedLocationStamp`.
```typescript theme={null}
const unsigned = await mock.create(signals);
```
Returns:
```typescript theme={null}
{
lpVersion: '0.2',
locationType: 'geojson-point',
location: { type: 'Point', coordinates: [-122.4194, 37.7749] },
srs: 'EPSG:4326',
temporalFootprint: { start: 1700000000, end: 1700000060 },
plugin: 'mock',
pluginVersion: '0.1.0',
signals: { latitude: 37.7749, longitude: -122.4194, ... }
}
```
***
## sign()
Signs a stamp. If `privateKey` was provided in options, uses that key. Otherwise uses the provided signer.
```typescript theme={null}
const stamp = await mock.sign(unsigned, signer);
```
***
## verify()
Validates stamp structure and signals.
```typescript theme={null}
const result = await mock.verify(stamp);
```
Returns `StampVerificationResult`:
```typescript theme={null}
{
valid: true,
signaturesValid: true,
structureValid: true,
signalsConsistent: true,
details: {}
}
```
***
## Full pipeline example
```typescript theme={null}
import { AstralSDK, MockPlugin } from '@decentralized-geo/astral-sdk';
import { ethers } from 'ethers';
const wallet = new ethers.Wallet(process.env.PRIVATE_KEY);
const astral = new AstralSDK({
chainId: 84532,
signer: wallet,
apiUrl: 'https://staging-api.astral.global'
});
// Register MockPlugin
astral.plugins.register(new MockPlugin({
lat: 48.8584,
lon: 2.2945,
accuracy: 15,
jitterMeters: 10
}));
// 1. Collect
const signals = (await astral.stamps.collect({ plugins: ['mock'] }))[0];
// 2. Create
const unsigned = await astral.stamps.create({ plugin: 'mock' }, signals);
// 3. Sign
const stamp = await astral.stamps.sign(
{ plugin: 'mock' },
unsigned,
{
algorithm: 'secp256k1',
signer: { scheme: 'eth-address', value: wallet.address },
sign: (data) => wallet.signMessage(data)
}
);
// 4. Verify
const result = await astral.stamps.verify(stamp);
console.log('Valid:', result.valid);
// 5. Build a proof
const claim = {
lpVersion: '0.2',
locationType: 'geojson-point',
location: { type: 'Point', coordinates: [2.2945, 48.8584] },
srs: 'EPSG:4326',
subject: { scheme: 'eth-address', value: wallet.address },
radius: 50,
time: {
start: Math.floor(Date.now() / 1000) - 120,
end: Math.floor(Date.now() / 1000)
}
};
const proof = astral.proofs.create(claim, [stamp]);
const vector = await astral.proofs.verify(proof);
console.log('Spatial:', vector.dimensions.spatial.meanDistanceMeters, 'm');
console.log('Temporal:', vector.dimensions.temporal.meanOverlap);
```
## Use cases
* **Unit testing** — Deterministic stamps for testing proof verification logic
* **Integration testing** — End-to-end pipeline testing without real devices
* **Development** — Build applications against the full stamp/proof pipeline
* **Demos** — Show the location proof workflow without hardware dependencies
# Plugins Overview
Source: https://docs.astral.global/plugins/overview
Location proof plugins for the Astral SDK
**Research Preview** — The plugin ecosystem is under active development.
# Location proof plugins
Location proof plugins collect and create location evidence. Each plugin integrates a proof-of-location system — a social or technical system that produces evidence about where something was at a given time. Plugins follow a standard interface so the SDK can orchestrate across multiple independent evidence sources.
## How plugins fit in
The Astral location proof pipeline has four stages:
1. **Collect** — Gather raw signals from a proof-of-location system (GPS, network latency, device attestation, etc.)
2. **Create** — Parse raw signals into a structured `UnsignedLocationStamp` conforming to Location Protocol v0.2
3. **Sign** — Add a cryptographic signature to produce a `LocationStamp`
4. **Verify** — Check a stamp's internal validity (signatures, structure, signal consistency)
Not every plugin implements every stage. ProofMode, for example, collects evidence on-device via its mobile app — the plugin handles parsing and verification, not collection. WitnessChain collects via an API. The MockPlugin implements all four for testing. **ProofMode is working today** (its stamps can be verified end to end); the other plugins are experimental, with interfaces defined and early verification logic in the service. Client-side, the SDK ships MockPlugin for development. We're keen to build new proof-of-location plugins with partners.
Once you have signed stamps, the SDK's `ProofsModule` bundles them with a claim and verifies the proof as a whole, producing a multidimensional `CredibilityVector`.
## Plugin status
| Plugin | Package | Status | collect | create | sign | verify |
| ------------------------------------- | -------------------------------------- | -------------- | ----------- | ----------- | ----------- | ---------------- |
| [Mock](/plugins/mock) | Built into SDK | Complete | Yes | Yes | Yes | Yes |
| [Proofmode](/plugins/proofmode) | `@location-proofs/plugin-proofmode` | Alpha | Coming soon | Yes | — | Yes (structural) |
| [WitnessChain](/plugins/witnesschain) | `@location-proofs/plugin-witnesschain` | In development | Coming soon | Coming soon | Coming soon | Coming soon |
## Quick start
```typescript theme={null}
import { AstralSDK, MockPlugin } from '@decentralized-geo/astral-sdk';
const astral = new AstralSDK({ chainId: 84532, signer: wallet });
// Register plugins at startup
astral.plugins.register(new MockPlugin({ lat: 37.7749, lon: -122.4194 }));
// Collect → Create → Sign → Verify
const signals = (await astral.stamps.collect({ plugins: ['mock'] }))[0];
const unsigned = await astral.stamps.create({ plugin: 'mock' }, signals);
const stamp = await astral.stamps.sign({ plugin: 'mock' }, unsigned, signer);
const result = await astral.stamps.verify(stamp);
// Bundle into a proof
const proof = astral.proofs.create(claim, [stamp]);
const vector = await astral.proofs.verify(proof);
```
## Plugin pages
Built-in test plugin for development
Mobile device proofs via the ProofMode app
Infrastructure-based proofs via network triangulation
Build your own proof-of-location plugin
# ProofMode
Source: https://docs.astral.global/plugins/proofmode
Mobile device proofs via the ProofMode app
**Alpha** — The ProofMode plugin is in early development. See below for what's available and what's coming next.
# ProofMode plugin
[ProofMode](https://proofmode.org) is a mobile app (iOS and Android) that collects device evidence — GPS coordinates, network context, device metadata, PGP signatures, and SafetyNet/Play Integrity attestations. The `@location-proofs/plugin-proofmode` package integrates ProofMode into the Astral SDK's standard plugin pipeline and verifies stamp consistency.
## How it works
1. The user captures evidence in the ProofMode app on their phone
2. They export a ZIP bundle from the app
3. Your application passes the ZIP data through the standard SDK pipeline
4. The plugin parses it into an `UnsignedLocationStamp` and can verify its structure and signals
```
ProofMode App → ZIP export → stamps.create() → stamps.sign() → stamps.verify()
```
## Installation
```bash theme={null}
npm install @location-proofs/plugin-proofmode
```
**GitHub:** [github.com/location-proofs/plugin-proofmode](https://github.com/location-proofs/plugin-proofmode)
## Registration
```typescript theme={null}
import { AstralSDK } from '@decentralized-geo/astral-sdk';
import { ProofModePlugin } from '@location-proofs/plugin-proofmode';
const astral = new AstralSDK({ chainId: 84532, signer: wallet });
astral.plugins.register(new ProofModePlugin());
```
## Plugin properties
```typescript theme={null}
plugin.name // 'proofmode'
plugin.version // '0.1.0'
plugin.runtimes // ['node', 'browser']
plugin.description // 'ProofMode device-based location proofs with PGP signatures
// and hardware attestation'
```
***
## Standard pipeline
ProofMode implements the standard `create()` and `verify()` interface methods, so it works with the SDK's `StampsModule`.
### Collecting evidence
Evidence collection happens in the [ProofMode app](https://proofmode.org) on the user's device. The app captures GPS coordinates, sensor data, PGP signatures, and hardware attestations, then exports a ZIP bundle. Your application receives this ZIP and passes it through the SDK pipeline below.
### create()
Pass the ZIP bundle as `signals.data.zipData`:
```typescript theme={null}
import { readFileSync } from 'fs';
const zipData = new Uint8Array(readFileSync('proofmode-export.zip'));
const unsigned = await astral.stamps.create(
{ plugin: 'proofmode' },
{
plugin: 'proofmode',
timestamp: Math.floor(Date.now() / 1000),
data: { zipData }
}
);
```
Under the hood, `create()` parses the ZIP and extracts GPS coordinates, timestamps, PGP signatures, SafetyNet tokens, and device metadata into a structured `UnsignedLocationStamp`.
### verify()
Verify a ProofMode stamp's internal validity:
```typescript theme={null}
const result = await astral.stamps.verify(stamp);
```
Checks performed:
* **Structure** — LP version is `'0.2'`, plugin is `'proofmode'`, location/signals/temporalFootprint present
* **Signatures** — At least one signature exists with non-empty value and signer info
* **Signal consistency:**
* Latitude and longitude are in valid ranges
* Location provider and accuracy are consistent
* SafetyNet JWT structure is valid (base64url-encoded, 3 parts)
* `Location.Time` matches `temporalFootprint` within 3600 seconds
Returns `StampVerificationResult`:
```typescript theme={null}
{
valid: boolean,
signaturesValid: boolean,
structureValid: boolean,
signalsConsistent: boolean,
details: { ... } // ProofMode-specific verification details
}
```
***
## Lower-level API
For more control over the parsing step, you can use the plugin's helpers directly:
### parseBundle()
Parse a ProofMode ZIP export into a structured bundle.
```typescript theme={null}
const plugin = new ProofModePlugin();
const bundle = plugin.parseBundle(zipData);
```
| Parameter | Type | Description |
| --------- | ------------ | --------------------------------------- |
| `zipData` | `Uint8Array` | Raw ZIP file data from ProofMode export |
Returns `ParsedBundle`:
```typescript theme={null}
interface ParsedBundle {
metadata: ProofModeMetadata; // Parsed signal data
publicKey?: string; // ASCII-armored PGP public key
metadataSignature?: Uint8Array; // PGP detached signature of metadata
mediaSignature?: Uint8Array; // PGP detached signature of media
safetyNetToken?: string; // Google SafetyNet/Play Integrity JWT
otsProof?: Uint8Array; // OpenTimestamps proof
mediaFile?: Uint8Array; // The media file data
mediaFileName?: string;
expectedHash?: string; // SHA-256 hash from bundle filename
files: BundleFile[]; // All raw files in bundle
}
```
### createStampFromBundle()
Create an unsigned location stamp from a parsed bundle:
```typescript theme={null}
const unsigned = plugin.createStampFromBundle(bundle);
```
This is what `create()` calls internally after parsing.
***
## What the ZIP bundle contains
A ProofMode export ZIP typically includes:
| File | Description |
| ----------------------------------- | ------------------------------------ |
| `.proof.csv` or `.proof.json` | Location and device metadata signals |
| `pubkey.asc` | ASCII-armored PGP public key |
| `.asc` | PGP detached signatures |
| `.gst` | Google SafetyNet/Play Integrity JWT |
| `.ots` | OpenTimestamps proof |
| Media file | Photo or video captured during proof |
### Signal fields
The metadata file contains 30+ signal fields:
| Category | Fields |
| ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Location | `Location.Latitude`, `Location.Longitude`, `Location.Provider`, `Location.Accuracy`, `Location.Altitude`, `Location.Bearing`, `Location.Speed`, `Location.Time` |
| Network | `CellInfo`, `WiFi.MAC`, `IPv4`, `IPv6`, `Network` |
| Device | `DeviceID`, `Hardware`, `Manufacturer`, `Model` |
| ProofMode | `ProofHash`, `FileHash`, `MimeType`, `File.Name`, `File.Size` |
| Timestamps | `DateCreated`, `Timestamp` |
***
## Verification scope
### What verify checks today
* Stamp structure conforms to Location Protocol v0.2
* Signatures array is non-empty with valid signer info
* GPS coordinates are within valid ranges
* Signal consistency (provider/accuracy, timestamps alignment)
* SafetyNet JWT structure (3-part base64url format)
### Deferred to v1+
* Full PGP cryptographic signature verification (checking signatures against the public key)
* SafetyNet/Play Integrity certificate chain verification
* OpenTimestamps proof verification
***
## Coming soon
**`collect()` — In-app evidence collection** is planned for developers building React Native apps. This will allow triggering ProofMode evidence collection directly from your app, rather than requiring the user to export a ZIP. This is under active development.
***
## Example
```typescript theme={null}
import { AstralSDK } from '@decentralized-geo/astral-sdk';
import { ProofModePlugin } from '@location-proofs/plugin-proofmode';
import { readFileSync } from 'fs';
const astral = new AstralSDK({ chainId: 84532, signer: wallet });
astral.plugins.register(new ProofModePlugin());
// Load a ProofMode ZIP export
const zipData = new Uint8Array(readFileSync('proofmode-export.zip'));
// Create stamp through the standard pipeline
const unsigned = await astral.stamps.create(
{ plugin: 'proofmode' },
{ plugin: 'proofmode', timestamp: Math.floor(Date.now() / 1000), data: { zipData } }
);
console.log('Stamp location:', unsigned.location);
console.log('Stamp time:', unsigned.temporalFootprint);
// Sign the stamp
const stamp = await astral.stamps.sign(
{ plugin: 'proofmode' },
unsigned,
signer
);
// Verify
const result = await astral.stamps.verify(stamp);
console.log('Valid:', result.valid);
console.log('Structure:', result.structureValid);
console.log('Signals consistent:', result.signalsConsistent);
// Use in a proof with stamps from other plugins
const proof = astral.proofs.create(claim, [stamp, otherStamp]);
const vector = await astral.proofs.verify(proof);
```
# WitnessChain
Source: https://docs.astral.global/plugins/witnesschain
Infrastructure-based proof-of-location via network latency triangulation
**Experimental** — WitnessChain is an early-stage plugin. The interface is defined and the Verify service has experimental server-side verification logic for it; the client plugin (collecting and creating stamps) is still under development on the `develop` branch. ProofMode is the plugin that's working end to end today. We'd love to develop this and other proof-of-location plugins with partners — [get in touch](mailto:contact@astral.global).
# WitnessChain plugin
[WitnessChain](https://www.witnesschain.com) provides infrastructure-based proof-of-location through network latency triangulation. Independent challenger nodes measure round-trip times to a prover, then use the speed-of-light constraint to verify the prover's claimed location. This is fundamentally different from device-based proofs like ProofMode — the evidence comes from the network infrastructure, not the user's device.
## How it works
1. A **prover** registers their claimed location with WitnessChain
2. Independent **challenger** nodes send network challenges
3. Challengers measure round-trip latency and compare against speed-of-light bounds
4. If the measured latency is consistent with the claimed location, the challenge succeeds
5. Results include ECDSA-signed attestations from each challenger
This approach provides location evidence that doesn't depend on the prover's device being trustworthy — the evidence comes from independent network observers.
## Current status
The plugin is in development on the `develop` branch. The interface is fully defined:
```typescript theme={null}
class WitnessChainPlugin implements LocationProofPlugin {
readonly name = 'witnesschain';
readonly version = '0.1.0';
readonly runtimes: Runtime[];
readonly requiredCapabilities: string[];
readonly description = 'WitnessChain proof-of-location via network latency triangulation';
collect(options?: CollectOptions): Promise;
create(signals: RawSignals): Promise;
sign(stamp: UnsignedLocationStamp, signer: StampSigner): Promise;
verify(stamp: LocationStamp): Promise;
}
```
### Planned capabilities
| Method | Description | Status |
| ----------- | ------------------------------------------------------------ | -------------- |
| `collect()` | Fetch challenge results from WitnessChain API | In development |
| `create()` | Parse challenge results into location stamp | In development |
| `sign()` | Sign stamp with prover key | In development |
| `verify()` | Verify challenger ECDSA signatures and challenge consistency | In development |
## Challenge result structure
Each WitnessChain challenge produces:
```typescript theme={null}
interface WitnessChainChallengeResult {
id: string;
challenger: string; // Ethereum address of challenger node
claims: { // Prover's claimed location
latitude: number;
longitude: number;
radius: number;
};
result: {
challenge_succeeded: boolean;
ping_delay: number; // Measured latency
};
message: string; // JSON-encoded challenge data
signature: string; // ECDSA signature from challenger
consolidated_result: {
KnowLoc: boolean; // WitnessChain's location assessment
KnowLocUncertainty: number;
verified: boolean;
};
}
```
## Why WitnessChain matters for multifactor proofs
WitnessChain provides a fundamentally independent evidence source:
* **Device proofs** (ProofMode) rely on the prover's hardware
* **Infrastructure proofs** (WitnessChain) rely on external network observers
When both agree, the `independence` dimension of the `CredibilityVector` reflects the corroboration from truly independent systems.
## Links
* **GitHub:** [github.com/location-proofs/plugin-witnesschain](https://github.com/location-proofs/plugin-witnesschain)
* **WitnessChain:** [witnesschain.com](https://www.witnesschain.com)
# Quickstart
Source: https://docs.astral.global/quickstart
Your first location proof and verified spatial computation in 5 minutes
**Research Preview** — Astral is under active development and not yet production-ready. APIs may change. We're building in public and welcome feedback.
This guide walks through both of Astral's core capabilities: creating and verifying a location proof, then running a verified spatial computation with it.
## Step 1: Create a Location Proof
A location proof starts on the device. You collect signals from proof-of-location systems, process them into stamps, sign them, and compose the proof.
```typescript theme={null}
import { AstralSDK, MockPlugin } from '@decentralized-geo/astral-sdk';
const astral = new AstralSDK({
chainId: 84532,
signer: wallet,
apiUrl: 'https://staging-api.astral.global'
});
// 1. Register a proof-of-location plugin. The SDK ships MockPlugin for local
// development; on a real device, evidence comes from a source like the
// ProofMode app (GPS, sensors, hardware-backed signatures).
astral.plugins.register(new MockPlugin({ name: 'mock-1', lat: 37.7749, lon: -122.4194 }));
// 2. Collect raw signals (returns an array), then create an unsigned stamp
const signals = await astral.stamps.collect({ plugins: ['mock-1'] });
const unsigned = await astral.stamps.create({ plugin: 'mock-1' }, signals[0]);
// 3. Sign the stamp with the device key
const stamp = await astral.stamps.sign({ plugin: 'mock-1' }, unsigned, deviceSigner);
// 4. Compose a location proof: a claim ("I was here") bundled with signed stamps
const claim = {
lpVersion: '0.2',
locationType: 'geojson-point',
location: { type: 'Point', coordinates: [-122.4194, 37.7749] },
srs: 'http://www.opengis.net/def/crs/OGC/1.3/CRS84',
subject: { scheme: 'eth-address', value: '0x1234...' },
radius: 100,
time: { start: Date.now() / 1000 - 60, end: Date.now() / 1000 },
};
const proof = astral.proofs.create(claim, [stamp]);
```
Each step adds a layer. The signals are raw sensor data. The stamp processes them into a structured artifact. The signature binds the stamp to a specific identity. The proof bundles the claim with the evidence.
```typescript theme={null}
const claim = {
lpVersion: '0.2',
locationType: 'geojson-point',
location: { type: 'Point', coordinates: [-122.4194, 37.7749] },
srs: 'http://www.opengis.net/def/crs/OGC/1.3/CRS84',
subject: { scheme: 'eth-address', value: '0x1234...' },
radius: 100, // meters
time: { start: Date.now() / 1000 - 60, end: Date.now() / 1000 },
eventType: 'presence' // optional
}
```
See [Location Proofs](/concepts/location-proofs) for the full data model.
## Step 2: Verify the Proof
Submit the location proof to Astral's Verify endpoint. Verification runs inside a TEE — each stamp is checked for signature validity, structural integrity, and consistency with the claim. Multiple stamps from independent systems are cross-correlated to strengthen confidence.
```typescript theme={null}
// `mode: 'tee'` routes verification to the hosted service and returns a
// VerifiedLocationProof; the default 'local' mode returns the vector directly.
const verified = await astral.proofs.verify(proof, { mode: 'tee', chainId: 84532 });
console.log(verified.credibility); // a multidimensional credibility vector
const locationUID = verified.attestation.uid;
```
The result is a **credibility vector** — not a binary yes/no, and not a single score, but a structured assessment of how strongly the evidence supports the claim across several dimensions (its exact structure is still an [open research question](/concepts/location-proof-evaluation)). It tells you how well the evidence backs the claim — not, by itself, that the entity was definitely there. How much that's worth, and what threshold to require, is your application's call.
## Step 3: Run a Spatial Computation
Now use the verified location in a spatial operation. The computation runs inside the same TEE — PostGIS computes the spatial relationship and the TEE signs the result.
```typescript theme={null}
// Arguments are positional: (container, containee, options).
// `schema` is the EAS schema UID the signed result is encoded against.
const inside = await astral.compute.contains(
geofencePolygon, // container
locationUID, // containee — the verified location
{ schema: SCHEMA_UID }
);
console.log(`Inside geofence: ${inside.result}`); // true
```
The signed result proves the computation was performed correctly on the stated inputs. Because the input was a verified location proof, the full chain of trust is preserved: who claimed to be where, the evidence supporting that claim, and the spatial relationship the system computed.
Raw GeoJSON also works — useful for reference geometries like official boundaries or known landmarks. But raw coordinates carry no proof of origin. The computation is still verified, but the inputs are unverified.
## Step 4: Use the Result
The signed result is portable. Use it directly in your application:
```typescript theme={null}
// Agent workflow — branch on the verified spatial answer
if (inside.result) {
confirmDelivery(inside); // the signed result is the audit trail
}
// Backend — store as evidence
await db.insert({ delivery_id, proof: inside });
```
Or submit it onchain. [EAS](https://docs.attest.org/) (the Ethereum Attestation Service) is an open protocol for structured, signed attestations, plus a smart contract to register attestations onchain.
Astral's signed results can be packaged as EAS attestations. EAS supports **resolver contracts** — smart contracts that execute arbitrary logic when an attestation is created onchain. A verified spatial result can then trigger token transfers, access grants, escrow releases, or any other onchain action. (This is one path among several — most applications use signed results offchain, as in Step 4 above.)
```typescript theme={null}
import { ethers } from 'ethers';
const wallet = new ethers.Wallet(PRIVATE_KEY, provider);
const astral = new AstralSDK({
chainId: 84532,
signer: wallet,
apiUrl: 'https://staging-api.astral.global'
});
// Arguments are positional: (geometry, target, radius, options).
const result = await astral.compute.within(
locationUID, // geometry — the verified location
landmarkUID, // target
500, // radius in meters
{ schema: RESOLVER_SCHEMA_UID, recipient: wallet.address }
);
// Submit to EAS — triggers your resolver contract onchain
const { uid } = await astral.compute.submit({
attestation: result.attestation,
delegatedAttestation: result.delegatedAttestation,
});
console.log('Onchain attestation:', uid);
```
For the full blockchain flow — writing resolver contracts, registering schemas, chain configuration — see [Blockchain Integration](/guides/blockchain-integration).
## Next Steps
Understand location data, location proofs, and geocomputation
Walk through common workflows step by step
Full API documentation
TypeScript SDK documentation
# Changelog
Source: https://docs.astral.global/resources/changelog
What's new in Astral
# Changelog
## v0.1.0 — Research Preview
Initial release of the Astral Research Preview.
* Six geocomputation operations: distance, area, length, contains, within, intersects
* TEE execution (current deployment target EigenCompute; not tied to one provider) — validated in test deployments; continuous attested operation is not yet funded (see [What you are trusting](/trust-model/what-you-are-trusting))
* EAS integration for onchain attestation
* TypeScript SDK
* Location proof verification — ProofMode is working (its stamps can be verified end to end); experimental stamp-verification logic for witnesschain, gpsd, geoclue, wifi-mls, and ip-geolocation also exists, with interfaces defined
# FAQ
Source: https://docs.astral.global/resources/faq
Frequently asked questions
**Research Preview** — This project is under active development.
# Frequently Asked Questions
## General
Astral Location Services is verifiable location infrastructure. It does two things: it **verifies location proofs** (via the `/verify` endpoint — evaluating evidence about where something was), and it runs **geospatial computation** (distance, containment, intersection) inside a trusted execution environment. Both produce signed results that any downstream system can verify independently — an agent, a backend, a compliance report, or a smart contract.
A normal geospatial API returns an answer you have to take on faith. Astral signs the result inside a TEE, so anyone can check that the computation was performed correctly on the stated inputs — without re-running it or trusting the server. (This is a hardware-backed attestation, not a zero-knowledge proof — and it depends on the service running under attestation; see the [trust model](/trust-model/what-you-are-trusting) for current deployment status.)
Most uses are entirely offchain:
* Delivery and field-service verification
* Compliance and audit trails (e.g. an asset stayed within an approved area)
* Spatial decisions for autonomous agents, with a verifiable record
* Parametric triggers based on verified proximity
The same signed results are also natively compatible with Ethereum smart contracts, for onchain use cases (proof-of-visit, geofenced access, and so on). See [Use Cases](/use-cases) for detailed examples.
No. Astral Location Services is under active development. APIs may change. We're building in public and welcome feedback!
## Technical
**Supported operations:**
* `distance` - Distance between two geometries (meters)
* `contains` - Is geometry B inside geometry A?
* `within` - Is point within radius of target?
* `intersects` - Do geometries overlap?
* `area` - Area of a polygon (square meters)
* `length` - Length of a line (meters)
**Future operations:**
* `buffer`, `centroid`, `union`, `intersection`, `disjoint`
v0 targets **Base Sepolia** (chain ID 84532). Additional chains (Base Mainnet, Ethereum Sepolia/Mainnet) are planned.
1. Operations run in EigenCompute's TEE (Trusted Execution Environment)
2. Under remote attestation, the TEE attests that specific code executed on specific inputs
3. Results are signed with a key held inside the TEE
4. Smart contracts verify `attestation.attester == astralSigner`
Note: continuous attested operation is not yet funded — today a valid signature proves Astral's key produced the result. See the [trust model](/trust-model/what-you-are-trusting).
**Today:** A centralized service with TEE execution attestation. You are trusting Astral (the operator) and the TEE — and, to some extent, the TEE manufacturer.
**Future:** AVS consensus (multiple operators), ZK proofs, decentralized signing.
Yes. GPS is spoofable. Astral verifies that **computations are correct**, not that **inputs are authentic**. If a user provides a fake GPS coordinate, we'll compute on that fake coordinate.
We've developed a framework for [multifactor location proofs](https://collective.flashbots.net/t/towards-stronger-location-proofs/5323), which compose evidence from multiple corroborating proof-of-location systems to support a location claim. The aim is to raise the cost of spoofing, not to make it impossible. ProofMode is working today; experimental stamp-verification logic for several other systems (witnesschain, gpsd, geoclue, wifi-mls, ip-geolocation) also exists, with interfaces defined. We're keen to develop new proof-of-location plugins with partners. As these mature, they plug into Astral for stronger verification.
## Integration
No. Astral operates the service. You use the SDK (or call the API directly) to get signed results; if you want them onchain, you submit them to your contracts yourself.
You do. The delegated attestation pattern means:
* Astral signs the attestation offchain
* You submit with Astral's signature (paying gas)
* EAS records Astral as the attester
This lets you control costs and timing.
No — and most uses don't. Signed results are produced offchain and used directly in your application; that's the primary path. A blockchain is only involved if you choose to submit a result onchain.
Yes — signed results are natively compatible with Ethereum smart contracts. Use an EAS resolver:
```solidity theme={null}
function onAttest(Attestation calldata attestation, uint256)
internal override returns (bool)
{
require(attestation.attester == astralSigner, "Not from Astral");
(bool result, , , ) = abi.decode(attestation.data, (bool, bytes32[], uint64, string));
require(result, "Location check failed");
// ... your logic
}
```
Complementary!
* **Turf.js**: Client-side, instant, free, unverified
* **Astral**: Server-side, verified, signed result
Use Turf for UX (showing distance in real-time), and Astral when you need a result you can verify (and, if you want, submit onchain).
## Data
GeoJSON standard: **\[longitude, latitude]** in WGS84 (EPSG:4326).
```typescript theme={null}
// San Francisco
const sf = { type: 'Point', coordinates: [-122.4194, 37.7749] };
```
Metric only:
* Distance/length: **meters**
* Area: **square meters**
* Radius (in `within`): **meters**
No unit conversion options. Convert client-side if needed.
Both work! You can pass:
* Attestation UIDs (verified, traceable)
* Raw GeoJSON (unverified, for reference data or prototyping)
For security-sensitive operations, prefer attestation UIDs.
* **Location records**: On EAS (onchain) or IPFS/your storage (offchain)
* **Signed results**: Returned to you; optionally submitted to EAS as attestations
* **Compute service**: Stateless, no persistent storage
## Getting Help
* **Documentation**: You're here!
* **GitHub**: [astral-location-services](https://github.com/AstralProtocol/astral-location-services)
* **Telegram**: [Join our community](https://t.me/+UkTOSXnDcDM5ZTBk)
We're building in public:
* Open issues with feedback
* Share your use cases
* Submit PRs for improvements
Return to the introduction
# Playground
Source: https://docs.astral.global/resources/playground
Interactive tool for exploring geospatial operations
The [Astral Playground](https://playground.astral.global) lets you experiment with geospatial operations before integrating them into your application.
## Modes
### Preview Mode
Test operations with instant visual feedback. Results are computed locally using Turf.js for a quick approximation.
Preview mode results may not exactly match verified results due to differences in how Turf.js and PostGIS perform geospatial computations, but they should be approximately the same.
1. Select an operation from the dropdown
2. Drag markers or draw shapes on the map
3. See the preview result update in real-time
4. Click **Get Verified Result** to compute via PostGIS
### Policy Builder Mode
Create and publish attestations onchain.
1. Switch to **Policy Builder** tab
2. Connect your wallet
3. Select your target chain (Base Sepolia, Sepolia, Base, or Ethereum)
4. Enter a schema UID (defaults provided for numeric/boolean operations)
5. Configure your geometries
6. Click **Get Verified Result** to compute
7. Click **Publish to Chain** to submit the attestation via EAS
## Operations
| Operation | What it does | Inputs |
| -------------- | ------------------------------------ | --------------------- |
| **Distance** | Meters between two points | 2 points |
| **Within** | Is point A within radius of point B? | 2 points + radius (m) |
| **Contains** | Is the point inside the polygon? | polygon + point |
| **Intersects** | Do geometries overlap? | 2 geometries |
| **Area** | Square meters of a polygon | polygon |
| **Length** | Meters of a line | linestring |
## Working with the Map
**Points** — Drag markers directly on the map.
**Polygons & Lines** — Enable **map editing** to use drawing tools:
* Click the polygon/line tool in the top-left
* Click to place vertices
* Double-click to finish
* Click vertices to edit, drag to move
## GeoJSON Editor
The **GeoJSON** tab lets you paste or edit geometries directly:
```json theme={null}
{
"type": "Point",
"coordinates": [-122.4194, 37.7749]
}
```
Supported types: `Point`, `Polygon`, `LineString`.
## Code Snippets
The **SDK** and **cURL** tabs show integration code that updates as you configure your operation. Copy these directly into your project.
Ready to integrate? Follow the quickstart guide.
# Research
Source: https://docs.astral.global/resources/research
Papers, research agenda, and academic context
# Research
Astral sits at the intersection of geospatial science, cryptography, and trusted computing. We publish our research openly and welcome academic collaboration.
## Papers
### Towards stronger location proofs
Our foundational paper on composable location proofs — how to combine evidence from multiple proof-of-location systems into credible, verifiable claims about physical presence.
[Read on Flashbots Collective](https://collective.flashbots.net/t/towards-stronger-location-proofs/5323)
## Research agenda
We're working on three interconnected problems:
### Composable location proofs
How do you combine evidence from independent proof-of-location systems (device attestation, network triangulation, institutional records) into a structured credibility assessment? Our [location proofs](/concepts/location-proofs) framework approaches this by defining claims, stamps, and multi-factor verification. The exact structure of the resulting credibility vector — its dimensions and how they're computed — is itself an open research question.
### Verifiable geocomputation
How do you prove that a spatial computation (distance, containment, intersection) was performed correctly on specific inputs? Today we use TEE execution with signed results. Future work explores zero-knowledge proofs for spatial predicates.
### Spatial accountability for autonomous systems
As autonomous agents (drones, vehicles, robots) operate in physical space, how do you create verifiable records of where they were and what spatial constraints they respected? This connects geofence compliance, corridor verification, and auditable spatial logs.
## Open source
Astral's core infrastructure is developed in the open, and more is being opened as the Research Preview matures. Contributions, issues, and research collaborations are welcome.
[GitHub](https://github.com/AstralProtocol)
## Get involved
If you're working on related problems — verifiable computation, location privacy, spatial data integrity, or autonomous systems accountability — we'd like to hear from you. Open an issue on GitHub or reach out through the channels listed there.
# Roadmap
Source: https://docs.astral.global/resources/roadmap
What's built, what's coming, and what's missing
**Research Preview** — This project is under active development.
# Roadmap
Astral is infrastructure for verifiable location: signed answers to spatial questions, and evidence-based verification of location claims. (Smart contracts are one place those signed results can go, not the whole story.) This page tracks what's done, what we're working on, and what's planned.
## Current Status
Complete technical specification defining architecture, API, SDK, and schemas
Building the compute service, SDK extensions, and example contracts
Integration testing with EigenCompute and EAS
Testnet deployment and public launch
***
## What's Built
Complete spec defining architecture, API design, SDK structure, and security model
Core SDK with location and compute namespaces defined
EAS schema designs for Location and Policy attestations
This documentation site
***
## In Progress
| Component | Status | Notes |
| --------------------- | ----------------- | ---------------------------------------------------------------------------- |
| Compute Service API | Running (staging) | Express/Fastify service |
| PostGIS Integration | Running (staging) | Docker container setup |
| SDK Compute Extension | Built | TypeScript client |
| EigenCompute (TEE) | Test deployments | Validated on real TEE hardware; continuous attested operation not yet funded |
| Example Contracts | In Development | Resolver templates |
***
## Planned Features
### Phase 1: v0
* [x] Core operations: `distance`, `contains`, `within`, `intersects`, `area`, `length`
* [x] PostGIS-powered computations
* [x] EigenCompute TEE test deployments (continuous attested operation not yet funded)
* [x] Delegated attestation signing
* [x] SDK with compute namespace
* [ ] Basic rate limiting (IP-based)
### Phase 2: Authentication
* [ ] Wallet-based request signing
* [ ] Per-wallet rate limiting
* [ ] Higher limits for authenticated users
### Phase 3: Extended Operations
* [ ] `buffer` - Create buffer zone around geometry
* [ ] `centroid` - Find center point
* [ ] `union` - Merge geometries
* [ ] `intersection` - Overlapping area
* [ ] `disjoint` - Geometries don't touch
### Phase 4: Location Proofs (Research in Progress)
We're actively researching location proof mechanisms. See the [Location Proof framework design](https://collective.flashbots.net/t/towards-stronger-location-proofs/5323).
* [x] Framework design and specification
* [x] First working plugin (ProofMode — verification end to end)
* [x] Experimental plugin interfaces defined (WitnessChain, gpsd, GeoClue, Wi-Fi/MLS, IP geolocation)
* [ ] Mature the experimental plugins; develop new ones with partners
* [ ] Multi-stamp verification
* [ ] Sensor fusion (accelerometer, barometer)
* [ ] Physical proof integration (NFC/QR)
### Phase 5: Decentralization
* [ ] AVS consensus (multiple operators)
* [ ] Cryptoeconomic security
* [ ] ZK proof generation (optional)
* [ ] Decentralized signer rotation
***
## What's Missing (v0)
These are explicitly **out of scope** for v0:
| Feature | Status | Notes |
| -------------------------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| Full location proof verification | In progress | ProofMode verification works; other plugins' verification logic is experimental; multi-stamp correlation and hardening are ongoing |
| Gas sponsorship | Deferred | Developers pay via delegated attestations |
| Reference data caching | Deferred | Start stateless, add if needed |
| Complex geometry limits | Deferred | Add based on usage patterns |
| Cross-chain operations | Not planned | Single chain per SDK instance |
| Unit conversion | Not planned | Metric only |
***
## Research Questions
These are open questions being investigated:
1. **Nonce/Replay Protection**
* Should Policy Attestations include explicit nonce?
* Current design relies on unique UIDs + timestamps
2. **EigenCompute Networking**
* Verify TEE allows outbound for EAS queries
* Understand latency characteristics
3. **PostGIS in TEE**
* Validate performance in TEE environment
* Benchmark spatial operations
4. **Deterministic Geocompute**
* Ensuring identical results across runs
* Floating point precision handling
* GEOS version pinning
***
## Contributing
We're building in public! Ways to contribute:
* **Feedback**: Open issues with suggestions or questions
* **Use cases**: Share what you'd build with location-based contracts
* **Code**: Contributions welcome — see the [GitHub org](https://github.com/AstralProtocol)
Threat model, known limitations, and responsible disclosure
# Schemas
Source: https://docs.astral.global/resources/schemas
EAS schema definitions for Astral attestations
**Research Preview** — Additional schema UIDs will be published as they are deployed.
# EAS Schemas
Astral Location Services uses the Ethereum Attestation Service (EAS) for all attestations. This page documents the schema definitions.
## Location Attestation Schema
Used for storing spatial data (points, polygons, routes).
```
bytes geometry, string geometryType, string srs, bytes properties
```
| Field | Type | Description |
| -------------- | ------ | ----------------------------------------------- |
| `geometry` | bytes | Encoded GeoJSON geometry |
| `geometryType` | string | "Point", "Polygon", "LineString", etc. |
| `srs` | string | Spatial reference system (default: "EPSG:4326") |
| `properties` | bytes | JSON-encoded metadata |
***
## Policy Attestation Schemas
Output schemas for geospatial computations.
### BooleanPolicyAttestation
For predicate operations (`contains`, `within`, `intersects`).
```
bool result, bytes32[] inputRefs, uint64 timestamp, string operation
```
| Field | Type | Description |
| ----------- | ---------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `result` | bool | The boolean computation result |
| `inputRefs` | bytes32\[] | References to inputs (UIDs or hashes) |
| `timestamp` | uint64 | Unix timestamp of computation |
| `operation` | string | Operation with parameters (e.g., "within:50000", "contains") — the `within` radius is encoded in centimeters; see the note below |
### NumericPolicyAttestation
For measurement operations (`distance`, `length`, `area`).
```
uint256 result, string units, bytes32[] inputRefs, uint64 timestamp, string operation
```
| Field | Type | Description |
| ----------- | ---------- | ---------------------------------------------------- |
| `result` | uint256 | Scaled integer result (centimeters or cm²) |
| `units` | string | Base unit ("meters" or "square\_meters") |
| `inputRefs` | bytes32\[] | References to inputs |
| `timestamp` | uint64 | Unix timestamp of computation |
| `operation` | string | Operation with parameters (e.g., "distance", "area") |
### GeometryPolicyAttestation (Future)
For transformation operations (`buffer`, `centroid`, `union`).
```
bytes geometry, string geometryType, bytes32[] inputRefs, uint64 timestamp, string operation
```
**Operation strings include parameters** — For example, the `within` operation returns `"within:50000"` (radius in centimeters), not just `"within"`. Resolver contracts should use prefix matching when validating operations.
***
## Default Schema UIDs
Astral provides pre-registered schemas without resolver contracts for general use. Use these when you don't need custom validation logic.
### Base Sepolia (84532)
| Schema | UID | Use For |
| ------------------------ | -------------------------------------------------------------------- | ---------------------------------- |
| BooleanPolicyAttestation | `0x4958625091a773dcfb37a1c33099a378f32a975a7fb61f33d53c4be7589898f5` | `contains`, `within`, `intersects` |
| NumericPolicyAttestation | *Register your own* | `distance`, `length`, `area` |
| Location | *Register your own* | Location attestations |
The BooleanPolicyAttestation schema UID above is the default used by the staging API. For production use or custom resolver logic, register your own schema.
**Custom schemas**: If you need a resolver contract for custom validation (like the Location-Gated NFT), register your own schema with the same field types. The schema UID will be different, but the data encoding is identical.
***
## Input References
The `inputRefs` array contains a `bytes32` for each input:
| Input Type | Reference |
| -------------------- | -------------------------------- |
| Onchain attestation | The UID |
| Offchain attestation | The UID |
| Raw GeoJSON | `keccak256(abi.encode(geojson))` |
This enables verification that specific inputs were used:
```solidity theme={null}
(bool result, bytes32[] memory inputRefs, , ) = abi.decode(...);
// Verify expected landmark was checked
require(inputRefs[1] == EXPECTED_LANDMARK_UID, "Wrong location");
```
***
## Result Scaling
Numeric results are stored as scaled integers:
| Measurement | Storage | Conversion |
| ----------- | ------------------ | ----------------------- |
| Distance | centimeters | `meters = result / 100` |
| Length | centimeters | `meters = result / 100` |
| Area | square centimeters | `m² = result / 10000` |
```solidity theme={null}
// In your resolver
(uint256 resultCm, , , , ) = abi.decode(...);
uint256 meters = resultCm / 100;
```
***
## Decoding in Solidity
### Boolean Policy
```solidity theme={null}
function decodeBooleanPolicy(bytes memory data) public pure returns (
bool result,
bytes32[] memory inputRefs,
uint64 timestamp,
string memory operation
) {
return abi.decode(data, (bool, bytes32[], uint64, string));
}
```
### Numeric Policy
```solidity theme={null}
function decodeNumericPolicy(bytes memory data) public pure returns (
uint256 result,
string memory units,
bytes32[] memory inputRefs,
uint64 timestamp,
string memory operation
) {
return abi.decode(data, (uint256, string, bytes32[], uint64, string));
}
```
***
## Schema Registry
Schemas are registered in the EAS SchemaRegistry. When deploying your resolver:
```typescript theme={null}
import { SchemaRegistry } from '@ethereum-attestation-service/eas-sdk';
const registry = new SchemaRegistry(REGISTRY_ADDRESS);
// Register schema with your resolver
// IMPORTANT: Use revocable: true - Astral signs with revocable=true
const tx = await registry.connect(signer).register({
schema: "bool result,bytes32[] inputRefs,uint64 timestamp,string operation",
resolverAddress: yourResolver.address,
revocable: true
});
const receipt = await tx.wait();
const schemaUID = receipt.logs[0].args.uid;
```
Threat model, known limitations, and responsible disclosure
# Staging
Source: https://docs.astral.global/resources/staging
Test against the staging API on Base Sepolia
## Base URL
```
https://staging-api.astral.global
```
## Endpoints
```bash theme={null}
# Health check
curl https://staging-api.astral.global/health
# API info
curl https://staging-api.astral.global/
```
## Configuration
| Setting | Value |
| ---------------- | -------------------------------------------- |
| Chain | Base Sepolia (84532) |
| EAS Contract | `0x4200000000000000000000000000000000000021` |
| Attester Address | `0x590fdb53ed3f0B52694876d42367192a5336700F` |
## Example Request
```bash theme={null}
curl -X POST https://staging-api.astral.global/compute/v0/distance \
-H "Content-Type: application/json" \
-d '{
"chainId": 84532,
"from": {"type": "Point", "coordinates": [-122.4194, 37.7749]},
"to": {"type": "Point", "coordinates": [-73.9352, 40.7128]}
}'
```
`chainId` only tells the service which EAS chain to format the delegated attestation for, so the result *could* be submitted onchain later. Nothing here touches a blockchain unless you choose to submit the result yourself — the call above just returns a signed result.
## Verifying signatures
**What a matching signature proves.** Checking the attester address proves the result was signed by a key Astral controls. It does **not**, on its own, prove the result was produced inside an independently attested TEE enclave: Astral has run the service on real TEE hardware in test deployments but does not currently fund continuous attested operation. Treat the staging signer as "Astral's staging key," not as a hardware-attestation guarantee. To evaluate against real TEEs, reach out at [contact@astral.global](mailto:contact@astral.global).
All attestations from the staging service are signed by the attester address above. To verify a result came from Astral's staging environment:
```solidity theme={null}
require(attestation.attester == 0x590fdb53ed3f0B52694876d42367192a5336700F, "Not from Astral staging");
```
```typescript theme={null}
const ASTRAL_STAGING_SIGNER = '0x590fdb53ed3f0B52694876d42367192a5336700F';
// Verify the EAS attestation's attester field matches
```
## Notes
* No authentication required
* Rate limited to 100 requests/hour per IP
* Attestations are signed with the staging key — a different key will be used for production
# Compute Module
Source: https://docs.astral.global/sdk/compute
Verifiable geospatial computation methods
**Research Preview** — The SDK is under active development.
# Compute module
The `ComputeModule` provides geospatial operations designed to run in a trusted execution environment, so the computation can be verified independently. Each operation returns a signed result (a delegated attestation that can be submitted to EAS). Note that the TEE makes the *computation* verifiable, not the input locations truthful, and continuous attested operation is not yet funded — see the [trust model](/trust-model/what-you-are-trusting).
```typescript theme={null}
import { AstralSDK } from '@decentralized-geo/astral-sdk';
const astral = new AstralSDK({
chainId: 84532,
signer: wallet,
apiUrl: 'https://staging-api.astral.global'
});
// Access via astral.compute
astral.compute.distance(from, to, options);
astral.compute.area(geometry, options);
astral.compute.length(geometry, options);
astral.compute.contains(container, containee, options);
astral.compute.within(geometry, target, radius, options);
astral.compute.intersects(a, b, options);
astral.compute.submit(input);
astral.compute.estimate(attestation);
astral.compute.health();
```
***
## Input types
All compute methods accept flexible input types:
```typescript theme={null}
type Input =
| string // Onchain attestation UID
| GeoJSON.Geometry // Raw GeoJSON geometry
| { uid: string } // Onchain reference
| { uid: string; uri: string } // Offchain reference
| { verifiedProof: VerifiedLocationProof } // Verified proof input
```
### Examples
```typescript theme={null}
// Using attestation UID
const result = await astral.compute.distance(
'0x1234...abcd',
'0x5678...efgh',
options
);
// Using raw GeoJSON
const result = await astral.compute.distance(
{ type: 'Point', coordinates: [2.2945, 48.8584] },
{ type: 'Point', coordinates: [-0.1276, 51.5074] },
options
);
// Mixing UIDs and GeoJSON
const result = await astral.compute.contains(
boundaryUID,
{ type: 'Point', coordinates: [lon, lat] },
options
);
```
Raw GeoJSON is not verified for authenticity. The `inputRefs` will contain a keccak256 hash of the geometry rather than a UID.
***
## Compute options
All spatial methods **require** options:
```typescript theme={null}
interface ComputeOptions {
schema: string; // Required: EAS schema UID for the result
recipient?: string; // Optional: recipient address (defaults to zero address)
}
```
### Schema requirements
**Use the correct schema type** — Compute operations encode data differently than location attestations. Using the wrong schema UID causes a data/schema mismatch where the attestation is stored but cannot be decoded correctly.
| Operation | Required schema type |
| ---------------------------------- | ------------------------ |
| `distance`, `area`, `length` | NumericPolicyAttestation |
| `contains`, `within`, `intersects` | BooleanPolicyAttestation |
```typescript theme={null}
// CORRECT: Boolean operation with boolean schema
await compute.within(point, target, 500, {
schema: BOOLEAN_POLICY_SCHEMA_UID
});
// WRONG: Boolean operation with location schema
await compute.within(point, target, 500, {
schema: LOCATION_SCHEMA_UID
});
// The attestation is created but data is unreadable!
```
See [Schemas](/resources/schemas) for schema definitions and default UIDs.
***
## Numeric operations
### distance()
Calculate the geodesic distance between two geometries.
```typescript theme={null}
astral.compute.distance(
from: Input,
to: Input,
options: ComputeOptions
): Promise
```
```typescript theme={null}
const result = await astral.compute.distance(
userLocationUID,
landmarkUID,
{ schema: SCHEMA_UID }
);
console.log(result.result); // 523.45
console.log(result.units); // 'meters'
console.log(result.operation); // 'distance'
```
### area()
Calculate the area of a polygon.
```typescript theme={null}
astral.compute.area(
geometry: Input,
options: ComputeOptions
): Promise
```
```typescript theme={null}
const result = await astral.compute.area(
propertyBoundaryUID,
{ schema: SCHEMA_UID }
);
console.log(result.result); // 5432.10
console.log(result.units); // 'square_meters'
```
### length()
Calculate the length of a LineString.
```typescript theme={null}
astral.compute.length(
geometry: Input,
options: ComputeOptions
): Promise
```
```typescript theme={null}
const result = await astral.compute.length(
routeUID,
{ schema: SCHEMA_UID }
);
console.log(result.result); // 2345.67
console.log(result.units); // 'meters'
```
***
## Boolean operations
### contains()
Check if a container geometry contains another geometry.
```typescript theme={null}
astral.compute.contains(
container: Input,
containee: Input,
options: ComputeOptions
): Promise
```
```typescript theme={null}
const result = await astral.compute.contains(
geofencePolygonUID,
userLocationUID,
{ schema: SCHEMA_UID }
);
if (result.result) {
console.log('User is inside the geofence!');
await astral.compute.submit(result.delegatedAttestation);
}
```
### within()
Check if a geometry is within a specified radius of a target.
```typescript theme={null}
astral.compute.within(
geometry: Input,
target: Input,
radius: number, // meters
options: ComputeOptions
): Promise
```
```typescript theme={null}
const result = await astral.compute.within(
userLocationUID,
landmarkUID,
500,
{ schema: SCHEMA_UID, recipient: userAddress }
);
if (result.result) {
console.log('User is within 500m of the landmark!');
await astral.compute.submit(result.delegatedAttestation);
}
```
### intersects()
Check if two geometries intersect.
```typescript theme={null}
astral.compute.intersects(
a: Input,
b: Input,
options: ComputeOptions
): Promise
```
```typescript theme={null}
const result = await astral.compute.intersects(
territory1UID,
territory2UID,
{ schema: SCHEMA_UID }
);
console.log('Territories overlap:', result.result);
```
***
## Submission methods
### submit()
Submit a delegated attestation to EAS. Accepts two input formats:
```typescript theme={null}
// Format 1: DelegatedAttestation directly
astral.compute.submit(
attestation: DelegatedAttestation
): Promise
// Format 2: Object with both attestation types
astral.compute.submit(
input: {
attestation: AttestationObject;
delegatedAttestation: DelegatedAttestationObject;
}
): Promise
```
```typescript theme={null}
const result = await astral.compute.within(
userLocationUID, landmarkUID, 500, { schema: SCHEMA_UID }
);
if (result.result) {
const submission = await astral.compute.submit(result.delegatedAttestation);
console.log('Attestation UID:', submission.uid);
}
```
### estimate()
Estimate gas for attestation submission.
```typescript theme={null}
astral.compute.estimate(
attestation: DelegatedAttestation
): Promise
```
```typescript theme={null}
const gas = await astral.compute.estimate(result.delegatedAttestation);
console.log('Estimated gas:', gas.toString());
if (gas < 200000n) {
await astral.compute.submit(result.delegatedAttestation);
}
```
### health()
Check the compute service health status.
```typescript theme={null}
astral.compute.health(): Promise
```
```typescript theme={null}
const status = await astral.compute.health();
console.log('Service status:', status.status);
console.log('Database status:', status.database);
```
***
## Return types
### NumericComputeResult
```typescript theme={null}
interface NumericComputeResult {
result: number;
units: string; // 'meters' or 'square_meters'
operation: string; // 'distance', 'area', or 'length'
timestamp: number;
inputRefs: string[]; // UIDs or hashes of inputs
attestation: AttestationObject;
delegatedAttestation: DelegatedAttestationObject;
proofInputs?: ProofInputContext[]; // When using VerifiedLocationProof inputs
}
```
### BooleanComputeResult
```typescript theme={null}
interface BooleanComputeResult {
result: boolean;
operation: string; // 'contains', 'within', or 'intersects'
timestamp: number;
inputRefs: string[];
attestation: AttestationObject;
delegatedAttestation: DelegatedAttestationObject;
proofInputs?: ProofInputContext[];
}
```
### DelegatedAttestation
```typescript theme={null}
interface DelegatedAttestation {
message: {
schema: string;
recipient: string;
expirationTime: bigint;
revocable: boolean;
refUID: string;
data: string;
value: bigint;
nonce: bigint;
deadline: bigint;
};
signature: {
v: number;
r: string;
s: string;
};
attester: string;
}
```
***
## How delegated attestations work
| Step | Who | Does what |
| ---- | -------------- | ---------------------------------------------- |
| 1 | **Developer** | Calls `compute.within()` or other method |
| 2 | **Astral API** | Runs computation in TEE, signs attestation |
| 3 | **Developer** | Receives signed `delegatedAttestation` |
| 4 | **Developer** | Calls `compute.submit()` (pays gas) |
| 5 | **EAS** | Verifies signature, records Astral as attester |
| 6 | **Resolver** | Checks `attestation.attester == astralSigner` |
This pattern means:
* Astral does not need to pay gas for every computation
* Developers control when/whether to submit onchain
* Smart contracts can verify attestations came from Astral
***
## Signature expiry
Delegated attestation signatures have a deadline:
```typescript theme={null}
const result = await astral.compute.within(uid1, uid2, 500, options);
const deadline = result.delegatedAttestation.message.deadline;
const now = BigInt(Math.floor(Date.now() / 1000));
if (now < deadline) {
await astral.compute.submit(result.delegatedAttestation);
} else {
// Signature expired — request new computation
const newResult = await astral.compute.within(uid1, uid2, 500, options);
await astral.compute.submit(newResult.delegatedAttestation);
}
```
***
## Complete example
```typescript theme={null}
import { AstralSDK } from '@decentralized-geo/astral-sdk';
import { ethers } from 'ethers';
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://staging-api.astral.global'
});
// Create a location attestation
const location = await astral.location.onchain.create({
location: { type: 'Point', coordinates: [2.2951, 48.8580] },
memo: 'Near Eiffel Tower'
});
// Check proximity to a landmark
const LANDMARK_UID = '0x...';
const SCHEMA_UID = '0x...';
const result = await astral.compute.within(
location.uid,
LANDMARK_UID,
500,
{ schema: SCHEMA_UID, recipient: wallet.address }
);
console.log('Within 500m:', result.result);
// Estimate gas and submit if check passed
if (result.result) {
const gas = await astral.compute.estimate(result.delegatedAttestation);
console.log('Estimated gas:', gas.toString());
const submission = await astral.compute.submit(result.delegatedAttestation);
console.log('Attestation submitted:', submission.uid);
}
```
Learn how the SDK integrates with EAS
# EAS Integration
Source: https://docs.astral.global/sdk/eas
Ethereum Attestation Service integration
**Research Preview** — The SDK is under development.
# EAS Integration
The Astral SDK integrates deeply with the Ethereum Attestation Service (EAS) for both location attestations and geospatial computation results.
## Overview
Astral uses EAS in two ways:
1. **Location Attestations**: Store location data as EAS attestations (offchain or onchain)
2. **Compute Results**: Store verified computation results via delegated attestations
***
## Delegated Attestations
The compute module uses the EAS delegated attestation pattern for submitting results onchain.
### How It Works
```mermaid theme={null}
sequenceDiagram
participant Dev as Developer
participant API as Astral API (TEE)
participant EAS as EAS Contract
participant Resolver as Your Resolver
Dev->>API: compute.within(location, landmark, radius)
API->>API: Run PostGIS computation
API->>API: Sign attestation with TEE key
API-->>Dev: delegatedAttestation
Dev->>EAS: compute.submit() (pays gas)
EAS->>EAS: Verify signature
EAS->>Resolver: onAttest(attestation)
Resolver->>Resolver: Check attester == astralSigner
Resolver->>Resolver: Execute business logic
```
| Step | Who | Action |
| ---- | ------------ | --------------------------------------------------- |
| 1 | Developer | Calls `astral.compute.within()` or similar |
| 2 | Astral API | Runs computation in TEE, signs attestation |
| 3 | Developer | Receives `delegatedAttestation` |
| 4 | Developer | Calls `astral.compute.submit()` (pays gas) |
| 5 | EAS Contract | Verifies signature, records Astral as attester |
| 6 | Resolver | Verifies `attester == astralSigner`, executes logic |
***
## Submitting Delegated Attestations
### Basic Submission
```typescript theme={null}
import { AstralSDK } from '@decentralized-geo/astral-sdk';
const astral = new AstralSDK({
chainId: 84532,
signer: wallet,
apiUrl: 'https://staging-api.astral.global'
});
// Compute a policy check
const result = await astral.compute.within(
userLocationUID,
landmarkUID,
500,
{ schema: SCHEMA_UID, recipient: userAddress }
);
// Submit to EAS if check passed
if (result.result) {
const submission = await astral.compute.submit(result.delegatedAttestation);
console.log('Attestation UID:', submission.uid);
}
```
### Gas Estimation
```typescript theme={null}
// Estimate gas before submitting
const gas = await astral.compute.estimate(result.delegatedAttestation);
console.log('Estimated gas:', gas.toString());
// Submit with confidence
if (gas < 300000n) {
await astral.compute.submit(result.delegatedAttestation);
}
```
***
## Delegated Attestation Structure
The `delegatedAttestation` object contains everything needed for submission:
```typescript theme={null}
interface DelegatedAttestation {
message: {
schema: string; // EAS schema UID
recipient: string; // Attestation recipient
expirationTime: bigint; // When attestation expires
revocable: boolean; // Whether can be revoked
refUID: string; // Reference UID (empty for new)
data: string; // Encoded attestation data
value: bigint; // ETH value (usually 0)
nonce: bigint; // Attester nonce
deadline: bigint; // Signature deadline
};
signature: {
v: number;
r: string;
s: string;
};
attester: string; // Astral signer address
}
```
***
## Signature Expiry
Delegated attestation signatures have a deadline. Check before submitting:
```typescript theme={null}
const result = await astral.compute.within(uid1, uid2, 500, options);
// Check deadline
const deadline = result.delegatedAttestation.message.deadline;
const now = BigInt(Math.floor(Date.now() / 1000));
if (now < deadline) {
// Signature still valid
await astral.compute.submit(result.delegatedAttestation);
} else {
// Signature expired - request new computation
const newResult = await astral.compute.within(uid1, uid2, 500, options);
await astral.compute.submit(newResult.delegatedAttestation);
}
```
***
## Verifying in Smart Contracts
Resolver contracts can verify attestations came from Astral:
```solidity theme={null}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@eas/contracts/resolver/SchemaResolver.sol";
contract MyResolver is SchemaResolver {
address public astralSigner;
constructor(IEAS eas, address _astralSigner)
SchemaResolver(eas)
{
astralSigner = _astralSigner;
}
function onAttest(
Attestation calldata attestation,
uint256 /*value*/
) internal override returns (bool) {
// 1. Verify from Astral
require(attestation.attester == astralSigner, "Not from Astral");
// 2. Decode the result
(
bool result,
bytes32[] memory inputRefs,
uint64 timestamp,
string memory operation
) = abi.decode(
attestation.data,
(bool, bytes32[], uint64, string)
);
// 3. Business logic
if (result) {
// Policy passed - execute action
}
return true;
}
function onRevoke(Attestation calldata, uint256)
internal pure override returns (bool)
{
return false;
}
}
```
***
## Location Attestation Schemas
Location attestations use a standard EAS schema:
```
uint256 eventTimestamp,string srs,string locationType,string location,string[] recipeType,string[] recipePayload,string[] mediaType,string[] mediaData,string memo
```
### Encoding/Decoding
```typescript theme={null}
// Build unsigned attestation
const unsigned = await astral.location.build({
location: { type: 'Point', coordinates: [lon, lat] },
memo: 'My location'
});
// Encode for EAS
const encoded = astral.location.encode(unsigned);
// Decode from EAS data
const decoded = astral.location.decode(encodedData);
console.log(decoded.location);
console.log(decoded.memo);
```
***
## EAS Contract Addresses
The SDK automatically uses the correct EAS contract address based on `chainId`:
| Chain | Chain ID | EAS Contract |
| ---------------- | -------- | -------------------------------------------- |
| Base Sepolia | 84532 | `0x4200000000000000000000000000000000000021` |
| Base Mainnet | 8453 | `0x4200000000000000000000000000000000000021` |
| Ethereum Mainnet | 1 | `0xA1207F3BBa224E2c9c3c6D5aF63D0eb1582Ce587` |
| Ethereum Sepolia | 11155111 | `0xC2679fBD37d54388Ce493F1DB75320D236e1815e` |
| Celo | 42220 | `0x72E1d8ccf5299fb36fEfD8CC4394B8ef7e98Af92` |
| Arbitrum | 42161 | `0xbD75f629A22Dc1ceD33dDA0b68c546A1c035c458` |
| Optimism | 10 | `0x4200000000000000000000000000000000000021` |
***
## Offchain vs Onchain Attestations
### Offchain (EIP-712 Signed)
* No gas cost to create
* Stored off-chain (IPFS, your server, etc.)
* Verified by recovering signer from signature
* Use `astral.location.offchain.*`
```typescript theme={null}
const attestation = await astral.location.offchain.create({
location: geojson
});
// attestation.signature contains EIP-712 signature
// attestation.uid is derived from content hash
```
### Onchain (Blockchain Stored)
* Gas cost to create
* Stored in EAS contract
* Verified by querying blockchain
* Use `astral.location.onchain.*`
```typescript theme={null}
const attestation = await astral.location.onchain.create({
location: geojson
});
// attestation.txHash contains transaction hash
// attestation.uid is from EAS contract
```
***
## Custom Schemas
You can use custom EAS schemas for location attestations:
```typescript theme={null}
const customSchema = {
uid: '0x1234...abcd', // Your schema UID
rawString: 'uint256 eventTimestamp,string srs,string locationType,string location,string[] recipeType,string[] recipePayload,string[] mediaType,string[] mediaData,string memo'
};
// Use at initialization
const astral = new AstralSDK({
chainId: 84532,
signer: wallet,
defaultSchema: customSchema
});
// Or per-operation
const attestation = await astral.location.offchain.create(
{ location: geojson },
{ schema: customSchema }
);
```
Return to SDK overview
# Installation
Source: https://docs.astral.global/sdk/installation
Install and configure the Astral SDK
**Research Preview** — The SDK is under active development.
# Installation
The Astral SDK is a unified package that includes location attestations, geospatial computations, stamps, proofs, and the plugin system.
## Package manager
```bash npm theme={null}
npm install @decentralized-geo/astral-sdk
```
```bash yarn theme={null}
yarn add @decentralized-geo/astral-sdk
```
```bash pnpm theme={null}
pnpm add @decentralized-geo/astral-sdk
```
## Peer dependencies
The SDK requires ethers.js v6:
```bash theme={null}
npm install ethers@^6.0.0
```
## Basic initialization
```typescript theme={null}
import { AstralSDK } from '@decentralized-geo/astral-sdk';
import { ethers } from 'ethers';
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://staging-api.astral.global'
});
// All modules are available
const attestation = await astral.location.offchain.create({ ... });
const distance = await astral.compute.distance(uid1, uid2, options);
const stamps = await astral.stamps.collect();
```
## Plugin registration
Register plugins at startup before using stamps or proofs:
```typescript theme={null}
import { AstralSDK } from '@decentralized-geo/astral-sdk';
import { MockPlugin } from '@decentralized-geo/astral-sdk';
import { ProofModePlugin } from '@location-proofs/plugin-proofmode';
const astral = new AstralSDK({
chainId: 84532,
signer: wallet,
apiUrl: 'https://staging-api.astral.global'
});
// Register plugins before using stamps/proofs
astral.plugins.register(new MockPlugin());
astral.plugins.register(new ProofModePlugin());
// Now stamps module can orchestrate across registered plugins
const signals = await astral.stamps.collect();
```
## With browser wallet
```typescript theme={null}
import { AstralSDK } from '@decentralized-geo/astral-sdk';
import { ethers } from 'ethers';
const provider = new ethers.BrowserProvider(window.ethereum);
const signer = await provider.getSigner();
const network = await provider.getNetwork();
const astral = new AstralSDK({
chainId: Number(network.chainId),
signer: signer,
apiUrl: 'https://staging-api.astral.global'
});
```
## Read-only mode
For querying and verification without signing:
```typescript theme={null}
import { AstralSDK } from '@decentralized-geo/astral-sdk';
import { ethers } from 'ethers';
const provider = new ethers.JsonRpcProvider('https://sepolia.base.org');
const astral = new AstralSDK({
chainId: 84532,
provider: provider,
apiUrl: 'https://staging-api.astral.global'
});
// Can verify attestations
const result = await astral.location.onchain.verify(attestation);
// Can compute (API calls don't require signer)
const distance = await astral.compute.distance(uid1, uid2, options);
// Cannot submit to EAS without signer
// await astral.compute.submit(attestation); // Would throw
```
## Configuration options
```typescript theme={null}
interface AstralConfig {
/** Target chain ID. Required. */
chainId: number;
/** Ethers signer for signing attestations and transactions. */
signer?: Signer;
/** Ethers provider for read-only blockchain operations. */
provider?: Provider;
/** Astral API base URL. Defaults to 'https://staging-api.astral.global' */
apiUrl?: string;
/** Required for TEE verification and hosted stamp verification. */
apiKey?: string;
/** Enable debug logging. Defaults to false. */
debug?: boolean;
/** Pre-register custom EAS schemas. */
schemas?: RuntimeSchemaConfig[];
/** Default schema to use when none is specified. */
defaultSchema?: RuntimeSchemaConfig;
/** Throw errors on schema validation failures. Defaults to false. */
strictSchemaValidation?: boolean;
}
```
## Supported chains
| Network | Chain ID | Status |
| ---------------- | -------- | ------ |
| Base Sepolia | 84532 | Active |
| Base Mainnet | 8453 | Active |
| Ethereum Sepolia | 11155111 | Active |
| Ethereum Mainnet | 1 | Active |
| Celo | 42220 | Active |
| Arbitrum | 42161 | Active |
| Optimism | 10 | Active |
## Submodule imports
You can import submodules directly for tree-shaking:
```typescript theme={null}
import { LocationModule } from '@decentralized-geo/astral-sdk/location';
import { ComputeModule } from '@decentralized-geo/astral-sdk/compute';
import { PluginRegistry } from '@decentralized-geo/astral-sdk/plugins';
import { StampsModule } from '@decentralized-geo/astral-sdk/stamps';
import { ProofsModule } from '@decentralized-geo/astral-sdk/proofs';
```
## TypeScript support
The SDK ships with full type definitions:
```typescript theme={null}
import {
AstralSDK,
// Location types
LocationAttestationInput,
UnsignedLocationAttestation,
OffchainLocationAttestation,
OnchainLocationAttestation,
VerificationResult,
// Compute types
Input,
ComputeOptions,
NumericComputeResult,
BooleanComputeResult,
DelegatedAttestation,
// Plugin types
LocationProofPlugin,
PluginRegistry,
// Stamp/proof types
LocationStamp,
LocationProof,
CredibilityVector,
// Configuration
AstralConfig,
RuntimeSchemaConfig,
} from '@decentralized-geo/astral-sdk';
```
## Environment variables
For server-side applications:
```bash theme={null}
# .env
PRIVATE_KEY=0x...
CHAIN_ID=84532
ASTRAL_API_URL=https://staging-api.astral.global
```
```typescript theme={null}
import { AstralSDK } from '@decentralized-geo/astral-sdk';
import { ethers } from 'ethers';
const provider = new ethers.JsonRpcProvider(getRpcUrl(process.env.CHAIN_ID));
const wallet = new ethers.Wallet(process.env.PRIVATE_KEY, provider);
const astral = new AstralSDK({
chainId: Number(process.env.CHAIN_ID),
signer: wallet,
apiUrl: process.env.ASTRAL_API_URL
});
```
Learn about location attestation workflows
# Location Module
Source: https://docs.astral.global/sdk/location
Offchain and onchain location attestation workflows
**Research Preview** — The SDK is under development.
# Location Module
The `LocationModule` provides operations for creating, signing, and verifying location attestations using the Ethereum Attestation Service (EAS).
```typescript theme={null}
import { AstralSDK } from '@decentralized-geo/astral-sdk';
const astral = new AstralSDK({
chainId: 84532,
signer: wallet
});
// Access via astral.location
astral.location.offchain.* // Offchain workflow
astral.location.onchain.* // Onchain workflow
astral.location.build() // Common operations
astral.location.encode()
astral.location.decode()
```
***
## Common Operations
### build()
Build an unsigned location attestation from input data.
```typescript theme={null}
astral.location.build(
input: LocationAttestationInput
): Promise
```
#### Example
```typescript theme={null}
const unsigned = await astral.location.build({
location: {
type: 'Point',
coordinates: [2.2945, 48.8584]
},
locationType: 'geojson-point', // Optional - auto-detected
memo: 'Eiffel Tower visit'
});
console.log(unsigned.eventTimestamp);
console.log(unsigned.location); // Stringified GeoJSON
console.log(unsigned.locationType); // 'geojson-point'
```
***
### encode()
Encode an unsigned attestation for EAS.
```typescript theme={null}
astral.location.encode(
attestation: UnsignedLocationAttestation,
schemaType?: string // Defaults to 'location'
): string
```
#### Example
```typescript theme={null}
const unsigned = await astral.location.build({ location: geojson });
const encoded = astral.location.encode(unsigned);
// Use encoded data with EAS directly if needed
console.log('Encoded:', encoded);
```
***
### decode()
Decode EAS-encoded data back to attestation fields.
```typescript theme={null}
astral.location.decode(
encodedData: string,
schemaType?: string // Defaults to 'location'
): Record
```
#### Example
```typescript theme={null}
// Decode from EAS attestation data
const decoded = astral.location.decode(attestation.data);
console.log(decoded.eventTimestamp);
console.log(decoded.locationType);
console.log(decoded.location);
```
***
## Offchain Workflow
The offchain workflow creates EIP-712 signed attestations that can be verified without blockchain transactions.
### offchain.create()
Build and sign an attestation in one step.
```typescript theme={null}
astral.location.offchain.create(
input: LocationAttestationInput,
options?: OffchainAttestationOptions
): Promise
```
#### Example
```typescript theme={null}
const attestation = await astral.location.offchain.create({
location: {
type: 'Point',
coordinates: [2.2945, 48.8584]
},
memo: 'Eiffel Tower',
timestamp: new Date()
});
console.log('UID:', attestation.uid);
console.log('Signer:', attestation.signer);
console.log('Signature:', attestation.signature);
```
***
### offchain.sign()
Sign a pre-built unsigned attestation.
```typescript theme={null}
astral.location.offchain.sign(
unsigned: UnsignedLocationAttestation,
options?: OffchainAttestationOptions
): Promise
```
#### Example
```typescript theme={null}
// Build first
const unsigned = await astral.location.build({
location: { type: 'Point', coordinates: [-122.4194, 37.7749] },
memo: 'San Francisco'
});
// Sign separately
const signed = await astral.location.offchain.sign(unsigned);
console.log('UID:', signed.uid);
```
***
### offchain.verify()
Verify an offchain attestation's signature.
```typescript theme={null}
astral.location.offchain.verify(
attestation: OffchainLocationAttestation,
options?: OffchainAttestationOptions
): Promise
```
#### Example
```typescript theme={null}
const result = await astral.location.offchain.verify(attestation);
if (result.isValid) {
console.log('Valid! Signed by:', result.signerAddress);
} else {
console.log('Invalid:', result.reason);
}
```
***
### offchain.publish()
Publish an offchain attestation to storage (IPFS, etc.).
```typescript theme={null}
astral.location.offchain.publish(
attestation: OffchainLocationAttestation
): Promise
```
#### Example
```typescript theme={null}
const published = await astral.location.offchain.publish(attestation);
console.log('Publications:', published.publications);
// [{ storageType: 'ipfs', reference: 'Qm...', publishedAt: 1234567890 }]
```
***
## Onchain Workflow
The onchain workflow registers attestations directly on a blockchain via EAS.
### onchain.create()
Build and register an attestation in one step.
```typescript theme={null}
astral.location.onchain.create(
input: LocationAttestationInput,
options?: OnchainAttestationOptions
): Promise
```
#### Example
```typescript theme={null}
const attestation = await astral.location.onchain.create({
location: {
type: 'Polygon',
coordinates: [[[
[-122.42, 37.77],
[-122.41, 37.77],
[-122.41, 37.78],
[-122.42, 37.78],
[-122.42, 37.77]
]]]
},
memo: 'Property boundary'
});
console.log('UID:', attestation.uid);
console.log('Chain:', attestation.chain);
console.log('Tx Hash:', attestation.txHash);
console.log('Block:', attestation.blockNumber);
```
***
### onchain.register()
Register a pre-built unsigned attestation.
```typescript theme={null}
astral.location.onchain.register(
unsigned: UnsignedLocationAttestation,
options?: OnchainAttestationOptions
): Promise
```
#### Example
```typescript theme={null}
// Build first
const unsigned = await astral.location.build({
location: geojson,
memo: 'My location'
});
// Register separately
const onchain = await astral.location.onchain.register(unsigned);
console.log('Tx Hash:', onchain.txHash);
```
***
### onchain.verify()
Verify an onchain attestation exists and is not revoked.
```typescript theme={null}
astral.location.onchain.verify(
attestation: OnchainLocationAttestation,
options?: OnchainAttestationOptions
): Promise
```
#### Example
```typescript theme={null}
const result = await astral.location.onchain.verify(attestation);
if (result.isValid) {
console.log('Valid onchain attestation');
console.log('Revoked:', result.revoked);
} else {
console.log('Invalid:', result.reason);
}
```
***
### onchain.revoke()
Revoke an onchain attestation.
```typescript theme={null}
astral.location.onchain.revoke(
attestation: OnchainLocationAttestation,
options?: OnchainAttestationOptions
): Promise
```
#### Example
```typescript theme={null}
// Only works if attestation was created with revocable: true
if (attestation.revocable && !attestation.revoked) {
await astral.location.onchain.revoke(attestation);
console.log('Attestation revoked');
}
```
***
## Input Types
### LocationAttestationInput
```typescript theme={null}
interface LocationAttestationInput {
// Location data - flexible format
location: unknown; // GeoJSON, WKT, coordinate pair, H3 index
// Optional format hints
locationType?: string; // e.g., 'geojson-point', 'wkt-polygon', 'h3'
targetLocationFormat?: string; // Convert to this format
// Timing
timestamp?: Date; // Defaults to now
// Media attachments
media?: MediaInput[];
// Additional fields
memo?: string;
recipient?: string; // Ethereum address
}
```
### Supported Location Formats
| Format | locationType | Example |
| ------------------ | ----------------------------- | -------------------------------------------- |
| GeoJSON Point | `geojson-point` | `{ type: 'Point', coordinates: [lon, lat] }` |
| GeoJSON Polygon | `geojson-polygon` | `{ type: 'Polygon', coordinates: [...] }` |
| GeoJSON LineString | `geojson-linestring` | `{ type: 'LineString', coordinates: [...] }` |
| WKT | `wkt-*` | `'POINT(-122.4194 37.7749)'` |
| Coordinate Pair | `coordinates-decimal+lon-lat` | `[-122.4194, 37.7749]` |
| H3 Index | `h3` | `'8928308280fffff'` |
Location format is auto-detected when `locationType` is not specified.
***
## Output Types
### OffchainLocationAttestation
```typescript theme={null}
interface OffchainLocationAttestation {
// From UnsignedLocationAttestation
eventTimestamp: number;
srs: string;
locationType: string;
location: string;
recipeType: string[];
recipePayload: string[];
mediaType: string[];
mediaData: string[];
memo?: string;
// Signature fields
uid: string;
signature: string;
signer: string;
version: string;
// Storage
publications?: PublicationRecord[];
}
```
### OnchainLocationAttestation
```typescript theme={null}
interface OnchainLocationAttestation {
// From UnsignedLocationAttestation
eventTimestamp: number;
srs: string;
locationType: string;
location: string;
// ... other fields
// Blockchain fields
uid: string;
attester: string;
chain: string;
chainId: number;
txHash: string;
blockNumber: number;
revocable: boolean;
revoked: boolean;
}
```
### VerificationResult
```typescript theme={null}
interface VerificationResult {
isValid: boolean;
revoked?: boolean;
signerAddress?: string;
attestation?: LocationAttestation;
reason?: string;
}
```
***
## Coordinate Format
Coordinates use the GeoJSON standard: **\[longitude, latitude]**
```typescript theme={null}
// San Francisco
const sf = {
type: 'Point',
coordinates: [-122.4194, 37.7749] // [lon, lat]
};
// Polygon (counterclockwise, closed ring)
const area = {
type: 'Polygon',
coordinates: [[[
[-122.42, 37.77],
[-122.41, 37.77],
[-122.41, 37.78],
[-122.42, 37.78],
[-122.42, 37.77] // Close the ring
]]]
};
```
Learn about geospatial computation methods
# Location Proofs
Source: https://docs.astral.global/sdk/location-proofs
Proof construction and multidimensional verification
**Research Preview** — The proofs module is under active development.
# Location proofs
The `ProofsModule` handles proof construction and multidimensional verification. A location proof bundles a claim ("I was at location X at time T") with one or more stamps (evidence from proof-of-location systems). Verification produces a `CredibilityVector` — not a single score.
```typescript theme={null}
import { AstralSDK } from '@decentralized-geo/astral-sdk';
const astral = new AstralSDK({
chainId: 84532,
signer: wallet,
apiUrl: 'https://staging-api.astral.global'
});
// Access via astral.proofs
astral.proofs.create(claim, stamps);
astral.proofs.verify(proof, options);
```
For conceptual background, see [Location Proofs](/concepts/location-proofs).
***
## proofs.create()
Bundle a claim with stamps into a location proof. This is a **synchronous** operation.
```typescript theme={null}
astral.proofs.create(
claim: LocationClaim,
stamps: LocationStamp[]
): LocationProof
```
### Parameters
| Parameter | Type | Required | Description |
| --------- | ----------------- | -------- | ------------------------------------- |
| `claim` | `LocationClaim` | Yes | The location claim to prove |
| `stamps` | `LocationStamp[]` | Yes | One or more signed stamps as evidence |
Requires at least one stamp.
### Example
```typescript theme={null}
const claim: LocationClaim = {
lpVersion: '0.2',
locationType: 'geojson-point',
location: { type: 'Point', coordinates: [-122.4194, 37.7749] },
srs: 'EPSG:4326',
subject: { scheme: 'eth-address', value: '0x...' },
radius: 100,
time: { start: Math.floor(Date.now() / 1000) - 60, end: Math.floor(Date.now() / 1000) }
};
const proof = astral.proofs.create(claim, [stamp1, stamp2]);
```
***
## proofs.verify()
Verify a location proof. Two modes available:
### Local mode (default)
Runs verification in-process. Free and fast. Returns a `CredibilityVector`.
```typescript theme={null}
astral.proofs.verify(
proof: LocationProof,
options?: { mode?: 'local' }
): Promise
```
```typescript theme={null}
const vector = await astral.proofs.verify(proof);
// or explicitly:
const vector = await astral.proofs.verify(proof, { mode: 'local' });
console.log(vector.dimensions.spatial.meanDistanceMeters);
console.log(vector.dimensions.temporal.meanOverlap);
console.log(vector.dimensions.validity.signaturesValidFraction);
console.log(vector.dimensions.independence.uniquePluginRatio);
```
Local verification:
* Verifies each stamp via its plugin's `verify()` method
* Measures spatial alignment between each stamp and the claim
* Measures temporal overlap between each stamp and the claim
* Assesses independence across stamps from different plugins
### TEE mode
Runs in a hosted Trusted Execution Environment. Returns a `VerifiedLocationProof` with an EAS attestation.
```typescript theme={null}
astral.proofs.verify(
proof: LocationProof,
options: {
mode: 'tee';
chainId?: number;
submitOnchain?: boolean;
schema?: string;
recipient?: string;
}
): Promise
```
```typescript theme={null}
const verified = await astral.proofs.verify(proof, {
mode: 'tee',
chainId: 84532,
submitOnchain: true
});
console.log(verified.attestation.uid);
console.log(verified.credibility.dimensions.spatial);
console.log(verified.remoteAttestation?.platform); // "sgx", "tdx", "sev"
```
### Type guard
Use `isVerifiedLocationProof()` to narrow the return type:
```typescript theme={null}
import { isVerifiedLocationProof } from '@decentralized-geo/astral-sdk';
const result = await astral.proofs.verify(proof, options);
if (isVerifiedLocationProof(result)) {
// TEE mode — has attestation
console.log(result.attestation.uid);
} else {
// Local mode — CredibilityVector
console.log(result.dimensions.spatial);
}
```
***
## ProofsModule.exampleWeighting()
Static helper demonstrating how to collapse a `CredibilityVector` into a single decision value. This is provided as an **example** — applications should implement their own weighting functions.
```typescript theme={null}
const score = ProofsModule.exampleWeighting(vector);
// Example: reject if score is below threshold
if (score > 0.7) {
console.log('Proof accepted');
}
```
`CredibilityVector` intentionally has no single score. The example weighting is for demonstration only. Applications should define their own weighting based on their risk tolerance and use case.
***
## CredibilityVector
The core verification output. Four dimensions, no opinionated scoring.
```typescript theme={null}
interface CredibilityVector {
dimensions: {
spatial: {
meanDistanceMeters: number; // Average distance from stamps to claim
maxDistanceMeters: number; // Worst-case distance
withinRadiusFraction: number; // 0-1, fraction of stamps within claim radius
};
temporal: {
meanOverlap: number; // 0-1, average temporal overlap with claim
minOverlap: number; // 0-1, worst-case overlap
fullyOverlappingFraction: number; // Fraction of stamps fully within claim time
};
validity: {
signaturesValidFraction: number; // 0-1, fraction with valid signatures
structureValidFraction: number; // 0-1, fraction with valid structure
signalsConsistentFraction: number; // 0-1, fraction with consistent signals
};
independence: {
uniquePluginRatio: number; // 0-1, 1.0 = all stamps from different plugins
spatialAgreement: number; // 0-1, how much independent sources agree
pluginNames: string[]; // List of plugins used
};
};
stampResults: StampResult[];
meta: {
stampCount: number;
evaluatedAt: number; // Unix seconds
evaluationMode: 'local' | 'tee' | 'zk';
};
}
```
### StampResult
Per-stamp assessment within the `CredibilityVector`:
```typescript theme={null}
interface StampResult {
stampIndex: number;
plugin: string;
signaturesValid: boolean;
structureValid: boolean;
signalsConsistent: boolean;
distanceMeters: number; // Haversine distance to claim
temporalOverlap: number; // 0-1 fraction
withinRadius: boolean;
details: Record; // Plugin-specific
}
```
***
## VerifiedLocationProof
Returned by TEE mode verification. Contains the full credibility vector plus an EAS attestation.
```typescript theme={null}
interface VerifiedLocationProof {
proof: LocationProof;
credibility: CredibilityVector;
attestation: {
uid: string;
schema: string;
attester: string;
recipient: string;
revocable: boolean;
refUID: string;
data: string;
time: number;
expirationTime: number; // 0 = never
revocationTime: number; // 0 = not revoked
signature?: string;
};
delegatedAttestation?: {
signature: string; // EIP-712
attester: string;
deadline: number;
nonce: number;
};
chainId?: number;
remoteAttestation?: {
quote: string;
platform: string; // "sgx", "tdx", "sev"
metadata?: Record;
};
evaluationMethod: string;
evaluatedAt: number;
}
```
***
## LocationClaim
Defines what the proof is asserting.
```typescript theme={null}
interface LocationClaim {
lpVersion: string; // e.g., "0.2"
locationType: string; // e.g., "geojson-point", "h3-index"
location: LocationData; // GeoJSON geometry or string
srs: string; // e.g., "EPSG:4326"
subject: SubjectIdentifier; // Who/what was at the location
radius: number; // Spatial uncertainty in meters
time: TimeBounds; // { start, end } — Unix seconds
eventType?: string; // Optional event classification
}
interface SubjectIdentifier {
scheme: string; // "eth-address", "device-pubkey", "did:pkh"
value: string;
}
```
***
## Multi-stamp cross-correlation
Independent plugins increase credibility. When stamps from different proof-of-location systems agree, the `independence` dimension reflects this:
```typescript theme={null}
import { AstralSDK, MockPlugin } from '@decentralized-geo/astral-sdk';
import { ProofModePlugin } from '@location-proofs/plugin-proofmode';
const astral = new AstralSDK({ chainId: 84532, signer: wallet });
astral.plugins.register(new MockPlugin({ lat: 37.7749, lon: -122.4194 }));
astral.plugins.register(new ProofModePlugin());
// Stamps from different systems
const mockStamp = /* ... collect, create, sign with mock plugin ... */;
const proofmodeStamp = /* ... create from ProofMode ZIP bundle ... */;
// Bundle into proof with multiple stamps
const proof = astral.proofs.create(claim, [mockStamp, proofmodeStamp]);
// Verify — cross-correlation is reflected in the vector
const vector = await astral.proofs.verify(proof);
console.log(vector.dimensions.independence.uniquePluginRatio); // 1.0 (all different)
console.log(vector.dimensions.independence.spatialAgreement); // 0-1
console.log(vector.dimensions.independence.pluginNames); // ['mock', 'proofmode']
console.log(vector.meta.stampCount); // 2
```
Independent, corroborating evidence from different proof systems strengthens the proof. Redundant stamps from the same plugin don't add independence — but they don't subtract either.
***
## Complete example
```typescript theme={null}
import { AstralSDK, MockPlugin } from '@decentralized-geo/astral-sdk';
import { ethers } from 'ethers';
const wallet = new ethers.Wallet(process.env.PRIVATE_KEY);
const astral = new AstralSDK({
chainId: 84532,
signer: wallet,
apiUrl: 'https://staging-api.astral.global'
});
astral.plugins.register(new MockPlugin({
lat: 37.7749,
lon: -122.4194,
accuracy: 10
}));
// 1. Collect and create a stamp
const signals = (await astral.stamps.collect({ plugins: ['mock'] }))[0];
const unsigned = await astral.stamps.create({ plugin: 'mock' }, signals);
const stamp = await astral.stamps.sign(
{ plugin: 'mock' },
unsigned,
{
algorithm: 'secp256k1',
signer: { scheme: 'eth-address', value: wallet.address },
sign: (data) => wallet.signMessage(data)
}
);
// 2. Define the claim
const claim = {
lpVersion: '0.2',
locationType: 'geojson-point',
location: { type: 'Point', coordinates: [-122.4194, 37.7749] },
srs: 'EPSG:4326',
subject: { scheme: 'eth-address', value: wallet.address },
radius: 100,
time: { start: Math.floor(Date.now() / 1000) - 60, end: Math.floor(Date.now() / 1000) }
};
// 3. Bundle into proof
const proof = astral.proofs.create(claim, [stamp]);
// 4. Verify locally
const vector = await astral.proofs.verify(proof);
console.log('Spatial:', vector.dimensions.spatial.meanDistanceMeters, 'm');
console.log('Temporal overlap:', vector.dimensions.temporal.meanOverlap);
console.log('Signatures valid:', vector.dimensions.validity.signaturesValidFraction);
// 5. Or verify in TEE for attestation
const verified = await astral.proofs.verify(proof, {
mode: 'tee',
chainId: 84532
});
console.log('Attestation UID:', verified.attestation.uid);
```
Learn about the plugin system and registry
# Migration Guide
Source: https://docs.astral.global/sdk/migration
Migrate from Astral SDK v0.1.x to v0.2.0
**Research Preview** — The SDK is under development.
# Migration Guide: v0.1.x to v0.2.0
This guide covers the breaking changes and migration steps for upgrading from Astral SDK v0.1.x to v0.2.0.
## Overview of Changes
### What Changed
1. **Namespaced API**: Methods are organized under `location.offchain.*`, `location.onchain.*`, and `compute.*`
2. **New Compute Module**: The `compute.*` namespace provides verifiable geospatial operations
3. **Simplified Configuration**: Single `AstralConfig` for initialization
### What Stayed the Same
* Core attestation types (`UnsignedLocationAttestation`, `OffchainLocationAttestation`, `OnchainLocationAttestation`)
* Location format support (GeoJSON, WKT, H3, coordinates)
* EAS integration and schema encoding
***
## Package Changes
### Before (v0.1.x)
```bash theme={null}
# Two separate packages
npm install @decentralized-geo/astral-sdk
npm install @decentralized-geo/astral-compute
```
### After (v0.2.0)
```bash theme={null}
# Single unified package
npm install @decentralized-geo/astral-sdk
```
***
## Initialization Changes
### Before (v0.1.x)
```typescript theme={null}
import { AstralSDK } from '@decentralized-geo/astral-sdk';
import { AstralCompute, AstralEAS } from '@decentralized-geo/astral-compute';
// Separate initialization
const sdk = new AstralSDK({ signer: wallet });
const compute = new AstralCompute({ chainId: 84532 });
const eas = new AstralEAS(wallet, 84532);
```
### After (v0.2.0)
```typescript theme={null}
import { AstralSDK } from '@decentralized-geo/astral-sdk';
// Unified initialization
const astral = new AstralSDK({
chainId: 84532,
signer: wallet,
apiUrl: 'https://staging-api.astral.global'
});
// Access modules via namespaces
astral.location.offchain.*
astral.location.onchain.*
astral.compute.*
```
***
## Method Migration Table
### Location Module
| v0.1.x Method | v0.2.0 Method |
| --------------------------------- | -------------------------- |
| `sdk.buildLocationAttestation()` | `astral.location.build()` |
| `sdk.encodeLocationAttestation()` | `astral.location.encode()` |
| `sdk.decodeLocationAttestation()` | `astral.location.decode()` |
### Offchain Workflow
| v0.1.x Method | v0.2.0 Method |
| ------------------------------------------ | ------------------------------------ |
| `sdk.createOffchainLocationAttestation()` | `astral.location.offchain.create()` |
| `sdk.signOffchainLocationAttestation()` | `astral.location.offchain.sign()` |
| `sdk.verifyOffchainLocationAttestation()` | `astral.location.offchain.verify()` |
| `sdk.publishOffchainLocationAttestation()` | `astral.location.offchain.publish()` |
### Onchain Workflow
| v0.1.x Method | v0.2.0 Method |
| ------------------------------------------ | ------------------------------------ |
| `sdk.createOnchainLocationAttestation()` | `astral.location.onchain.create()` |
| `sdk.registerOnchainLocationAttestation()` | `astral.location.onchain.register()` |
| `sdk.verifyOnchainLocationAttestation()` | `astral.location.onchain.verify()` |
| `sdk.revokeOnchainLocationAttestation()` | `astral.location.onchain.revoke()` |
### Compute Module
| v0.1.x (astral-compute) | v0.2.0 Method |
| ----------------------- | ----------------------------- |
| `compute.distance()` | `astral.compute.distance()` |
| `compute.area()` | `astral.compute.area()` |
| `compute.length()` | `astral.compute.length()` |
| `compute.contains()` | `astral.compute.contains()` |
| `compute.within()` | `astral.compute.within()` |
| `compute.intersects()` | `astral.compute.intersects()` |
| `eas.submitDelegated()` | `astral.compute.submit()` |
| `eas.estimateGas()` | `astral.compute.estimate()` |
***
## Code Migration Examples
### Example 1: Creating an Offchain Attestation
#### Before (v0.1.x)
```typescript theme={null}
import { AstralSDK } from '@decentralized-geo/astral-sdk';
const sdk = new AstralSDK({ signer: wallet });
const unsigned = await sdk.buildLocationAttestation({
location: { type: 'Point', coordinates: [2.2945, 48.8584] },
memo: 'Eiffel Tower'
});
const signed = await sdk.signOffchainLocationAttestation(unsigned);
const verified = await sdk.verifyOffchainLocationAttestation(signed);
```
#### After (v0.2.0)
```typescript theme={null}
import { AstralSDK } from '@decentralized-geo/astral-sdk';
const astral = new AstralSDK({ chainId: 84532, signer: wallet });
// Option 1: Build and sign separately
const unsigned = await astral.location.build({
location: { type: 'Point', coordinates: [2.2945, 48.8584] },
memo: 'Eiffel Tower'
});
const signed = await astral.location.offchain.sign(unsigned);
// Option 2: Create in one step
const attestation = await astral.location.offchain.create({
location: { type: 'Point', coordinates: [2.2945, 48.8584] },
memo: 'Eiffel Tower'
});
const verified = await astral.location.offchain.verify(attestation);
```
***
### Example 2: Creating an Onchain Attestation
#### Before (v0.1.x)
```typescript theme={null}
const sdk = new AstralSDK({ signer: wallet, chainId: 84532 });
const unsigned = await sdk.buildLocationAttestation({
location: geojson
});
const onchain = await sdk.registerOnchainLocationAttestation(unsigned);
```
#### After (v0.2.0)
```typescript theme={null}
const astral = new AstralSDK({ chainId: 84532, signer: wallet });
// Option 1: Build and register separately
const unsigned = await astral.location.build({ location: geojson });
const onchain = await astral.location.onchain.register(unsigned);
// Option 2: Create in one step
const onchain = await astral.location.onchain.create({ location: geojson });
```
***
### Example 3: Geospatial Computation
#### Before (v0.1.x)
```typescript theme={null}
import { AstralCompute, AstralEAS } from '@decentralized-geo/astral-compute';
const compute = new AstralCompute({ chainId: 84532 });
const eas = new AstralEAS(wallet, 84532);
const result = await compute.within(
userLocationUID,
landmarkUID,
500,
{ schema: SCHEMA_UID }
);
if (result.result) {
await eas.submitDelegated(result.delegatedAttestation);
}
```
#### After (v0.2.0)
```typescript theme={null}
import { AstralSDK } from '@decentralized-geo/astral-sdk';
const astral = new AstralSDK({
chainId: 84532,
signer: wallet,
apiUrl: 'https://staging-api.astral.global'
});
const result = await astral.compute.within(
userLocationUID,
landmarkUID,
500,
{ schema: SCHEMA_UID }
);
if (result.result) {
await astral.compute.submit(result.delegatedAttestation);
}
```
***
### Example 4: Full Workflow
#### Before (v0.1.x)
```typescript theme={null}
import { AstralSDK } from '@decentralized-geo/astral-sdk';
import { AstralCompute, AstralEAS } from '@decentralized-geo/astral-compute';
// Initialize three clients
const sdk = new AstralSDK({ signer: wallet });
const compute = new AstralCompute({ chainId: 84532 });
const eas = new AstralEAS(wallet, 84532);
// Create location
const unsigned = await sdk.buildLocationAttestation({
location: userCoords
});
const location = await sdk.registerOnchainLocationAttestation(unsigned);
// Check policy
const result = await compute.within(
location.uid,
landmarkUID,
500,
{ schema: SCHEMA_UID }
);
// Submit result
if (result.result) {
await eas.submitDelegated(result.delegatedAttestation);
}
```
#### After (v0.2.0)
```typescript theme={null}
import { AstralSDK } from '@decentralized-geo/astral-sdk';
// Single unified client
const astral = new AstralSDK({
chainId: 84532,
signer: wallet,
apiUrl: 'https://staging-api.astral.global'
});
// Create location
const location = await astral.location.onchain.create({
location: userCoords
});
// Check policy
const result = await astral.compute.within(
location.uid,
landmarkUID,
500,
{ schema: SCHEMA_UID }
);
// Submit result
if (result.result) {
await astral.compute.submit(result.delegatedAttestation);
}
```
***
## Configuration Changes
### Before (v0.1.x)
```typescript theme={null}
// AstralSDK config
interface AstralSDKConfig {
signer: Signer;
chainId?: number;
defaultChain?: string;
}
// AstralCompute config
interface AstralComputeConfig {
chainId: number;
apiUrl?: string;
}
```
### After (v0.2.0)
```typescript theme={null}
interface AstralConfig {
// Required
chainId: number;
// Optional
signer?: Signer;
provider?: Provider;
apiUrl?: string;
debug?: boolean;
schemas?: RuntimeSchemaConfig[];
defaultSchema?: RuntimeSchemaConfig;
strictSchemaValidation?: boolean;
}
```
***
## Legacy Support
If you need to maintain backward compatibility during migration, the v0.1.x API is available via `AstralSDKLegacy`:
```typescript theme={null}
import { AstralSDKLegacy } from '@decentralized-geo/astral-sdk';
// Use v0.1.x API
const sdk = new AstralSDKLegacy({ signer: wallet });
const unsigned = await sdk.buildLocationAttestation({ ... });
const signed = await sdk.signOffchainLocationAttestation(unsigned);
```
`AstralSDKLegacy` is deprecated and will be removed in v0.3.0. Migrate to the new API before then.
***
## TypeScript Import Changes
### Before (v0.1.x)
```typescript theme={null}
import { AstralSDK } from '@decentralized-geo/astral-sdk';
import {
AstralCompute,
AstralEAS,
NumericComputeResult,
BooleanComputeResult
} from '@decentralized-geo/astral-compute';
```
### After (v0.2.0)
```typescript theme={null}
import {
AstralSDK,
// Location types
LocationAttestationInput,
UnsignedLocationAttestation,
OffchainLocationAttestation,
OnchainLocationAttestation,
VerificationResult,
// Compute types
Input,
ComputeOptions,
NumericComputeResult,
BooleanComputeResult,
DelegatedAttestation,
// Configuration
AstralConfig,
RuntimeSchemaConfig,
} from '@decentralized-geo/astral-sdk';
```
***
## Need Help?
If you encounter issues during migration:
1. Check the [SDK Overview](/sdk/overview) for complete API reference
2. Review the [Location Module](/sdk/location) and [Compute Module](/sdk/compute) documentation
3. Join our [Telegram community](https://t.me/+UkTOSXnDcDM5ZTBk) for support
4. Open an issue on [GitHub](https://github.com/AstralProtocol/astral-location-services)
Return to SDK overview
# SDK Overview
Source: https://docs.astral.global/sdk/overview
Unified TypeScript SDK for the Astral Protocol
**Research Preview** — The SDK is under active development.
# SDK overview
The Astral SDK is the official TypeScript client for the Astral Protocol. It provides a unified interface for working with location attestations, multi-factor location proofs, and verifiable geospatial computations.
## Design philosophy
* **Namespaced API**: Clear separation between location, compute, stamps, proofs, and plugin operations
* **Workflow-oriented**: Distinct offchain and onchain workflows for location attestations
* **Plugin-extensible**: Register location proof plugins; collect signals, create location stamps, and compose multifactor location evidence bundles
* **Multidimensional verification**: No single "confidence score" — applications apply their own weighting to a `CredibilityVector`
* **Type-safe**: Full TypeScript support with comprehensive types
* **Batteries included**: Handles signing, encoding, EAS integration, and plugin orchestration
## Package structure
```typescript theme={null}
import { AstralSDK } from '@decentralized-geo/astral-sdk';
// Initialize with unified configuration
const astral = new AstralSDK({
chainId: 84532,
signer: wallet,
apiUrl: 'https://staging-api.astral.global',
apiKey: 'your-api-key'
});
// Location module — offchain workflow
astral.location.offchain.create(input);
astral.location.offchain.sign(unsigned);
astral.location.offchain.verify(attestation);
astral.location.offchain.publish(attestation);
// Location module — onchain workflow
astral.location.onchain.create(input);
astral.location.onchain.register(unsigned);
astral.location.onchain.verify(attestation);
astral.location.onchain.revoke(attestation);
// Location module — common operations
astral.location.build(input);
astral.location.encode(attestation);
astral.location.decode(data);
// Compute module — spatial operations
astral.compute.distance(from, to, options);
astral.compute.area(geometry, options);
astral.compute.length(geometry, options);
astral.compute.contains(container, containee, options);
astral.compute.within(geometry, target, radius, options);
astral.compute.intersects(a, b, options);
// Compute module — submission
astral.compute.submit(delegatedAttestation);
astral.compute.estimate(delegatedAttestation);
astral.compute.health();
// Stamps module — evidence collection
astral.stamps.collect(options);
astral.stamps.create(options, signals);
astral.stamps.sign(options, stamp, signer);
astral.stamps.verify(stamp, options);
// Proofs module — proof construction + verification
astral.proofs.create(claim, stamps);
astral.proofs.verify(proof, options);
// Plugins module — plugin registration + discovery
astral.plugins.register(plugin);
astral.plugins.get(name);
astral.plugins.list();
```
## Quick start
```typescript theme={null}
import { AstralSDK } from '@decentralized-geo/astral-sdk';
import { ethers } from 'ethers';
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://staging-api.astral.global'
});
// Create an offchain location attestation
const attestation = await astral.location.offchain.create({
location: {
type: 'Point',
coordinates: [2.2945, 48.8584]
},
memo: 'Eiffel Tower'
});
console.log('Attestation UID:', attestation.uid);
// Compute distance between two locations
const distance = await astral.compute.distance(
attestation.uid,
landmarkUID,
{ schema: SCHEMA_UID }
);
console.log('Distance:', distance.result, distance.units);
```
## Configuration
```typescript theme={null}
interface AstralConfig {
// Required
chainId: number; // Target chain ID (e.g., 84532 for Base Sepolia)
// Recommended
signer?: Signer; // For signing attestations and transactions
provider?: Provider; // For read-only blockchain operations
apiUrl?: string; // Defaults to 'https://staging-api.astral.global'
apiKey?: string; // Required for TEE verification and hosted endpoints
// Advanced
debug?: boolean; // Enable debug logging
schemas?: RuntimeSchemaConfig[]; // Pre-register custom schemas
defaultSchema?: RuntimeSchemaConfig; // Override default schema
strictSchemaValidation?: boolean; // Throw on schema validation errors
}
```
| Chain | Chain ID |
| ---------------- | -------- |
| Base Sepolia | 84532 |
| Base Mainnet | 8453 |
| Ethereum Sepolia | 11155111 |
| Ethereum Mainnet | 1 |
| Celo | 42220 |
| Arbitrum | 42161 |
| Optimism | 10 |
## Module architecture
```
AstralSDK
├── location: LocationModule (offchain + onchain attestation workflows)
│ ├── offchain: OffchainWorkflow
│ │ ├── create() — build + sign in one step
│ │ ├── sign() — sign unsigned attestation
│ │ ├── verify() — verify signature
│ │ └── publish() — store to IPFS/storage
│ ├── onchain: OnchainWorkflow
│ │ ├── create() — build + register in one step
│ │ ├── register() — register unsigned attestation
│ │ ├── verify() — verify onchain attestation
│ │ └── revoke() — revoke attestation
│ ├── build() — build unsigned attestation
│ ├── encode() — encode for EAS
│ └── decode() — decode from EAS
├── compute: ComputeModule (verifiable geospatial operations)
│ ├── distance() — distance between geometries
│ ├── area() — area of polygon
│ ├── length() — length of line
│ ├── contains() — containment check
│ ├── within() — proximity check
│ ├── intersects() — intersection check
│ ├── submit() — submit delegated attestation
│ ├── estimate() — estimate gas
│ └── health() — service health check
├── stamps: StampsModule (evidence collection orchestration)
│ ├── collect() — collect raw signals from plugins
│ ├── create() — process signals into unsigned stamp
│ ├── sign() — sign a stamp
│ └── verify() — verify stamp validity
├── proofs: ProofsModule (proof construction + verification)
│ ├── create() — bundle claim + stamps into proof
│ └── verify() — verify proof (local or TEE mode)
└── plugins: PluginRegistry (plugin registration + discovery)
├── register() — register a plugin
├── get() — get plugin by name
├── has() — check if plugin exists
├── list() — list plugin metadata
├── all() — get all plugins
└── withMethod() — find plugins with a specific method
```
## Key concepts
Clear separation: `location.offchain.*`, `location.onchain.*`, `compute.*`, `stamps.*`, `proofs.*`, and `plugins.*`
Accept UIDs, raw GeoJSON, or attestation objects interchangeably.
SDK handles the delegated attestation pattern — you pay gas, Astral is attester.
Register proof-of-location plugins for multifactor evidence collection and verification.
## Pages
Install and configure the SDK
Offchain and onchain attestation workflows
Evidence collection and stamp creation
Proof construction and multidimensional verification
Plugin system and registry
Geospatial computation methods
Ethereum Attestation Service integration
Migrate from v0.1.x to v0.2.0
# Plugins
Source: https://docs.astral.global/sdk/plugins
Extensible plugin system for proof-of-location integrations
**Research Preview** — The plugin system is under active development.
# Plugins
The Astral SDK provides an extensible plugin system for integrating proof-of-location systems. Plugins implement a standard interface and are registered with the SDK at startup. The `StampsModule` and `ProofsModule` then orchestrate across registered plugins.
```typescript theme={null}
import { AstralSDK } from '@decentralized-geo/astral-sdk';
const astral = new AstralSDK({ chainId: 84532, signer: wallet });
// Access via astral.plugins
astral.plugins.register(plugin);
astral.plugins.get(name);
astral.plugins.has(name);
astral.plugins.list();
astral.plugins.all();
astral.plugins.withMethod(method);
```
***
## LocationProofPlugin interface
Every plugin implements this interface. All methods are optional — plugins implement what makes sense for their environment and data source.
```typescript theme={null}
interface LocationProofPlugin {
readonly name: string; // Unique identifier
readonly version: string; // Semver
readonly runtimes: Runtime[]; // Supported environments
readonly requiredCapabilities: string[]; // e.g., ['gps', 'network']
readonly description: string;
collect?(options?: CollectOptions): Promise;
create?(signals: RawSignals): Promise;
sign?(stamp: UnsignedLocationStamp, signer?: StampSigner): Promise;
verify?(stamp: LocationStamp): Promise;
}
```
### Method responsibilities
| Method | Purpose | When to implement |
| ----------- | ------------------------------------ | ---------------------------------------- |
| `collect()` | Gather raw signals from sensors/APIs | Plugin can actively collect evidence |
| `create()` | Parse signals into an unsigned stamp | Plugin produces structured location data |
| `sign()` | Add cryptographic signature to stamp | Plugin has its own signing mechanism |
| `verify()` | Check stamp internal validity | Plugin can verify its own stamps |
### Runtime type
```typescript theme={null}
type Runtime = 'react-native' | 'node' | 'browser';
```
***
## PluginRegistry API
### register()
Register a plugin with the SDK. Validates runtime compatibility and throws if the current environment is not supported.
```typescript theme={null}
astral.plugins.register(plugin: LocationProofPlugin): void
```
```typescript theme={null}
import { MockPlugin } from '@decentralized-geo/astral-sdk';
astral.plugins.register(new MockPlugin());
// Throws if current runtime is not in plugin.runtimes
```
### get()
Get a plugin by name. Throws if not found.
```typescript theme={null}
astral.plugins.get(name: string): LocationProofPlugin
```
```typescript theme={null}
const mock = astral.plugins.get('mock');
console.log(mock.version); // '0.1.0'
```
### has()
Check if a plugin is registered.
```typescript theme={null}
astral.plugins.has(name: string): boolean
```
### list()
List metadata for all registered plugins.
```typescript theme={null}
astral.plugins.list(): PluginMetadata[]
```
```typescript theme={null}
interface PluginMetadata {
name: string;
version: string;
runtimes: Runtime[];
requiredCapabilities: string[];
description: string;
}
```
### all()
Get all registered plugin instances.
```typescript theme={null}
astral.plugins.all(): LocationProofPlugin[]
```
### withMethod()
Find plugins that implement a specific method.
```typescript theme={null}
astral.plugins.withMethod(
method: keyof LocationProofPlugin
): LocationProofPlugin[]
```
```typescript theme={null}
// Find all plugins that can collect signals
const collectors = astral.plugins.withMethod('collect');
console.log(collectors.map(p => p.name)); // ['mock']
```
### Properties
```typescript theme={null}
astral.plugins.currentRuntime // Current detected runtime: 'node', 'browser', or 'react-native'
astral.plugins.size // Number of registered plugins
```
***
## Plugin status
| Plugin | Package | Status | collect | create | sign | verify |
| ------------ | -------------------------------------- | -------------- | ----------- | ----------- | ----------- | ----------- |
| Mock | Built into SDK | Complete | Yes | Yes | Yes | Yes |
| ProofMode | `@location-proofs/plugin-proofmode` | Alpha | Coming soon | Yes | — | Yes |
| WitnessChain | `@location-proofs/plugin-witnesschain` | In development | Coming soon | Coming soon | Coming soon | Coming soon |
See the Plugins section for detailed documentation on each plugin.
***
## MockPlugin
Built into the SDK for testing and development. Runs in all environments.
```typescript theme={null}
import { MockPlugin } from '@decentralized-geo/astral-sdk';
const mock = new MockPlugin({
lat: 37.7749, // Default: 40.7484 (Empire State Building)
lon: -122.4194, // Default: -73.9857
jitterMeters: 5, // Random offset in meters (default: 0)
accuracy: 10, // Meters (default: 10)
timestamp: 1700000000, // Unix seconds (default: current time)
durationSeconds: 60, // Temporal footprint duration (default: 60)
privateKey: '0x...' // Deterministic signing key (optional)
});
astral.plugins.register(mock);
```
See [MockPlugin documentation](/plugins/mock) for full details.
***
## How to write a custom plugin
Implement the `LocationProofPlugin` interface with whichever methods your system supports, then register with `astral.plugins.register()`. See [Building a custom plugin](/plugins/custom) for the complete guide with examples and testing patterns.
Geospatial computation methods
# Stamps Module
Source: https://docs.astral.global/sdk/stamps
Evidence collection and stamp creation across registered plugins
**Research Preview** — The stamps module is under active development.
# Stamps module
The `StampsModule` orchestrates evidence collection, stamp creation, signing, and verification across registered plugins. Each stamp represents evidence from a single proof-of-location system.
```typescript theme={null}
import { AstralSDK } from '@decentralized-geo/astral-sdk';
const astral = new AstralSDK({
chainId: 84532,
signer: wallet,
apiUrl: 'https://staging-api.astral.global'
});
// Access via astral.stamps
astral.stamps.collect(options);
astral.stamps.create(options, signals);
astral.stamps.sign(options, stamp, signer);
astral.stamps.verify(stamp, options);
```
For conceptual background, see [Location Proofs](/concepts/location-proofs).
***
## stamps.collect()
Collect raw signals from registered plugins.
```typescript theme={null}
astral.stamps.collect(
options?: StampsCollectOptions
): Promise
```
Returns an **array** of `RawSignals` — one per plugin that implements `collect()`. Not all plugins support collection; ProofMode, for example, collects on-device via its mobile app rather than through the SDK.
### Parameters
| Parameter | Type | Required | Description |
| ----------------- | ---------- | -------- | -------------------------------------------------------------------------------------------------- |
| `options.plugins` | `string[]` | No | Subset of registered plugins to collect from. Omit to collect from all that implement `collect()`. |
| `options.timeout` | `number` | No | Collection timeout in milliseconds |
| `options.signals` | `string[]` | No | Specific signals to request (plugin-dependent) |
### Example
```typescript theme={null}
// Collect from all plugins that support it
const allSignals = await astral.stamps.collect();
// Returns: [{ plugin: 'mock', timestamp: ..., data: {...} }, ...]
// Collect from specific plugins
const signals = await astral.stamps.collect({
plugins: ['mock'],
timeout: 5000
});
```
***
## stamps.create()
Process raw signals into an unsigned stamp using a specific plugin.
```typescript theme={null}
astral.stamps.create(
options: StampsCreateOptions,
signals: RawSignals
): Promise
```
### Parameters
| Parameter | Type | Required | Description |
| ---------------- | ------------ | -------- | ----------------------------------------------- |
| `options.plugin` | `string` | Yes | Name of the plugin to use for stamp creation |
| `signals` | `RawSignals` | Yes | Raw signals from `collect()` or external source |
### Example
```typescript theme={null}
const signals = (await astral.stamps.collect({ plugins: ['mock'] }))[0];
const unsigned = await astral.stamps.create(
{ plugin: 'mock' },
signals
);
console.log(unsigned.location); // GeoJSON Point
console.log(unsigned.temporalFootprint); // { start, end }
console.log(unsigned.plugin); // 'mock'
```
***
## stamps.sign()
Sign an unsigned stamp with a key.
```typescript theme={null}
astral.stamps.sign(
options: StampsSignOptions,
stamp: UnsignedLocationStamp,
signer: StampSigner
): Promise
```
### Parameters
| Parameter | Type | Required | Description |
| ---------------- | ----------------------- | -------- | ---------------------- |
| `options.plugin` | `string` | Yes | Name of the plugin |
| `stamp` | `UnsignedLocationStamp` | Yes | Output from `create()` |
| `signer` | `StampSigner` | Yes | Signing key |
### StampSigner
```typescript theme={null}
interface StampSigner {
algorithm: string; // e.g., "secp256k1", "ed25519"
signer: SubjectIdentifier; // { scheme, value }
sign(data: string): Promise;
}
```
### Example
```typescript theme={null}
const stamp = await astral.stamps.sign(
{ plugin: 'mock' },
unsigned,
{
algorithm: 'secp256k1',
signer: { scheme: 'eth-address', value: wallet.address },
sign: (data) => wallet.signMessage(data)
}
);
// stamp now has a signatures array and is ready for proofs
```
***
## stamps.verify()
Verify a stamp's internal validity.
```typescript theme={null}
astral.stamps.verify(
stamp: LocationStamp,
options?: { hosted?: boolean }
): Promise
```
### Parameters
| Parameter | Type | Required | Description |
| ---------------- | --------------- | -------- | --------------------------------------------------------------------------------------- |
| `stamp` | `LocationStamp` | Yes | The stamp to verify |
| `options.hosted` | `boolean` | No | If `true`, send to Astral API for verification. Default: local verification via plugin. |
### Example
```typescript theme={null}
// Local verification (delegates to the stamp's plugin)
const result = await astral.stamps.verify(stamp);
console.log(result.valid); // true
console.log(result.signaturesValid); // true
console.log(result.structureValid); // true
console.log(result.signalsConsistent); // true
console.log(result.details); // plugin-specific details
// Hosted verification
const hosted = await astral.stamps.verify(stamp, { hosted: true });
```
***
## Types
### RawSignals
```typescript theme={null}
interface RawSignals {
plugin: string; // Plugin that produced these signals
timestamp: number; // Unix seconds
data: Record; // Plugin-specific signal data
}
```
### UnsignedLocationStamp
```typescript theme={null}
interface UnsignedLocationStamp {
lpVersion: string; // e.g., "0.2"
locationType: string; // e.g., "geojson-point"
location: LocationData; // GeoJSON geometry or string
srs: string; // Spatial reference system (e.g., "EPSG:4326")
temporalFootprint: TimeBounds; // { start, end } — Unix seconds
plugin: string; // Plugin name
pluginVersion: string; // Semver
signals: Record; // Plugin-specific data
}
```
### LocationStamp
```typescript theme={null}
interface LocationStamp extends UnsignedLocationStamp {
signatures: Signature[]; // One or more signatures
}
interface Signature {
signer: SubjectIdentifier; // { scheme, value }
algorithm: string; // e.g., "secp256k1", "pgp"
value: string; // Hex or base64 encoded
timestamp: number; // Unix seconds
}
```
### StampVerificationResult
```typescript theme={null}
interface StampVerificationResult {
valid: boolean;
signaturesValid: boolean;
structureValid: boolean;
signalsConsistent: boolean;
details: Record; // Plugin-specific verification details
}
```
### StampsCollectOptions
```typescript theme={null}
interface StampsCollectOptions {
plugins?: string[]; // Subset of plugins, or all
timeout?: number; // Milliseconds
signals?: string[]; // Specific signals to request
}
```
### StampsCreateOptions
```typescript theme={null}
interface StampsCreateOptions {
plugin: string; // Which plugin to use
}
```
### StampsSignOptions
```typescript theme={null}
interface StampsSignOptions {
plugin: string; // Which plugin to use
}
```
***
## Complete example
```typescript theme={null}
import { AstralSDK, MockPlugin } from '@decentralized-geo/astral-sdk';
import { ethers } from 'ethers';
// Setup
const wallet = new ethers.Wallet(process.env.PRIVATE_KEY);
const astral = new AstralSDK({
chainId: 84532,
signer: wallet,
apiUrl: 'https://staging-api.astral.global'
});
// Register a plugin
astral.plugins.register(new MockPlugin({
lat: 37.7749,
lon: -122.4194,
accuracy: 10
}));
// 1. Collect signals
const signals = (await astral.stamps.collect({ plugins: ['mock'] }))[0];
// 2. Create unsigned stamp
const unsigned = await astral.stamps.create({ plugin: 'mock' }, signals);
// 3. Sign the stamp
const stamp = await astral.stamps.sign(
{ plugin: 'mock' },
unsigned,
{
algorithm: 'secp256k1',
signer: { scheme: 'eth-address', value: wallet.address },
sign: (data) => wallet.signMessage(data)
}
);
// 4. Verify
const result = await astral.stamps.verify(stamp);
console.log('Valid:', result.valid);
// stamp is now ready to include in a location proof
```
Bundle stamps into proofs and verify with CredibilityVector
# Types
Source: https://docs.astral.global/sdk/types
TypeScript type definitions for the Astral SDK
**Research Preview** — APIs may change. [GitHub](https://github.com/AstralProtocol)
# Types
Core TypeScript types exported by `@decentralized-geo/astral-sdk`.
## SDK configuration
```typescript theme={null}
interface AstralSDKConfig {
chainId: number; // Target chain ID (e.g., 84532 for Base Sepolia)
apiUrl?: string; // API base URL (default: https://staging-api.astral.global)
signer?: ethers.Signer; // Ethers signer for onchain submissions
}
```
## Compute types
### Input types
Geographic features can be provided as raw GeoJSON, EAS attestation UIDs, or offchain references.
```typescript theme={null}
/** GeoJSON geometry */
type GeoJSONInput = {
type: 'Point' | 'LineString' | 'Polygon' | 'MultiPoint' | 'MultiLineString' | 'MultiPolygon';
coordinates: number[] | number[][] | number[][][] | number[][][][];
};
/** Onchain attestation UID */
type UIDInput = string; // "0x..."
/** Offchain attestation reference */
type OffchainInput = {
uid: string;
uri: string; // IPFS or HTTP URI
};
/** Inline attestation object */
type InlineAttestationInput = {
attestation: {
uid: string;
schema: string;
data: string;
};
};
type Input = GeoJSONInput | UIDInput | OffchainInput | InlineAttestationInput;
```
### Compute request types
```typescript theme={null}
interface DistanceRequest {
from: Input;
to: Input;
chainId: number;
schema?: string;
recipient?: string;
}
interface AreaRequest {
geometry: Input;
chainId: number;
schema?: string;
recipient?: string;
}
interface LengthRequest {
geometry: Input;
chainId: number;
schema?: string;
recipient?: string;
}
interface ContainsRequest {
container: Input;
geometry: Input;
chainId: number;
schema?: string;
recipient?: string;
}
interface WithinRequest {
geometry: Input;
target: Input;
radius: number; // meters
chainId: number;
schema?: string;
recipient?: string;
}
interface IntersectsRequest {
geometry1: Input;
geometry2: Input;
chainId: number;
schema?: string;
recipient?: string;
}
```
### Compute result
```typescript theme={null}
interface ComputeResult {
result: T;
operation: string;
units?: string; // "meters" | "square_meters"
timestamp: number;
inputRefs: string[];
attestation: AttestationData;
delegatedAttestation: DelegatedAttestationData;
}
```
## Attestation types
```typescript theme={null}
interface AttestationData {
schema: string;
attester: string;
recipient: string;
data: string; // ABI-encoded
signature: string;
}
interface DelegatedAttestationData {
signature: string;
attester: string;
deadline: number; // Unix timestamp
}
```
## Verify types
See the [Verify API reference](/api-reference/verify/proof) and the [types reference](/api-reference/types) for the full `LocationClaim`, `LocationStamp`, `LocationProof`, and `CredibilityVector` type definitions.
## EAS types
```typescript theme={null}
interface SubmitDelegatedOptions {
signature: string;
attester: string;
schema: string;
data: string;
recipient: string;
deadline: number;
}
interface SubmitResult {
hash: string; // Transaction hash
uid: string; // Attestation UID
}
```
# Architecture
Source: https://docs.astral.global/trust-model/architecture
TEE, PostGIS, and the stateless execution model
**Research Preview** — APIs may change. [GitHub](https://github.com/AstralProtocol)
# Architecture
Astral runs geocomputation inside a self-contained Docker container, designed to execute within a Trusted Execution Environment (TEE) via EigenCompute. This page describes how the system is built.
**Deployment status.** Astral has run these services on real TEE hardware in test deployments, but does not currently fund continuous operation on attested hardware. The hosted staging service signs results with a key Astral controls; continuous remote attestation of the running enclave is target-state, not a live guarantee today. A valid signature today proves that a key Astral holds produced the result — not yet that an independently attested enclave did. See [What you are trusting](/trust-model/what-you-are-trusting) for the full accounting. If you want to evaluate Astral against real TEEs, reach out at [contact@astral.global](mailto:contact@astral.global).
## Execution model
```mermaid theme={null}
flowchart TB
subgraph TEE["EigenCompute TEE Environment"]
subgraph Container["Docker Container"]
subgraph Engine["Astral Geocomputation Engine"]
E1["Validates inputs"]
E2["Executes PostGIS queries"]
E3["Signs results with TEE-held key"]
end
subgraph DB["PostgreSQL + PostGIS"]
D1["All spatial computations"]
D2["Ephemeral, no persistent state"]
end
Engine --> DB
end
G1["TEE guarantees:"]
G2["• Code hasn't been modified"]
G3["• Inputs weren't tampered with"]
G4["• Outputs came from executing that code"]
end
```
## Container design
PostGIS runs **inside** the Docker container, not as an external service. This is essential for verifiable computation in the TEE — no external dependencies means the entire execution environment is attested.
Each request brings all required inputs. No persistent state between requests. This ensures determinism and simplifies verification — same inputs always produce same outputs.
The service holds a signing key that is generated within the TEE or securely provisioned. The design intent is that the operator cannot extract it — a property that holds when the enclave runs under remote attestation (see deployment status above). All signed results are produced with this key.
## Internal computation flow
```mermaid theme={null}
flowchart LR
subgraph TEE["EigenCompute TEE"]
subgraph Container["Docker Container"]
API[Node.js API] --> PostGIS[PostgreSQL + PostGIS]
PostGIS --> GEOS[GEOS Library]
end
end
Input[Request] --> API
API --> Output[Signed Result]
```
PostGIS uses [GEOS](https://libgeos.org/) for geometry operations — the same C++ library used by QGIS, GDAL, and most professional geospatial software.
## Why this architecture
The design choices above serve a single goal: making geocomputation results verifiable.
* **Self-contained** means the TEE attestation covers the entire execution environment. No external database calls that could be intercepted or altered.
* **Stateless** means determinism is straightforward. Given the same inputs, the container produces the same output every time.
* **Key inside TEE** means that, when the enclave runs under attestation, the signing key cannot be extracted or used outside it. If you trust the TEE — and the enclave is attested — you trust the signature. Today, a valid signature proves a key Astral controls produced the result; binding that key to a continuously attested enclave is target-state, not a live guarantee.
What the signature covers and what computation reproducibility means
# Security Considerations
Source: https://docs.astral.global/trust-model/security
Threat model, known limitations, and responsible disclosure
**Research Preview** — APIs may change. [GitHub](https://github.com/AstralProtocol)
# Security considerations
This page documents the security model, known limitations, and best practices for building with Astral.
## Astral signer address
**Current Astral Signer (Base Sepolia):** `0x590fdb53ed3f0B52694876d42367192a5336700F`
Resolver contracts must verify that `attestation.attester` equals this address. See [Staging](/resources/staging) for the full configuration.
## Known considerations
### Replay attacks
**Status:** Documented, resolver responsibility
Signed results could potentially be reused:
* **Temporal replay:** Old result used for current benefit
* **Cross-context replay:** Result for one resolver used at another
**Mitigations (your responsibility):**
```solidity theme={null}
contract SecureResolver is SchemaResolver {
mapping(bytes32 => bool) public usedAttestations;
function onAttest(Attestation calldata attestation, uint256)
internal override returns (bool)
{
// 1. Check not already used
bytes32 attHash = keccak256(abi.encode(attestation.uid));
require(!usedAttestations[attHash], "Already used");
usedAttestations[attHash] = true;
// 2. Check timestamp freshness
(, , uint64 timestamp, ) = abi.decode(...);
require(timestamp > block.timestamp - 1 hours, "Too old");
// 3. Verify expected inputs
(, bytes32[] memory inputRefs, , ) = abi.decode(...);
require(inputRefs[1] == EXPECTED_LOCATION, "Wrong location");
// ... business logic
}
}
```
### Input trust
**Status:** Raw GeoJSON not verified
Raw GeoJSON inputs are accepted for flexibility, but are **not verified for authenticity**:
```typescript theme={null}
// This works but geometry source is unverified
const result = await astral.compute.contains(
{ type: 'Polygon', coordinates: [...] }, // Raw, unverified
userLocationUID
);
```
The signed result proves:
* "Astral computed the relationship between A and B"
It does **not** prove:
* "Geometry A came from a trusted source"
* "User was actually at location B"
**Best practice:** For high-security applications, require attested inputs (UIDs) rather than raw GeoJSON.
### GPS spoofing
**Status:** Partially addressed by location proofs; an active research area
Astral verifies that a computation was correct, not that the input location is where the device actually was. Raw GPS can be spoofed. Location proofs raise the cost: ProofMode, for example, includes on-device protections that resist some *software* spoofing (such as detecting rooted or tampered devices), but offers little against *hardware/physical* attacks like attenuating or replaying the RF signal. The goal is to raise the cost of forgery above the value of the action a proof supports, not to achieve certainty.
**Future:** Combining multiple independent, corroborating stamps raises the bar further, and we're researching harder proof-of-location systems — including authenticated GNSS signals such as Galileo [OSNMA](https://www.gsc-europa.eu/galileo/services/galileo-open-service-navigation-message-authentication-osnma).
### TEE attestation deployment
**Status:** Test deployments only; continuous attested operation not yet funded
The signature on a result proves it was produced by a key Astral controls. Binding that key to an independently attested enclave — so a valid signature also proves *which code* ran and *where* — requires the service to run under continuous remote attestation. Astral has demonstrated this on real TEE hardware in test deployments but does not currently fund continuous attested operation.
Until then, treat a valid Astral signature as "signed by Astral's key," not as a hardware-attestation guarantee. If you want to evaluate Astral against real TEEs, reach out at [contact@astral.global](mailto:contact@astral.global).
## Best practices
### For resolver authors
```solidity theme={null}
require(attestation.attester == astralSigner, "Not from Astral");
```
```solidity theme={null}
require(timestamp > block.timestamp - MAX_AGE, "Attestation too old");
```
```solidity theme={null}
require(inputRefs[1] == EXPECTED_LANDMARK, "Wrong location checked");
```
```solidity theme={null}
require(!usedAttestations[uid], "Already used");
usedAttestations[uid] = true;
```
```solidity theme={null}
function updateSigner(address newSigner) external onlyOwner {
astralSigner = newSigner;
}
```
### For application developers
* **Prefer UIDs** over raw GeoJSON for sensitive operations
* **Set appropriate timeouts** — don't accept stale results
* **Validate recipient** — ensure the result is for the right user
* **Handle signature expiry** — delegated attestation signatures have deadlines
## Key management
### Service signing key
* Key generated or provisioned within the TEE
* Intended to be non-extractable by operators when the enclave runs under attestation (see [TEE attestation deployment](#tee-attestation-deployment))
* Used to sign all results
### Key rotation
Resolver contracts should support key rotation:
```solidity theme={null}
address public astralSigner;
event SignerUpdated(address oldSigner, address newSigner);
function updateAstralSigner(address newSigner) external onlyOwner {
emit SignerUpdated(astralSigner, newSigner);
astralSigner = newSigner;
}
```
For the Research Preview, a simple owner-controlled update is sufficient. For production, consider making the owner a multisig.
## Audit status
| Component | Status |
| ----------------- | ------- |
| Compute Service | Pending |
| SDK | Pending |
| Example Contracts | Pending |
Full security audit will be conducted before mainnet deployment.
# What Is Verified
Source: https://docs.astral.global/trust-model/what-is-verified
What the signature covers and what computation reproducibility means
**Research Preview** — APIs may change. [GitHub](https://github.com/AstralProtocol)
# What is verified
When Astral produces a signed result, that signature makes specific claims. This page spells out exactly what is covered — and what that coverage depends on.
**Deployment status.** The guarantees below hold when the service runs inside an enclave under continuous remote attestation. Astral has done this on real TEE hardware in test deployments, but does not currently fund continuous operation on attested hardware. On the hosted staging service today, a valid signature proves a key Astral controls produced the result — binding that key to an independently attested enclave is target-state. To evaluate Astral against real TEEs, reach out at [contact@astral.global](mailto:contact@astral.global).
## Verifiability properties
| Property | How it's achieved |
| ----------------------- | ------------------------------------------------------------------------------ |
| **Input integrity** | Input signatures verified at the TEE boundary before processing |
| **Execution integrity** | Under attestation, the TEE ensures code runs as deployed and can't be modified |
| **Output authenticity** | Signing key held inside the TEE; under attestation, can't be extracted |
| **Determinism** | Stateless model + fixed precision = same inputs produce same outputs |
The integrity and authenticity properties depend on the enclave running under continuous remote attestation — see the deployment-status note above for what holds on the hosted service today.
**Privacy is a near-term design direction.** Today, signed results can still expose their inputs (see [Privacy](/concepts/privacy)). The same TEE design makes stronger privacy feasible — encrypted input coordinates, shielded outputs (a policy decision carrying no identifying detail), and outputs encrypted so only a specified counterparty can read them. We're designing these now; if that would be useful to you, reach out at [contact@astral.global](mailto:contact@astral.global).
## What the signature covers
Under attestation (see deployment status above), a signed result from Astral is intended to prove three things:
1. **The computation ran inside the TEE.** EigenCompute is designed to provide hardware attestation that the expected code is executing in the attested environment.
2. **The inputs were hashed and recorded.** Every signed result includes `inputRefs` — hashes of the inputs used. You can verify which inputs went into the computation. This holds today regardless of attestation status.
3. **The output was produced by that computation.** The signing key is held inside the TEE. When the enclave is attested, a valid signature means the output came from the attested code running on the referenced inputs. Without live attestation, a valid signature proves only that Astral's key signed the result.
## What the signature does not establish: location correspondence
The signature proves the *computation* (or, for verification, the *evaluation*) ran correctly on the stated inputs. It does **not**, on its own, establish that a location claim corresponds to physical reality. That correspondence comes from two things outside the signature:
* the **evidence** bundled into the [location proof](/concepts/location-proofs) — stamps from one or more proof-of-location systems, and
* the **evaluation function** applied to that evidence, which produces the [credibility vector](/concepts/location-proof-evaluation).
Astral runs that evaluation and reports it honestly; it does not pronounce a claim "true." **It is up to the verifier to decide whether a given evidence set and evaluation function are convincing enough for their context** — a $10 check-in and a $10M asset transfer warrant very different thresholds.
## Input references (inputRefs)
Every signed result includes an array of `inputRefs` — deterministic references to the inputs used in the computation. These let downstream consumers verify which inputs were used:
* For geographic features referenced by UID, the `inputRef` is the UID itself
* For raw GeoJSON inputs, the `inputRef` is a hash of the geometry
This means you can check not just *that* a computation was performed, but *what data* it operated on.
## Computation reproducibility
Determinism is what makes signed results meaningful. If someone else runs the same computation on the same inputs, they should get the same answer.
Astral achieves this through:
* **Centimeter precision rounding** before signing — eliminates floating-point variance
* **Pinned PostGIS version** in the container — no algorithm changes between builds
* **Stateless execution** — no accumulated state that could affect results
## The signing key
The service holds a signing key **inside** the TEE:
* Key is generated within the TEE or securely provisioned
* Intended to be non-extractable by the operator when the enclave is attested
* All results are signed with this key
* Downstream consumers verify that results came from the known Astral signer
Verifying that the attester matches Astral's signer proves the result came from a key Astral controls. Binding that key to an independently attested enclave — so a matching signature also proves *where* and *how* it was produced — is the target-state property described in the deployment-status note above.
```solidity theme={null}
// In a resolver contract
require(attestation.attester == ASTRAL_SIGNER, "Not from Astral");
```
Honest accounting of current assumptions
# What You Are Trusting
Source: https://docs.astral.global/trust-model/what-you-are-trusting
Honest accounting of current assumptions and the path forward
**Research Preview** — APIs may change. [GitHub](https://github.com/AstralProtocol)
# What you are trusting
We're transparent about what's verified and what's assumed because trust is the product. This page gives an honest accounting of the current trust assumptions and where they're headed.
## Current trust assumptions
| Assumption | Status | Notes |
| ----------------------------------------- | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| TEE executes code correctly | Verified **under attestation** | EigenCompute provides hardware attestation; continuous attested deployment is target-state (see below) |
| Service runs under continuous attestation | **Not yet** | Demonstrated in test deployments; not continuously funded today |
| Astral operates service honestly | Required | Single operator in the Research Preview |
| Signing key held securely in TEE | Verified **under attestation** | Intended to be non-extractable when the enclave is attested |
| Input locations are truthful | **Depends on the input** | Raw coordinates are trust-based; a [location proof](/concepts/location-proofs) backs the input with evaluable evidence — available today (ProofMode end to end; other plugins experimental) |
The Research Preview uses a **centralized trust model**: a single service with a known signer, designed to run inside a TEE. The TEE provides execution attestation, and deterministic operations ensure reproducibility. But you are trusting Astral to operate the service honestly.
**Deployment status.** Astral has run the service on real TEE hardware in test deployments, but does not currently fund continuous operation on attested hardware. On the hosted staging service today, a valid signature proves that a key Astral controls produced the result — not yet that an independently attested enclave did. This is the gap between "signed by a key" and "signed by an attested enclave," and we'd rather state it plainly. If you want to evaluate Astral against real TEEs, reach out at [contact@astral.global](mailto:contact@astral.global).
## Raw location inputs are trust-based — location proofs change that
If you pass **raw coordinates** to Astral, you are trusting they're honest. GPS is spoofable; a user can claim to be anywhere. A signed result over raw coordinates proves that *if* the user was at location A, *then* they were within 500m of location B — not that they were actually at A.
This is exactly what [location proofs](/concepts/location-proofs) address, and they exist today. Instead of bare coordinates, you submit a claim backed by evidence — stamps from one or more [proof-of-location systems](/concepts/pol-systems). Astral evaluates that evidence and returns a [credibility vector](/concepts/location-proof-evaluation). It can't make weak evidence strong, but it turns "trust the input" into "weigh the evidence and the evaluation." ProofMode works end to end now; other plugins are experimental, with interfaces defined (we're keen to develop more with partners — [get in touch](mailto:contact@astral.global)).
**Be direct with your users**: if you rely on raw coordinates rather than location proofs, the location data is trust-based — say so.
## What verification buys you today
Even with these assumptions, verifiable computation is meaningful:
* **The computation is correct.** You know the spatial relationship was evaluated faithfully, not fabricated.
* **The inputs are recorded.** `inputRefs` let you audit which data went into the computation.
* **The result is tamper-evident.** The signature proves the output was produced by Astral's key and hasn't been altered since. Under attestation, it further proves the output came from the attested environment.
This is substantially better than trusting an opaque API that returns `true` or `false` with no proof.
## The path forward
Multiple independent operators run the computation. Results must match to be accepted. No single operator can lie.
Cryptographic proof that the computation was correct. Verifiable by anyone without trusting the prover.
Multi-party computation for result signing. No single party holds the full key.
Evidence-based location claims replace raw GPS. Multiple corroborating stamps make spoofing harder.
Each enhancement reduces the trust surface. AVS consensus removes the single-operator assumption. ZK proofs make verification independent of hardware trust. Decentralized signers eliminate the single key. Location proofs address the input honesty gap.
Threat model, known limitations, and responsible disclosure
# Delivery Verification
Source: https://docs.astral.global/use-cases/delivery-verification
Escrow that releases when the courier arrives
**Research Preview** — APIs may change. [GitHub](https://github.com/AstralProtocol)
# 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](https://collective.flashbots.net/t/towards-stronger-location-proofs/5323) 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.
```solidity theme={null}
// 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.
```typescript theme={null}
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.
```typescript theme={null}
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
```typescript theme={null}
// 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 (
);
}
```
## Extensions
### Multi-signature confirmation
Require both courier arrival and buyer confirmation:
```solidity theme={null}
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:
```solidity theme={null}
address public arbiter;
function resolveDispute(bytes32 deliveryId, bool payToCourier) external {
require(msg.sender == arbiter);
// Handle dispute resolution
}
```
Policies that trigger based on verified proximity
# Geofence Compliance
Source: https://docs.astral.global/use-cases/geofence-compliance
Prove an asset stayed within an approved boundary
**Research Preview** — APIs may change. [GitHub](https://github.com/AstralProtocol)
# Geofence compliance
You're managing a drone fleet. Regulators require proof that each drone stayed within its approved flight corridor. Astral's `contains` operation produces a signed result recording whether each reported position fell inside the corridor — verifiable, auditable evidence of the *check*.
The drone's reported position is supplied as coordinates. The signed result proves the containment **computation** was performed correctly on those coordinates — it does not, by itself, prove the drone was physically there. Raw GPS can be spoofed. To strengthen the position itself, attach [location proofs](/concepts/location-proofs) (for example, hardware-attested device evidence) so the input carries its own credibility, not just the computation. This is a Research Preview — be direct with regulators about which inputs are evidence-backed and which are self-reported.
## How it works
1. Define the approved corridor as a polygon
2. Periodically check the drone's position with `compute.contains`
3. Each check produces a signed result
4. A compliance report is a series of signed results covering the flight duration
## Define the approved corridor
Register the approved flight corridor as an onchain location record.
```typescript theme={null}
import { AstralSDK } from '@decentralized-geo/astral-sdk';
const astral = new AstralSDK({ chainId: 84532, signer: wallet });
// Register approved flight corridor
const corridor = await astral.location.onchain.create({
location: {
type: 'Polygon',
coordinates: [[
[-122.420, 37.780],
[-122.410, 37.780],
[-122.410, 37.790],
[-122.420, 37.790],
[-122.420, 37.780]
]]
},
memo: "Approved flight corridor — permit #DR-2025-0042"
});
console.log('Corridor UID:', corridor.uid);
```
## Periodic compliance checks
At regular intervals during the flight, check whether the drone's position falls within the corridor.
```typescript theme={null}
async function checkCompliance(
droneCoords: [number, number],
corridorUID: string,
wallet: Signer
) {
const astral = new AstralSDK({ chainId: 84532, signer: wallet });
// Register current drone position
const dronePosition = await astral.location.onchain.create({
location: { type: 'Point', coordinates: droneCoords }
});
// Check containment — args are positional: (container, containee, options)
const result = await astral.compute.contains(
corridorUID,
dronePosition.uid,
{ schema: SCHEMA_UID }
);
return {
inCorridor: result.result,
timestamp: result.timestamp,
attestation: result.attestation
};
}
```
## Building a compliance report
A compliance report is a time series of signed results. Each one is independently verifiable.
```typescript theme={null}
interface ComplianceRecord {
timestamp: number;
inCorridor: boolean;
attestationUID: string;
}
async function runComplianceLoop(
corridorUID: string,
getDronePosition: () => Promise<[number, number]>,
intervalMs: number,
wallet: Signer
): Promise {
// `records` is returned immediately and filled over time as the interval
// fires (the caller holds the same array reference). In real use you'd
// persist each record as it's produced and clear the interval when the
// flight ends — illustrative here.
const records: ComplianceRecord[] = [];
const interval = setInterval(async () => {
const coords = await getDronePosition();
const result = await checkCompliance(coords, corridorUID, wallet);
records.push({
timestamp: result.timestamp,
inCorridor: result.inCorridor,
attestationUID: result.attestation.uid
});
// Alert on violation
if (!result.inCorridor) {
console.warn(`Corridor violation at ${new Date(result.timestamp * 1000).toISOString()}`);
}
}, intervalMs);
return records;
}
```
## Submitting compliance onchain
For regulatory requirements that demand onchain proof, submit each signed result as an EAS attestation.
```typescript theme={null}
async function submitComplianceProof(
result: BooleanComputeResponse,
wallet: Signer
) {
const astral = new AstralSDK({ chainId: 84532, signer: wallet });
const { uid } = await astral.compute.submit({
attestation: result.attestation,
delegatedAttestation: result.delegatedAttestation,
});
return uid;
}
```
## What the regulator sees
Each signed result in the compliance report includes:
| Field | Value |
| ---------------- | ------------------------------------------------- |
| Operation | `contains` |
| Result | `true` (in corridor) or `false` (violation) |
| Input references | Hashes of the drone position and corridor polygon |
| Timestamp | When the check was performed |
| Attester | Astral's TEE signing key |
The regulator can verify each result independently — the Astral signature proves the containment computation was performed correctly and the input references tie it to the specific corridor and reported position. What it does **not** prove on its own is that the drone was physically at that position: that depends on how the position was sourced (see the note above). A compliance story is only as strong as its weakest input.
## Variations
* **Maritime shipping** — verify vessels stay within approved shipping lanes
* **Autonomous vehicles** — prove a vehicle stayed within its operational design domain
* **Asset tracking** — verify high-value goods remained within approved transit routes
* **Environmental monitoring** — confirm survey equipment stayed within permitted research areas
End-to-end blockchain flow from computation to smart contract
# Onchain Attestation
Source: https://docs.astral.global/use-cases/onchain-attestation
End-to-end blockchain flow from computation to smart contract
**Research Preview** — APIs may change. [GitHub](https://github.com/AstralProtocol)
# Onchain attestation
This walkthrough covers the complete blockchain flow: register reference locations, compute a spatial relationship, submit the signed result as an EAS attestation, and let a resolver contract execute business logic.
The example builds an NFT that mints when the user is near a landmark — but the same pattern applies to any onchain action gated by spatial computation.
## The pattern
```mermaid theme={null}
sequenceDiagram
participant User
participant SDK as Astral SDK
participant Engine as Compute Service
participant EAS as EAS Contracts
participant Resolver as Your Resolver
User->>SDK: compute.within(...)
SDK->>Engine: Request computation
Engine-->>SDK: Signed result
SDK->>EAS: Submit delegated attestation
EAS->>Resolver: onAttest() callback
Resolver->>Resolver: Verify + execute logic
Resolver-->>EAS: return true/false
```
## Step 1: Register a reference location
Create an onchain location record for the landmark.
```typescript theme={null}
import { AstralSDK } from '@decentralized-geo/astral-sdk';
const astral = new AstralSDK({ chainId: 84532, signer: wallet });
const landmark = await astral.location.onchain.create({
location: { type: 'Point', coordinates: [-122.4194, 37.7749] },
memo: "San Francisco Visitor Center"
});
console.log('Landmark UID:', landmark.uid);
```
## Step 2: Deploy a resolver contract
The resolver receives the signed result via EAS's `onAttest` callback and executes your business logic — in this case, minting an NFT.
```solidity theme={null}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@eas/contracts/resolver/SchemaResolver.sol";
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract LocationGatedNFT is SchemaResolver, ERC721, Ownable {
address public astralSigner;
bytes32 public landmarkUID;
uint256 public nextTokenId = 1;
mapping(address => bool) public hasMinted;
mapping(bytes32 => bool) public usedAttestations;
error NotFromAstral();
error AlreadyUsed();
error WrongOperation();
error InvalidInputs();
error WrongLocation();
error AttestationTooOld();
error NotCloseEnough();
error AlreadyMinted();
event NFTMinted(address indexed recipient, uint256 tokenId, bytes32 attestationUID);
constructor(
IEAS eas,
address _astralSigner,
bytes32 _landmarkUID
)
SchemaResolver(eas)
ERC721("SF Visitor", "SFVISIT")
Ownable(msg.sender)
{
astralSigner = _astralSigner;
landmarkUID = _landmarkUID;
}
/// @dev Check if a string starts with a given prefix
function _startsWith(string memory str, string memory prefix) internal pure returns (bool) {
bytes memory strBytes = bytes(str);
bytes memory prefixBytes = bytes(prefix);
if (strBytes.length < prefixBytes.length) return false;
for (uint256 i = 0; i < prefixBytes.length; i++) {
if (strBytes[i] != prefixBytes[i]) return false;
}
return true;
}
function onAttest(
Attestation calldata attestation,
uint256 /*value*/
) internal override returns (bool) {
// 1. Verify from Astral's TEE signer
if (attestation.attester != astralSigner) revert NotFromAstral();
// 2. Prevent replay
if (usedAttestations[attestation.uid]) revert AlreadyUsed();
usedAttestations[attestation.uid] = true;
// 3. Decode signed result (boolean for 'within')
(
bool policyPassed,
bytes32[] memory inputRefs,
uint64 timestamp,
string memory operation
) = abi.decode(
attestation.data,
(bool, bytes32[], uint64, string)
);
// 4. Verify correct operation
if (!_startsWith(operation, "within")) revert WrongOperation();
// 5. Verify correct landmark was checked
if (inputRefs.length < 2) revert InvalidInputs();
if (inputRefs[1] != landmarkUID) revert WrongLocation();
// 6. Verify timestamp is recent (within 1 hour)
if (timestamp < block.timestamp - 1 hours) revert AttestationTooOld();
// 7. Verify user is within radius
if (!policyPassed) revert NotCloseEnough();
// 8. One mint per address
if (hasMinted[attestation.recipient]) revert AlreadyMinted();
hasMinted[attestation.recipient] = true;
// 9. Mint NFT
uint256 tokenId = nextTokenId++;
_mint(attestation.recipient, tokenId);
emit NFTMinted(attestation.recipient, tokenId, attestation.uid);
return true;
}
function onRevoke(Attestation calldata, uint256)
internal pure override returns (bool)
{
return false;
}
function updateAstralSigner(address _signer) external onlyOwner {
astralSigner = _signer;
}
}
```
## Step 3: Register the schema
Register an EAS schema that points to your resolver contract.
```typescript theme={null}
import { SchemaRegistry } from '@ethereum-attestation-service/eas-sdk';
const schemaRegistry = new SchemaRegistry(SCHEMA_REGISTRY_ADDRESS);
// Boolean result schema for 'within' operation
const schema = "bool result,bytes32[] inputRefs,uint64 timestamp,string operation";
const tx = await schemaRegistry.connect(signer).register({
schema,
resolverAddress: nftContract.address,
revocable: true // Must be true — Astral signs with revocable=true
});
const receipt = await tx.wait();
const SCHEMA_UID = receipt.logs[0].args.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.
## Step 4: Compute and submit
The user's app gets their location, computes proximity, and submits the signed result onchain.
```typescript theme={null}
async function mintNFT(userCoords: [number, number], wallet: Signer) {
const astral = new AstralSDK({ chainId: 84532, signer: wallet });
// Create user's location record
const userLocation = await astral.location.onchain.create({
location: { type: 'Point', coordinates: userCoords }
});
// Compute proximity — returns signed result
const result = await astral.compute.within(
userLocation.uid,
LANDMARK_UID,
500, // 500 meter radius
{
schema: SCHEMA_UID,
recipient: await wallet.getAddress()
}
);
if (!result.result) {
throw new Error('Not close enough to the landmark');
}
// Submit to EAS — triggers resolver, mints NFT
const { uid } = await astral.compute.submit(result.delegatedAttestation);
return {
attestationUID: uid
};
}
```
## Other resolver patterns
The same flow works for any onchain action. Swap the resolver logic to fit your use case:
**Token distribution** — airdrop tokens to users who prove they visited a location:
```solidity theme={null}
function _executeAction(address recipient) internal override {
token.transfer(recipient, amount);
}
```
**Access control** — grant onchain permissions based on spatial verification:
```solidity theme={null}
function _executeAction(address recipient) internal override {
hasAccess[recipient] = true;
}
```
**Geofenced transfers** — restrict token transfers to users within a region (see the [geofenced token guide](/guides/geofenced-token) for the full implementation).
For more resolver patterns, decoding signed results, and blockchain integration details, see [Blockchain integration](/guides/blockchain-integration).
Deep dive into resolver patterns and chain configuration
# Parametric Insurance
Source: https://docs.astral.global/use-cases/parametric-insurance
Policies that trigger based on verified proximity
**Research Preview** — APIs may change. [GitHub](https://github.com/AstralProtocol)
# Parametric insurance
You're building crop insurance. When a qualifying weather event occurs within a certain distance of insured farmland, the policy pays out — without a manual claims process or an adjuster visit — once the spatial condition is verified.
Astral verifies the **distance computation** between the inputs you supply. It does not establish that a qualifying weather event actually occurred, or that the weather-station coordinates are truthful — those come from outside the system (an oracle, a data feed, a registered location). The signed result is one trustworthy link in the chain, not the whole chain. Design the event-reporting and location inputs with the same care, and be explicit with policyholders about what is verified and what is trusted. This is a Research Preview, not a production insurance product.
## How it works
1. The insurance contract stores a farmland location UID and references to weather station locations
2. When a weather event is reported, the system computes the distance from the event to the insured farmland
3. If the distance is below the policy threshold, the payout triggers automatically
4. The signed result provides auditable proof that the spatial condition was met
## The flow
### Register locations
Both the insured farmland and weather station locations are registered as onchain location records.
```typescript theme={null}
import { AstralSDK } from '@decentralized-geo/astral-sdk';
const astral = new AstralSDK({ chainId: 84532, signer: wallet });
// Register farmland boundary
const farmland = await astral.location.onchain.create({
location: {
type: 'Polygon',
coordinates: [[
[-95.123, 38.456],
[-95.120, 38.456],
[-95.120, 38.460],
[-95.123, 38.460],
[-95.123, 38.456]
]]
},
memo: "Insured farmland parcel"
});
// Register weather station
const weatherStation = await astral.location.onchain.create({
location: { type: 'Point', coordinates: [-95.200, 38.470] },
memo: "NOAA weather station KS-042"
});
```
### Compute distance when an event occurs
When a qualifying weather event is reported at the station, compute the distance to the insured farmland.
```typescript theme={null}
async function checkPolicyTrigger(
farmlandUID: string,
weatherStationUID: string,
thresholdKm: number,
wallet: Signer
) {
const astral = new AstralSDK({ chainId: 84532, signer: wallet });
// Compute distance from weather event to farmland.
// Args are positional: (from, to, options). The numeric result is in meters.
const result = await astral.compute.distance(
weatherStationUID,
farmlandUID,
{ schema: SCHEMA_UID, recipient: INSURANCE_CONTRACT_ADDRESS }
);
const distanceKm = result.result / 1000;
if (distanceKm > thresholdKm) {
return { triggered: false, distanceKm };
}
// Submit signed result to trigger payout
const { uid } = await astral.compute.submit({
attestation: result.attestation,
delegatedAttestation: result.delegatedAttestation,
});
return { triggered: true, distanceKm, attestationUID: uid };
}
```
### Resolver contract
The insurance resolver decodes a numeric signed result and checks whether the distance is below the policy threshold.
```solidity theme={null}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@eas/contracts/resolver/SchemaResolver.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract ParametricInsurance is SchemaResolver, Ownable {
address public astralSigner;
struct Policy {
address beneficiary;
bytes32 farmlandUID;
bytes32 weatherStationUID;
uint256 thresholdCm; // Distance threshold in centimeters
uint256 payoutAmount;
bool active;
bool triggered;
}
mapping(bytes32 => Policy) public policies;
event PolicyCreated(bytes32 indexed policyId, address beneficiary, uint256 payout);
event PolicyTriggered(bytes32 indexed policyId, uint256 distanceCm);
constructor(IEAS eas, address _astralSigner)
SchemaResolver(eas)
Ownable(msg.sender)
{
astralSigner = _astralSigner;
}
function createPolicy(
bytes32 policyId,
address beneficiary,
bytes32 farmlandUID,
bytes32 weatherStationUID,
uint256 thresholdKm
) external payable onlyOwner {
require(msg.value > 0, "Must fund payout");
policies[policyId] = Policy({
beneficiary: beneficiary,
farmlandUID: farmlandUID,
weatherStationUID: weatherStationUID,
thresholdCm: thresholdKm * 100000, // km to cm
payoutAmount: msg.value,
active: true,
triggered: false
});
emit PolicyCreated(policyId, beneficiary, msg.value);
}
function onAttest(
Attestation calldata attestation,
uint256 /* value */
) internal override returns (bool) {
require(attestation.attester == astralSigner, "Not from Astral");
// Decode numeric signed result (distance)
(
uint256 distanceCm,
string memory units,
bytes32[] memory inputRefs,
uint64 timestamp,
string memory operation
) = abi.decode(
attestation.data,
(uint256, string, bytes32[], uint64, string)
);
// Verify this is a distance computation
require(
keccak256(bytes(operation)) == keccak256(bytes("distance")),
"Wrong operation"
);
// Extract policy ID from recipient
bytes32 policyId = bytes32(uint256(uint160(attestation.recipient)));
Policy storage policy = policies[policyId];
require(policy.active, "Policy not active");
require(!policy.triggered, "Already triggered");
// Verify correct locations were used
require(inputRefs.length >= 2, "Invalid inputs");
require(inputRefs[0] == policy.weatherStationUID, "Wrong weather station");
require(inputRefs[1] == policy.farmlandUID, "Wrong farmland");
// Check if distance is below threshold
require(distanceCm <= policy.thresholdCm, "Distance exceeds threshold");
// Trigger payout
policy.triggered = true;
(bool success, ) = policy.beneficiary.call{value: policy.payoutAmount}("");
require(success, "Payout failed");
emit PolicyTriggered(policyId, distanceCm);
return true;
}
function onRevoke(Attestation calldata, uint256)
internal pure override returns (bool)
{
return false;
}
function updateAstralSigner(address newSigner) external onlyOwner {
astralSigner = newSigner;
}
}
```
## Why this matters
Traditional crop insurance requires:
* Filing a claim
* Waiting for an adjuster
* Disputing coverage decisions
Parametric insurance reduces much of that. The spatial condition is defined upfront, the distance computation is independently verifiable, and the payout can be automated against it. The signed result is auditable proof that the *spatial condition was computed correctly on the given inputs* — the remaining trust (that the event occurred, that the station data is honest) sits with whatever feeds those inputs.
## Variations
* **Flood insurance** — use `intersects` to check if a flood polygon overlaps insured property
* **Wildfire proximity** — use `distance` from fire perimeter to insured structures
* **Hurricane path** — use `within` to check if insured property falls within the projected storm path
Prove an asset stayed within an approved boundary