Skip to content
skein
Tutorial · about 30 minutes

Build a listing gate
with Skein.

From a free quote to a verified, paid verdict, then the same thing for an AI agent over MCP. By the end you will have a gate that decides list, block, review or retry for any EVM token, and can prove why.

01 · OverviewWhat you'll build

Every exchange, launchpad and wallet that lists tokens faces the same question: is this one safe to put in front of users? Answering it well means buying a contract audit, a holder scan, a liquidity check, sanctions screening, wash-trading and bot detection, then deciding what to do when they disagree. Skein does that in one call. It buys each check from independent miners on the Telegraph network, fuses them under a published policy, and returns one verdict: APPROVE, REJECT, ESCALATE or ABSTAIN. Every input carries a receipt, and the whole decision carries a hash anyone can recompute.

In this tutorial you will:

  1. 01

    Get a free quote with curl

    See what a verdict buys and costs, without an account.
  2. 02

    Buy, stream and verify a verdict from TypeScript

    With @skeinlabs/sdk, spend caps, typed errors and idempotency.
  3. 03

    Pay per call through Telegraph

    No account at all: each verdict settles from your own escrow.
  4. 04

    Hand the same tools to an AI agent

    With @skeinlabs/mcp in Claude Code, Claude Desktop or Cursor.
  5. 05

    Ship a listing gate

    A script that decides list, block, review or retry, and fails CI on a block.

What you need

  • Node.js 20 or later. The SDK also runs on Bun, Deno and edge runtimes: anything with fetch.
  • For paid verdicts, one of: a partner key (free during the Telegraph hackathon; request one), or a funded Telegraph burner wallet, set up in section 09.
  • Quotes, verdict pages, event streams and verification are free and need neither.

02 · ConceptsHow a verdict works

A verdict is made in four steps, and each one leaves something you can check afterwards.

  1. 01

    Plan

    The policy (for example dex-list@0.1.0) names the checks it needs. Skein maps each to a Telegraph intent and prices it from the live miner catalogue.
  2. 02

    Buy

    Each check is one paid Telegraph ask to an independent miner. Each answer comes back with a receipt hash and a signal hash.
  3. 03

    Fuse

    The engine turns each raw answer into a finding, applies the policy's blockers, weights and conflict rules, and decides. Missing or failed required checks lead to ABSTAIN rather than a guess.
  4. 04

    Seal

    The decision, the policy version, the engine version and a Merkle root over every receipt (the bill of materials) are hashed into the verdict hash.
VerdictMeaningWhat your code should do
APPROVEEvery required check answered and nothing crossed a blocker.Proceed (list, pay out, trade).
REJECTA blocker fired: in dex-list, a failed sanctions screen or a contract audit failing at high severity.Stop, and show the reasons.
ESCALATEEvidence is mixed or sources conflict.Send it to a human reviewer.
ABSTAINNot enough evidence arrived to decide. Charged at upstream cost only.Retry later; never treat it as approval.

Three numbers qualify the verdict. riskBps is how risky the token looks, from 0 to 10,000 basis points. confidenceBps is how sure the engine is, given what arrived. coverage is the share of the policy's checks that answered. A low-risk verdict with 40% coverage is weaker than it looks, and the policy accounts for that.

03 · First callYour first call, with curl

Start with a quote. It is free and needs no key: it tells you which checks a verdict would buy for this token, from which Telegraph intents, at what upstream price, and what Skein charges. Here it is for WETH on Base:

