GHAN— by singular Get started

GHAN SDK quickstart

The GHAN SDK is one dependency. It generates its own ed25519 keypair on first run, renders templated partner cards only when your code asks it to, signs every event locally, and emits no telemetry at all until your application says consent is true.

Install

npm install @ghan/sdk

Or let an agent wire it:

npx @ghan/sdk init

That writes the wiring and leaves a reviewable diff. It does not commit.

Initialise

import { ghan } from '@ghan/sdk'

await ghan.init({
  appId: process.env.GHAN_APP_ID,

  // No telemetry is emitted while this is false.
  consent: settings.analyticsConsent,

  // Optional controls, all enforced client-side by the SDK.
  enabled: true,
  exclude: ['com.competitor.app'],   // never show these bundle ids
  excludeCategories: ['vpn'],        // never show these categories
  maxPerWeek: 2,                     // your frequency cap
})

init generates an ed25519 keypair on first run, stores it in your application's data directory, and registers only the public key. The private key never leaves the machine.

Render a slot

const result = await ghan.slot('first_success')
// result.rendered -> boolean
// result.chainId  -> string | null

Valid slot names: install_complete, first_success, update, idle, uninstall_intent. See lifecycle slots for what each one means and when it converts.

Nothing renders if your cap is reached, your exclusions matched everything, consent is false, the kill switch is on, or GHAN has nothing relevant. rendered is simply false; you never have to handle an error to stay safe.

Receiving an install

No code required. On first run the SDK checks for a GHAN token via the ghan:// protocol handler and via a handoff file, signs an install_claim with its own key if it finds one, and sends the device signals alongside it.

Register the protocol handler in your packaging config:

{
  "protocols": [{ "name": "GHAN", "schemes": ["ghan"] }]
}

If the handler is not registered, the handoff file still works; attribution rates are simply lower.

Usage telemetry

ghan.session.start()
ghan.session.end()
ghan.count('export_completed')   // a coarse counter name, never content

These are what the 48-hour clearing check reads. An app that reports nothing cannot clear chains, because GHAN will not take an app's word that an install survived.

ghan.setConsent(true)

Telemetry buffered before consent is discarded, not flushed.

The kill switch

The server can disable all placements for your app instantly. The SDK honours it on the next placement fetch — no release required, no user action required. It is there for the day you need to answer a reviewer in minutes rather than in a release cycle.

Canary probes

The SDK answers signed liveness probes from the server with its own signature. This is how GHAN can prove a slot is genuinely live in a shipped build. You do not need to do anything; not answering them is what gets an app suspended. See fraud prevention.

What the SDK will never do

  • Render remote HTML, JavaScript or a partner-supplied image outside the icon
  • Install anything
  • Show a card your code did not request
  • Emit telemetry before consent
  • Read anything outside its own state

Those are properties of the implementation, listed on the security page in the form you can hand to a reviewer.

Questions people ask about this

Which frameworks are supported?

Version 1 targets Electron, Tauri and Node desktop applications, which is the bulk of indie desktop software. Native Swift and C# SDKs are on the roadmap but are not shipping yet, and this page does not pretend otherwise.

What does npx @ghan/sdk init do?

It writes the SDK wiring into your project and leaves a reviewable diff. It does not commit, does not push and does not modify anything outside the files it lists. It exists so a coding agent can do the integration and hand a human a pull request rather than a set of instructions.

How does the receiving side claim an install?

On first run the SDK looks for a GHAN token in two places - a ghan:// protocol handler invocation and a handoff file in a known location. If it finds one it signs an install_claim with its own key and sends it with the device signals. If it finds nothing, nothing is claimed and nothing is billed to anyone.

How do I turn everything off?

Three ways, in increasing order of finality. Set enabled to false in init. Use the server kill switch, which disables every placement for your app network-wide on the next fetch. Or stop calling ghan.slot and ship the next release, since the SDK does nothing on its own.

Machine-readable versions of this page: markdown · llms.txt · llms-full.txt · OpenAPI · AI catalog