P
peck/docsv0.1 · preview

Getting started — building on peck.to overlay

This guide walks you through standing up a new application that reads from and writes to peck.to's overlay. It's aimed at developers who want to build things — a photo app, a podcast reader, a forum, a dashboard — on top of BSV's Bitcoin Schema social graph, using peck.to as infrastructure.

Target time: 30 minutes from zero to first fetched post.

What you'll build

By the end of this guide, you'll have: 1. A BRC-100 identity for your app 2. An open payment channel with overlay.peck.to 3. A working fetch-loop that reads posts filtered by media type 4. The ability to post content on the user's behalf (optional)

Everything is the same BSV chain. peck.to does not own the data — peck.to provides structured, metered access to what's already on chain.

Prerequisites

  • Node.js 20+ (TypeScript examples) or Python 3.11+ (Python examples)
  • A small amount of BSV for testing (~10,000 sats ≈ $0.003)
  • @bsv/sdk and bitcoin-agent-wallet for TS, or equivalent Python BSV library

Step 1 — Create an identity

Your app needs a BRC-100 identity. This is a Bitcoin key-pair that signs payment-channel messages, optionally posts content, and uniquely identifies your app to the overlay.

TypeScript

import { BitcoinAgentWallet, storeIdentityKey } from 'bitcoin-agent-wallet'
import { PrivateKey } from '@bsv/sdk'

// First-time setup: generate and store the key
const key = PrivateKey.fromRandom()
await storeIdentityKey({
  account: 'my-photo-app',
  privateKeyHex: key.toHex()
})

// Subsequent runs: load from OS keychain
const wallet = new BitcoinAgentWallet({
  privateKeyHex: key.toHex(),
  network: 'main',
  appName: 'my.photoapp',
})
await wallet.init()

console.log('Identity ready:', wallet.getIdentityKey())

Python

from bsv import PrivateKey
import keyring

# First-time: generate and store
key = PrivateKey()
keyring.set_password('my-photo-app', 'identity', key.hex())

# Subsequent: load
stored = keyring.get_password('my-photo-app', 'identity')
key = PrivateKey.from_hex(stored)

print(f'Identity ready: {key.public_key().hex()}')

Your identity lives in the OS keychain. Don't commit it to git.

Step 2 — Open a payment channel

To make any paid fetch, you need a channel with overlay. Deposit once, drain per-fetch, close when done.

TypeScript

// Request channel-open terms from overlay
const openReq = await fetch('https://overlay.peck.to/v1/channel/open', {
  method: 'POST',
  headers: { 'content-type': 'application/json' },
  body: JSON.stringify({
    client_pubkey: wallet.getIdentityKey(),
    lock_amount_sats: 10000,
    expiry_blocks: 144,
  }),
}).then(r => r.json())

// openReq contains: { channel_id, open_script, server_pubkey, expiry_height }

// Build and broadcast the funding TX
const fundingTx = await wallet.createFundingTransaction({
  outputs: [{
    satoshis: 10000,
    script: openReq.open_script,
  }],
})

await wallet.broadcast(fundingTx)

// Poll for active status
let status
do {
  await new Promise(r => setTimeout(r, 5000))
  status = await fetch(
    `https://overlay.peck.to/v1/channel/status?channel_id=${openReq.channel_id}`
  ).then(r => r.json())
} while (status.status !== 'active')

console.log('Channel active:', openReq.channel_id)

One wallet-prompt. ~10 seconds until active (one confirmation). You now have 10,000 sats of fetch-credit.

Step 3 — Make a paid fetch

Every request that hits a paid endpoint needs an X-Peck-Receipt header carrying a signed drain-message.

TypeScript — minimal fetcher

let lastNonce = 0
let lastAmountSpent = 0
const channelId = openReq.channel_id

async function paidFetch(endpoint: string, params: object) {
  // Compute expected fee (match fetch-fees.md — in production,
  // prefer a prices-lookup helper)
  const fee = FEES[endpoint] ?? 10
  const nextNonce = lastNonce + 1
  const nextAmountSpent = lastAmountSpent + fee

  // Build and sign drain-message
  const drainMessage = {
    channel_id: channelId,
    nonce: nextNonce,
    amount_spent_new: nextAmountSpent,
  }
  const sig = wallet.sign(canonicalise(drainMessage))

  const receipt = JSON.stringify({
    ...drainMessage,
    client_sig: sig,
  })

  // Make the call
  const url = `https://overlay.peck.to/v1/${endpoint}?` + 
    new URLSearchParams(params as any)
  const resp = await fetch(url, {
    headers: { 'X-Peck-Receipt': receipt },
  })

  if (resp.status === 402) {
    const detail = await resp.json()
    throw new Error(`Payment required: ${detail.reason}`)
  }

  // Update local state from server's ack
  lastNonce = nextNonce
  lastAmountSpent = nextAmountSpent
  return resp.json()
}

// Use it
const feed = await paidFetch('peck_posts_by_media', {
  media_type: 'image',
  limit: 20,
})
console.log('Got', feed.posts.length, 'image posts')

