Reference

Integration proofs

If you run a service whose users should be able to earn Goon Power for something they did there, you become an issuer: you sign a small statement, and the protocol verifies it.

Trust model#

An issuer is an ed25519 public key on an allowlist, with a list of activity types it may attest to, a maximum score per proof and a maximum per wallet per epoch. The protocol verifies who signed and that the statement is fresh, unique and within limits. It cannot verify that the activity took place. That is the issuer's word.

Proof fields#

FieldTypeRule
proof_idstring1–64 chars of A-Z a-z 0-9 _ . : -. Unique per issuer, forever.
issuer_idstringYour allowlisted identifier.
walletbase58The Solana address being credited.
activity_typestringMust be on your allowlist entry.
epochintegerPeriod index: floor(unixTime / epochSeconds). The current period or the one before, if not yet settled.
scoreintegerWhole Goon Power, 1 up to your per-proof limit.
expires_atunix secondsIn the future, and no more than 7 days away.
noncestringUnique per issuer, forever.
signaturebase58ed25519 signature over the canonical message.

Canonical message#

The bytes you sign are these eleven lines, joined by a single newline, UTF-8, no trailing newline.

Signed message
PoG-Integration-Proof-v1
chain:solana:mainnet
program:<program id>
issuer:<issuer_id>
proof:<proof_id>
wallet:<wallet>
activity:<activity_type>
epoch:<epoch>
score:<score>
expires:<expires_at>
nonce:<nonce>
  • The first line is a domain separator. A signature made for any other purpose cannot be reinterpreted as a proof.
  • The chain and program lines bind the proof to one deployment. A devnet proof is invalid on mainnet.
  • Use solana:devnet or solana:localnet on those clusters.
Clustermainnet
Program6KZLGdp5…FczoUr13
Global state (PDA, seed "state")BFM88ZRx…q43wxyjT
Vault authority (PDA, seed "vault")B3Zz1LyU…t7F6bjfW
GOON mint bound to this deploymentnone yet
Goon Vault token accountcreated when the mint is bound
APIhttps://api-production-bffee4f7.up.railway.app/v1
Current period index…

Signing#

issuer.ts
import { ed25519 } from "@noble/curves/ed25519";
import bs58 from "bs58";

const proof = {
  proof_id: "evt-2031-000418",
  issuer_id: "harbor-events",
  wallet: "7xKX...",
  activity_type: "event_attendance",
  epoch: Math.floor(Date.now() / 1000 / 86400),
  score: 5,
  expires_at: Math.floor(Date.now() / 1000) + 3600,
  nonce: crypto.randomUUID(),
};

const message = [
  "PoG-Integration-Proof-v1",
  "chain:solana:mainnet",
  `program:${PROGRAM_ID}`,
  `issuer:${proof.issuer_id}`,
  `proof:${proof.proof_id}`,
  `wallet:${proof.wallet}`,
  `activity:${proof.activity_type}`,
  `epoch:${proof.epoch}`,
  `score:${proof.score}`,
  `expires:${proof.expires_at}`,
  `nonce:${proof.nonce}`,
].join("\n");

const signature = bs58.encode(ed25519.sign(new TextEncoder().encode(message), SECRET_SEED));

await fetch(`${API}/v1/proofs`, {
  method: "POST",
  headers: { "content-type": "application/json" },
  body: JSON.stringify({ ...proof, signature }),
});

The repository ships the same logic as signIntegrationProof in @goon/shared, and as a command: pnpm --filter @goon/api issuer:sign.

Validation order#

  1. Shape: every field present and well-formed.
  2. Issuer exists and is active.
  3. Signature verifies against the issuer's key for this chain and program.
  4. Not expired; expiry within seven days.
  5. Activity type is on the issuer's list.
  6. Score is within the per-proof limit.
  7. Epoch is the current or previous period and has not settled.
  8. Neither proof_id nor nonce has been seen from this issuer.
  9. The wallet has room under the issuer's per-epoch cap. Credit is the smaller of the score and the remaining room.
CodeStatusMeaning
unknown_issuer / issuer_suspended403Not on the allowlist, or suspended.
bad_signature403The signature does not verify for this chain, program and issuer.
expired / expiry_too_far400Outside the accepted validity window.
activity_not_allowed403Activity type not approved for this issuer.
score_too_high400Above the per-proof limit.
wrong_epoch400Not the current or previous period.
epoch_settled409That epoch has been settled; its allocations are final.
replay409The proof ID or nonce was already used.
issuer_daily_cap409The wallet has reached this issuer's cap for the epoch.

Revocation#

Until the proof's epoch settles, the issuer can withdraw it. The credited Goon Power is voided and the wallet's score is recomputed. After settlement the allocation is part of an on-chain root and revocation is refused.

Revocation message, signed the same way
PoG-Integration-Revoke-v1
chain:solana:mainnet
program:<program id>
issuer:<issuer_id>
proof:<proof_id>

POST /v1/proofs/revoke with { issuer_id, proof_id, signature }. Protocol admins can also revoke a proof or suspend an issuer.

Becoming an issuer#

  1. Generate a keypair (pnpm --filter @goon/api issuer:keygen) and keep the seed in your own secret store.
  2. Send the public key, the activity types you want to attest to, and expected volumes.
  3. The protocol admins add the allowlist entry with your limits. It appears at GET /v1/issuers.
  4. Test on devnet, then go live.