shell · free quote
curl "https://api.skeinlabs.site/v1/quote?chain=8453&token=0x4200000000000000000000000000000000000006"
response · 4 Nov 2026
{
  "subject": { "chainId": 8453, "token": "0x4200000000000000000000000000000000000006" },
  "policy": "dex-list@0.1.0",
  "priceUsdc": 0.25,
  "pricing": "standard; ABSTAIN is charged at upstream cost only",
  "ok": true,
  "upstreamUsdc": 0.06,
  "checks": [
    { "check": "contract_audit",  "intent": "SMART_CONTRACT_AUDIT_GENERAL",      "quoteUsdc": 0.01 },
    { "check": "holder_count",    "intent": "TOKEN_HOLDER_COUNT_GENERAL",        "quoteUsdc": 0.01 },
    { "check": "liquidity_depth", "intent": "LIQUIDITY_DEPTH_VERIFY_GENERAL",    "quoteUsdc": 0.01 },
    { "check": "sanctions",       "intent": "SANCTIONS_SCREENING_MATCH_GENERAL", "quoteUsdc": 0.01 },
    { "check": "social_bots",     "intent": "SOCIAL_BOT_DETECTION_GENERAL",      "quoteUsdc": 0.01 },
    { "check": "wash_trading",    "intent": "WASH_TRADING_DETECTION_GENERAL",    "quoteUsdc": 0.01 }
  ]
}

When a verdict would be refused (a required intent has no live miner, or the live floors are above your budget), the quote says so with "ok": false and a refusal code, before any money moves. A paid verdict over plain HTTP looks like this. You'll rarely write it by hand, because the SDK does it for you:

shell · paid verdict (partner key)
curl -X POST https://api.skeinlabs.site/v1/verdicts \
  -H "Authorization: Bearer $SKEIN_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "subject": { "chainId": 8453, "token": "0x4200000000000000000000000000000000000006" },
    "policy": "dex-list@0.1.0",
    "mode": "async"
  }'
# → 202 { "id": "vrd_…", "status": "running" }

curl https://api.skeinlabs.site/v1/verdicts/vrd_…          # the verdict, once sealed
curl -X POST https://api.skeinlabs.site/v1/verify \
  -H "Content-Type: application/json" -d '{ "id": "vrd_…" }'

The Idempotency-Key is required. Send the same key with the same body again and you get the original verdict back, marked replayed, and you are not charged twice. Send it with a different body and you get 409 IDEMPOTENCY_CONFLICT.

04 · InstallInstall the SDK

@skeinlabs/sdk is a small TypeScript client with no runtime dependencies. It wraps every endpoint and adds typed errors, retries that keep the idempotency key, and a spend cap checked before any payment.

shell
mkdir listing-gate && cd listing-gate
npm init -y && npm pkg set type=module
npm install @skeinlabs/sdk
npm install -D tsx typescript @types/node
typescript · quote.ts
// quote.ts: run with  npx tsx quote.ts
import { Skein } from "@skeinlabs/sdk";

const skein = new Skein({ apiUrl: "https://api.skeinlabs.site" });

const q = await skein.quote({
  subject: { chainId: 8453, token: "0x4200000000000000000000000000000000000006" }, // WETH on Base
  policy: "dex-list@0.1.0",
});

if (q.ok) console.log(`${q.priceUsdc} USDC buys ${q.checks?.length} checks`);
else console.log("would be refused:", q.refusal?.code, q.refusal?.detail);

05 · VerdictBuy a verdict

Add a payer and call verdict(). With maxUsdcPerCall set, the SDK fetches a quote first and throws a SpendCapError if the price is above your cap, so nothing is paid. Then it submits the verdict, follows its event stream until it is sealed, and returns the full object.

typescript · verdict.ts
// verdict.ts: SKEIN_API_KEY=sk_part_… npx tsx verdict.ts
import { Skein, SkeinError, SpendCapError } from "@skeinlabs/sdk";

const skein = new Skein({
  apiUrl: "https://api.skeinlabs.site",
  payer: { kind: "apiKey", key: process.env.SKEIN_API_KEY! },
  maxUsdcPerCall: 0.3, // quoted first; refused before paying above this
});