In production use an official client library that handles nonce management, server-ack verification, and retries. The code above shows the wire protocol so you understand what's happening.

Step 4 — Read free endpoints

Free endpoints need no receipt. Skip the X-Peck-Receipt header:

// Always free
const chainTip = await fetch(
  'https://overlay.peck.to/v1/peck_chain_tip'
).then(r => r.json())

const meta = await fetch(
  `https://overlay.peck.to/v1/peck_post_meta?txid=${someTxid}`
).then(r => r.json())

// meta.teaser, meta.author, meta.engagement, etc.

Use these for: - OG-card rendering on link previews - Home-page landing (free feed samples) - Discovery signal aggregation

Step 5 — Post on behalf of the user (optional)

Writes are separate from fetch-fees. They cost a mining fee, paid directly to miners, not to peck.to.

// Construct a post via @peck/bitcoin-schema helpers
import { buildPostTx } from '@peck/bitcoin-schema'

const tx = await buildPostTx({
  wallet,
  content: 'Hello from my photo app!',
  app: 'my.photoapp',
  media: [{ path: './photo.jpg', contentType: 'image/jpeg' }],
})

await wallet.broadcast(tx)
console.log('Posted:', tx.id)

The post appears in overlay within seconds (indexer pickup). Anyone using peck.to or another compatible client sees it.

Step 6 — Close the channel when done

When your app session ends (or user logs out):

const closeResp = await fetch('https://overlay.peck.to/v1/channel/close', {
  method: 'POST',
  headers: { 'content-type': 'application/json' },
  body: JSON.stringify({
    channel_id: channelId,
    client_sig: wallet.sign(`close:${channelId}:${lastAmountSpent}`),
  }),
}).then(r => r.json())

// closeResp.client_refund_sats flows back to wallet on next block

If the user doesn't explicitly close (normal case for mobile apps), the channel sits idle. It costs nothing. Open it again next session and the same deposit continues.

Common integration patterns

Instagram-style photo feed

Filter to image posts, paginate, show in custom UI:

async function loadImageFeed(offset = 0) {
  return paidFetch('peck_posts_by_media', {
    media_type: 'image',
    limit: 20,
    offset,
  })
}

// Initial load (free — offset=0, limit=20, but media filter specific
// so still paid, 20 sats)
const page1 = await loadImageFeed(0)

// Scroll (paid, 20 sats per page)
const page2 = await loadImageFeed(20)

Reader app for long-form posts

Filter to posts with substantial body, show them in article view:

const longReads = await paidFetch('peck_feed_filtered', {
  media_type: 'text_only',
  min_engagement: 5,
  since_ts: Date.now()/1000 - 86400*7, // last week
  limit: 20,
})

Your service receives a peck.to URL and returns preview metadata. Uses only free endpoints — no channel needed.

async function unfurl(peckUrl: string) {
  const txid = extractTxidFromUrl(peckUrl)
  const meta = await fetch(
    `https://overlay.peck.to/v1/peck_post_meta?txid=${txid}`
  ).then(r => r.json())

  return {
    title: `${meta.author.display_name}`,
    description: meta.teaser,
    image: meta.thumbnail_url,
    url: peckUrl,
  }
}

What you don't need to build

  • Indexer / chain sync (peck.to overlay handles it)
  • Media storage (peck.to serves via peck_media_*)
  • Search (peck_search endpoint)
  • Identity registry (peck.to paymail + BRC-100 registry)
  • Follow-graph computation (peck_follows / peck_friends)

You build your app's UX. peck.to is the data-and-delivery layer.

Economics at a glance

A typical session profile for a medium-usage app:

Action Cost
Open channel (once) TX mining fee (~30 sats) + 10,000 sat deposit
Load first feed-page 20 sats
Scroll through 20 more pages 400 sats
Fetch 10 full posts 100 sats
View 5 medium images 500 sats
Close channel TX mining fee (~30 sats)
Total 1,090 sats ≈ $0.0003

Deposit returns at close minus what was spent. A user who only reads can session for months on a 10,000 sat deposit.

Troubleshooting

"Payment required" when you think you have balance: Check GET /v1/channel/status?channel_id=… — nonce drift, expired channel, or misaligned amount_spent. Reset local state from server response.

"Bad signature" errors: Verify your canonicalisation matches server expectations. Keys must be sorted, no trailing whitespace, canonical JSON.

Posts don't appear after writing: Indexer pickup is typically <5 seconds but can lag under high load. Check peck_chain_tip — if indexer is caught up to the block containing your TX, the post should be queryable.

Media serves broken: Ensure you're hitting peck_media_small/medium/large appropriate to file size. Over-size requests return 402 prompting a larger channel.

Next steps

  • Review fetch-fees.md for the full endpoint fee table
  • Review payment-channel.md for channel lifecycle details
  • Review public-access.md if you want to fund pools for viral sharing
  • Join the peck.to dev community (invite link forthcoming)

The overlay is meant to be built on. If you run into friction that this guide or the specs don't address, open an issue — those friction points tell us where the docs need to grow.