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

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

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<RawSignals>;
  create?(signals: RawSignals): Promise<UnsignedLocationStamp>;
  sign?(stamp: UnsignedLocationStamp, signer?: StampSigner): Promise<LocationStamp>;
  verify?(stamp: LocationStamp): Promise<StampVerificationResult>;
}

interface RawSignals {
  plugin: string;
  timestamp: number;             // Unix seconds
  data: Record<string, unknown>; // plugin-specific signal data
}

interface StampVerificationResult {
  valid: boolean;
  signaturesValid: boolean;
  structureValid: boolean;
  signalsConsistent: boolean;
  details: Record<string, unknown>;
}

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 format:
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<string, unknown>;
  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

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<RawSignals> {
    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

  // `create` transforms RawSignals into an unsigned stamp
  async create(signals: RawSignals): Promise<UnsignedLocationStamp> {
    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

  // `verify` returns a StampVerificationResult:
  // { valid, signaturesValid, structureValid, signalsConsistent, details }
  async verify(stamp: LocationStamp): Promise<StampVerificationResult> {
    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:
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:
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:

ProofMode

Device attestation and sensor fusion. Uses Secure Enclave (iOS) or hardware keystore (Android) to sign location observations.

WitnessChain

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

Location proofs

Understand how stamps combine into verifiable proofs

Verifying location proofs

Submit proofs and understand credibility scores