try {
  const v = await skein.verdict({
    subject: { chainId: 8453, token: "0x4200000000000000000000000000000000000006" },
    policy: "dex-list@0.1.0",
  });

  console.log(v.verdict, v.riskBps, "bps");
  console.log(v.summary);
  for (const r of v.reasons) console.log("-", r.detail);
  console.log("open risks:", v.openRisks);
  console.log("cost:", v.cost.priceUsdc, "USDC, upstream", v.cost.upstreamUsdc);
  console.log("page:", "https://skeinlabs.site/v/" + v.id);
} catch (e) {
  if (e instanceof SpendCapError) console.log("too expensive:", e.priceUsdc, "USDC");
  else if (e instanceof SkeinError) console.log(e.code, e.message);
  else throw e;
}

What comes back

typescript · the verdict object
interface VerdictObject {
  id: string;                      // "vrd_…", public: anyone can GET, verify and audit it
  verdict: "APPROVE" | "REJECT" | "ESCALATE" | "ABSTAIN";
  riskBps: number;                 // 0–10000: how risky, in basis points
  confidenceBps: number;           // 0–10000: how sure, given the evidence that arrived
  coverage: number;                // 0–1: share of the policy's checks that answered
  summary: string;                 // one line a human can read
  reasons: { code: string; check?: string; detail: string }[];
  openRisks: string[];             // what was not settled (a check that failed, a conflict)
  conflicts: string[];             // where two sources disagreed
  findings: Finding[];             // one per check: status, severity, value, source, receipt
  cost: { upstreamUsdc: number; priceUsdc: number; currency: "USDC" };
  bom: { root: string; items: number };   // Merkle root over every paid input's receipt
  policy: { id: string; version: string };
  engineVersion: string;
  verdictHash: string;             // hash over the whole decision: recompute it with verify()
  network: string;                 // "testnet" today; "simulated" means canned answers
  cached?: boolean;                // served from cache: no new purchases
  replayed?: boolean;              // same Idempotency-Key as before: not charged again
  payment: { rail: "apiKey" } | { rail: "telegraph"; minerId; amountUsdc; receiptHash; signalHash };
}

Here is a real one. On 4 November, before Skein's own upstream wallet was funded, its purchases failed, and it did the right thing: it abstained, named every required check that was missing, and charged nothing.

response · a real ABSTAIN
{
  "id": "vrd_mw1xqc4d3k4t22320e31",
  "verdict": "ABSTAIN",
  "summary": "Abstained: required check contract_audit is ERROR (+3 more).",
  "reasons": [
    { "code": "MISSING_REQUIRED_CHECK", "check": "contract_audit", "detail": "required check contract_audit is ERROR" },
    …
  ],
  "cost": { "upstreamUsdc": 0, "priceUsdc": 0, "pricingRule": "abstain_upstream_only", "currency": "USDC" },
  "bom": { "root": "0x0000…0000", "items": 0 },
  "verdictHash": "0x8f1ac48e5260f1c43b36942a1df39af128c5f747f7ac85564c253095437ec923",
  "network": "testnet",
  "durationMs": 2379
}

06 · StreamWatch it being made

A verdict takes seconds, because each check is a separate paid ask. Pass onEvent to see each step as it happens. The same events drive the thread graph on every verdict page.

EventWhenData
planBefore any purchaseThe checks and intents to buy, and their quotes
purchaseEach paid ask settlesCheck, miner, amount in micro-USDC, receipt hash, or an error
findingEach answer is parsedCheck, status (PASS, WARN, FAIL, UNKNOWN, UNAVAILABLE, ERROR), severity, value, notes
decisionThe engine decidesVerdict, risk, and the verdict hash
doneSealed and storedThe verdict id
refusedRefused before spendingA refusal code and detail
typescript · live events
const v = await skein.verdict({
  subject,
  onEvent: (e) => {
    switch (e.type) {
      case "plan":     console.log("buying", e.data.purchases?.map((p) => p.check)); break;
      case "purchase": console.log("paid", e.data.check, e.data.amountUusdc / 1e6, "USDC", e.data.receiptHash); break;
      case "finding":  console.log(e.data.check, e.data.status, e.data.notes.join("; ")); break;
      case "decision": console.log("decided", e.data.decision.verdict); break;
    }
  },
});

For queue workers and serverless functions, split the call in two. submit() returns as soon as the verdict has an id; wait() collects it later, following the stream and falling back to polling if the connection drops.

typescript · submit now, wait later
// Start now, collect later: useful in a queue worker or a serverless function.
const started = await skein.submit({ subject, idempotencyKey: jobId });
if ("status" in started && started.status === "running") {
  await queue.later({ verdictId: started.id });       // returns in milliseconds
}

// …in another invocation:
const v = await skein.wait(verdictId, { timeoutMs: 60_000 });

// Or follow any verdict's events yourself, replayed from the start:
for await (const e of skein.stream(verdictId)) console.log(e.type);

07 · VerifyVerify it

A verdict should not have to be trusted. verify() asks Skein to recompute it from the raw answers it paid for: re-run every adapter, re-apply the published policy, rebuild the receipts' Merkle root, and compare the hashes. It is free, and it works on anyone's verdict, not only yours.

typescript · verify and audit
const report = await skein.verify(v.id);
// {
//   ok: true,
//   checks: {
//     adaptersRerun: true,     // each raw paid answer re-parsed into the same finding
//     policyRerun: true,       // the policy file re-applied to those findings
//     policyFile: true,        // the policy text matches its published sha256
//     engineVersion: true,     // the engine that sealed it is the one that re-ran it
//     decisionMatches: true,   // same verdict, risk and confidence
//     bomRootRebuilt: true,    // the receipts' Merkle root rebuilt from the lines
//     receiptsMatch: true      // every line's receipt is the one Telegraph returned
//   },
//   verdictHash: { stored: "0x…", recomputed: "0x…" },
//   problems: []
// }
if (!report.ok) throw new Error("verdict did not verify: " + report.problems.join("; "));

const bom = await skein.bom(v.id);           // the bill of materials
for (const line of bom.lines) {
  console.log(line.check, line.minerName, line.amountUsdc, "USDC", line.receiptHash);
}

Every verdict response also carries X-Skein-Verdict-Hash and X-Skein-Bom-Root headers, and the verify page does the same check in a browser. Every receipt hash in the bill of materials is the one the Telegraph network returned for that purchase, so an auditor can trace each input to a settled payment.

08 · ProductionProduction patterns

Cap spend in three places

maxUsdcPerCall refuses one expensive verdict. Your own running total, as in the listing gate below, caps a batch. And each partner key carries a daily cap on Skein's side, so a runaway loop stops there too. The Telegraph client adds a fourth: it will not sign a payment challenge above its own maxUusdcPerCall.

Make retries safe

Derive the idempotency key from what you are deciding, not from the attempt. For example, one key per token per day. A crash, a retry or a re-run then returns the same verdict without paying again. The SDK only retries a POST when it carries a key, and it never sleeps longer than maxRetryWaitMs in total: a Retry-After until midnight throws at once instead of hanging your worker.

typescript · every option
const skein = new Skein({
  apiUrl: "https://api.skeinlabs.site",
  payer: { kind: "apiKey", key },
  maxUsdcPerCall: 0.3,      // checked against a fresh quote before any payment
  retries: 2,               // network errors, 429 and retryable 5xx (POSTs only with an idempotency key)
  maxRetryWaitMs: 10_000,   // never sleep longer than this in total; a longer Retry-After throws
  requestTimeoutMs: 15_000, // per HTTP request
  timeoutMs: 120_000,       // how long verdict() waits for the seal
});

Handle every outcome

All errors are SkeinError, with the API's code. A SpendCapError means nothing was paid. A SkeinError with a payment field means money moved (or may have) without a verdict, which can happen on the Telegraph rail, so count it against your caps.

typescript · error handling
import { SkeinError, SpendCapError } from "@skeinlabs/sdk";

try {
  return await skein.verdict({ subject, idempotencyKey });
} catch (e) {
  if (e instanceof SpendCapError) return hold("price above cap", e.priceUsdc);   // nothing paid
  if (!(e instanceof SkeinError)) throw e;
  if (e.payment) ledger.add(e.payment.amountUsdc);  // paid, but no verdict: count it
  switch (e.code) {
    case "BUDGET_TOO_LOW":                          // live floors above your budget
    case "UPSTREAM_UNAVAILABLE":                    // a required check has no live miner
    case "DAILY_CAP_REACHED":                       // Skein's own cap for the day
      return retryLater(e.code);
    case "RATE_LIMITED":                            // the SDK already waited and retried
    case "TIMEOUT":
      return retryLater(e.code, { sameKey: idempotencyKey });
    default:
      throw e;                                      // INVALID_SUBJECT, UNAUTHORIZED, …
  }
}
CodeHTTPMeaning
INVALID_SUBJECT400Unknown chain, or a malformed token address
UNKNOWN_POLICY400No published policy by that id and version
PAYMENT_REQUIRED402No partner key sent: send one, or pay through Telegraph
UNAUTHORIZED401Unknown or revoked partner key
IDEMPOTENCY_CONFLICT409The key was used before with a different body
BUDGET_TOO_LOW422Live floors exceed your budget; refused before any spend
RATE_LIMITED429Slow down; Retry-After says how long
UPSTREAM_UNAVAILABLE503A required intent has no live miner right now
DAILY_CAP_REACHED503Skein's own spend cap for the day is used up
TIMEOUT504Upstream answers did not arrive in time; call wait(id) again
SPEND_CAPSDKThe quote is above your maxUsdcPerCall; nothing was paid

Cache what the network caches

A verdict for the same token and policy within ten minutes is served from cache, marked cached, and buys nothing upstream. That is fine for a listing review, and it is why the listing gate checks cached and replayed before adding to its spend.

09 · TelegraphPay per call, with no account

Skein is itself a miner on Telegraph, so any Telegraph app can buy a verdict with one paid ask, settled from its own escrow. There is no key to request: you need a wallet with test USDC in the Telegraph escrow on Sepolia.

Fund a burner

shell · Sepolia, with Foundry's cast
# 1. A fresh burner wallet (Foundry's cast; or any wallet you only use for this)
cast wallet new

export KEY=0x…            # the burner's private key: never a wallet that holds real funds
export ME=0x…             # its address
export RPC=https://ethereum-sepolia-rpc.publicnode.com
export USDC=0xcc194e1a843B658526D0D5C431f670028582Ae17     # Telegraph test USDC
export DIAMOND=0x3624EB012B5E998BB57B6bE6C9Cfc9c4bc5eb99b  # Telegraph escrow

# 2. Sepolia ETH for gas: any public Sepolia faucet, sent to $ME

# 3. Mint 10 test USDC (6 decimals): anyone can mint the test token
cast send $USDC "mint(address,uint256)" $ME 10000000 --rpc-url $RPC --private-key $KEY

# 4. Deposit 5 USDC into escrow: paid asks settle from here
cast send $USDC "approve(address,uint256)" $DIAMOND 5000000 --rpc-url $RPC --private-key $KEY
cast send $DIAMOND "depositUSDC(uint256)" 5000000 --rpc-url $RPC --private-key $KEY

# 5. Check the escrow balance (micro-USDC)
cast call $DIAMOND "effectiveBalance(address)(int256)" $ME --rpc-url $RPC

Pay for a verdict

typescript · telegraph.ts
npm install viem   # the Telegraph entry signs with it

// telegraph.ts: SKEIN_PAYER_KEY=0x… npx tsx telegraph.ts
import { Skein } from "@skeinlabs/sdk";
import { TelegraphClient, TESTNET, viemSigner } from "@skeinlabs/sdk/telegraph";

const client = new TelegraphClient({
  network: TESTNET,                                   // Sepolia escrow, Telegraph dev node
  signer: viemSigner(process.env.SKEIN_PAYER_KEY as `0x${string}`),
  maxUusdcPerCall: 300_000,                           // 0.30 USDC: no challenge is signed above this
});

const skein = new Skein({
  apiUrl: "https://api.skeinlabs.site",                     // only for reading the sealed verdict back
  payer: {
    kind: "telegraph",
    client,
    minerId: process.env.SKEIN_MINER_ID,             // pin Skein's miner (see /status)
  },
});

const v = await skein.verdict({ subject: { chainId: 1, token: "0x…" } });
console.log(v.verdict, v.payment);
// { rail: "telegraph", minerId: "…", amountUsdc: …, receiptHash: "0x…", signalHash: "0x…" }

The client reads the network's 402 challenge and checks every field of the message it is asked to sign against the challenge itself. It refuses anything above its cap, signs with EIP-191, and never retries a call it has already paid for. The result's payment carries the receipt hash of your own purchase, and the verdict's bill of materials carries the receipts of every purchase Skein made upstream.

10 · MCPGive it to an agent

@skeinlabs/mcp is a Model Context Protocol server that gives any MCP client three tools: skein_quote (free), skein_verdict (paid) and skein_verify (free). It pays inside, with a per-call cap and a per-session cap, so an agent cannot overspend however it loops. And it returns each verdict as a few lines the model can act on, plus the full object as structured content.

Claude Code

shell
claude mcp add skein --env SKEIN_API_KEY=sk_part_… -- npx -y @skeinlabs/mcp

Claude Desktop, Cursor and others

Add the server to the client's MCP config. That's claude_desktop_config.json for Claude Desktop (Settings → Developer → Edit Config), ~/.cursor/mcp.json or a project's .cursor/mcp.json for Cursor, and the same shape for most other clients. Then restart the client.

json · mcp config
{
  "mcpServers": {
    "skein": {
      "command": "npx",
      "args": ["-y", "@skeinlabs/mcp"],
      "env": {
        "SKEIN_API_KEY": "sk_part_…",
        "SKEIN_MAX_USDC_PER_CALL": "0.30",
        "SKEIN_SESSION_CAP_USDC": "2"
      }
    }
  }
}
VariableDefaultMeaning
SKEIN_API_KEYA partner key
SKEIN_PAYER_KEYInstead of a key: a funded Telegraph burner
SKEIN_MINER_IDPin Skein's miner on Telegraph
SKEIN_MAX_USDC_PER_CALL0.30No verdict is bought above this
SKEIN_SESSION_CAP_USDC2What one server process may spend in total
SKEIN_API_URLapi.skeinlabs.site

What it looks like

a conversation, abridged
You: Before we add 0x4200000000000000000000000000000000000006 on Base to the
     launchpad, check it with Skein. Tell me the cost first.

Agent → skein_quote   { chainId: "base", token: "0x4200…0006" }
      ← dex-list@0.1.0: 0.25 USDC per verdict; buys 6 checks upstream for about 0.06 USDC
Agent → skein_verdict { chainId: "base", token: "0x4200…0006" }
      ← <VERDICT> · risk <n> bps · confidence <n>% · coverage <n>%
        <summary, reasons, open risks, cost>
        Verdict hash 0x…. Verify: https://skeinlabs.site/v/vrd_…
Agent → skein_verify  { id: "vrd_…" }
      ← VERIFIED: vrd_…  ✓ adaptersRerun ✓ policyRerun ✓ decisionMatches …

11 · Build itBuild the listing gate

Now put it together. The gate takes a list of tokens, checks each one within a per-run budget, verifies every verdict, and maps it to a decision. It writes decisions.json and exits with status 1 if anything is blocked, so a CI job or a listing pipeline can stop on it. The whole program is about a hundred lines; read it on GitHub. Here is its core:

typescript · gate.ts (core)
const DECISION = { APPROVE: "list", REJECT: "block", ESCALATE: "review", ABSTAIN: "retry" } as const;

for (const subject of subjects) {
  // 1. Free: what it would cost, or why it would be refused, before any money moves.
  const quote = await skein.quote({ subject, policy });
  if (!quote.ok) { refuse(subject, quote.refusal); continue; }
  if (spent + quote.priceUsdc > runCapUsdc) { refuse(subject, "run cap"); continue; }

  // 2. Buy it. One idempotency key per token per day: re-running today is free.
  const v = await skein.verdict({
    subject, policy, quote,
    idempotencyKey: `listing-gate:${subject.chainId}:${subject.token.toLowerCase()}:${day}`,
  });
  spent += v.cached || v.replayed ? 0 : v.cost.priceUsdc;

  // 3. Don't take it on trust: recompute it.
  const report = await skein.verify(v.id);

  // 4. Act. A verdict that fails verification goes to a human, never to "list".
  decide(subject, report.ok ? DECISION[v.verdict] : "review", v);
}

process.exit(rows.some((r) => r.decision === "block") ? 1 : 0);   // CI fails on a block
shell · run it
git clone -b ccr-4e0a77db-k2xyoy https://github.com/linoxbt/skein.git && cd skein/examples/listing-gate
npm install
SKEIN_API_KEY=sk_part_… npm run gate -- \
  base:0x4200000000000000000000000000000000000006 \
  ethereum:0xdAC17F958D2ee523a2206206994597C13D831ec7

# one block per token: decision, chain, token, verdict and risk, then why and the verdict page
<DECISION> 8453   0x4200…0006  <VERDICT> <risk> bps
           <summary>
           https://skeinlabs.site/v/vrd_…
<DECISION> 1      0xdAC1…1ec7  <VERDICT> <risk> bps
           <summary>
           https://skeinlabs.site/v/vrd_…

2 tokens · spent <total> USDC (cap 2)

Run it on every listing request

Keep the tokens you list in a file, and let a pull request that adds one run the gate. A block fails the check. A review shows up in the uploaded decisions, with a link to each verdict page for whoever approves the PR.

yaml · github actions
# .github/workflows/listing-gate.yml: check every token added to tokens.txt
on:
  pull_request:
    paths: ["tokens.txt"]
jobs:
  gate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 22 }
      - run: npm ci
      - run: npx tsx gate.ts $(cat tokens.txt)
        env:
          SKEIN_API_KEY: ${{ secrets.SKEIN_API_KEY }}
          SKEIN_RUN_CAP_USDC: "5"
      - uses: actions/upload-artifact@v4
        if: always()
        with: { name: decisions, path: decisions.json }

12 · TroubleshootingTroubleshooting

SymptomCause and fix
NO_PAYERThe client has no payer. Quotes and verification work without one; verdicts need a key or a Telegraph client.
PAYMENT_REQUIREDNo Authorization: Bearer header. Check that the key is in your environment.
UNAUTHORIZEDThe key is unknown or revoked. Keys start with sk_part_; request a new one.
Every verdict is ABSTAINThe required checks did not answer: see reasons and openRisks, and the status page for which intents have live miners. You are charged upstream cost only.
UPSTREAM_UNAVAILABLEA required intent has no live miner right now. Retry later, or choose a policy that does not require it.
TIMEOUTUpstream answers were slow. The verdict may still seal: call wait(id) with the id from the error's details.
Telegraph: insufficient balanceDeposit more test USDC into escrow (section 09, step 4) and check effectiveBalance.
Telegraph: challenge refusedThe price was above your client's maxUusdcPerCall, or the challenge failed its checks. Nothing was signed.
MCP tools don't appearRestart the client after editing its config, and check its MCP log: the server prints skein-mcp ready to stderr.

13 · NextWhere next

Packages: @skeinlabs/sdk · @skeinlabs/mcp. Found a mistake in this tutorial? Open an issue.