Point-of-Presence Signing Infrastructure
POPSigner is a distributed signing layer designed to live inline with execution—not behind an API queue.
POPSigner is Point-of-Presence signing infrastructure. It deploys where your systems already run—the same region, the same rack, the same execution path.
This isn't custody. This isn't MPC. This is signing at the point of execution.
┌─────────────────────────────────────────────────────────────┐
│ YOUR INFRASTRUCTURE │
│ │
│ ┌──────────────┐ inline ┌──────────────────────────┐ │
│ │ Execution │ ───────────▶ │ POPSigner POP │ │
│ │ (sequencer, │ │ (same region) │ │
│ │ bot, etc.) │ ◀─────────── │ │ │
│ └──────────────┘ signature └──────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
| Principle | Description |
|---|---|
| Inline Signing | Signing happens on the execution path, not behind a queue |
| Sovereignty by Default | Keys are remote, but you control them. Export anytime. Exit anytime. |
| Neutral Anchor | Recovery data is anchored to neutral data availability. If we disappear, you don't. |
Deploy without infrastructure. Connect and sign.
# Get your API key at https://popsigner.com
go get github.com/Bidon15/popsigner/sdk-gopackage main
import (
"context"
"fmt"
"log"
"os"
popsigner "github.com/Bidon15/popsigner/sdk-go"
"github.com/google/uuid"
)
func main() {
ctx := context.Background()
// Connect to POPSigner
client := popsigner.NewClient(os.Getenv("POPSIGNER_API_KEY"))
// Create a key
namespaceID := uuid.MustParse(os.Getenv("POPSIGNER_NAMESPACE_ID"))
key, err := client.Keys.Create(ctx, popsigner.CreateKeyRequest{
Name: "sequencer-key",
NamespaceID: namespaceID,
Algorithm: "secp256k1",
})
if err != nil {
log.Fatal(err)
}
fmt.Printf("Created key: %s (%s)\n", key.Name, key.Address)
// Sign inline with your execution
result, err := client.Sign.Sign(ctx, key.ID, []byte("transaction data"), false)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Signature: %x\n", result.Signature)
}Run POPSigner on your own infrastructure. Full control. No dependencies.
package main
import (
"context"
"os"
popsigner "github.com/Bidon15/popsigner"
)
func main() {
ctx := context.Background()
kr, _ := popsigner.New(ctx, popsigner.Config{
BaoAddr: "https://your-openbao.internal:8200",
BaoToken: os.Getenv("BAO_TOKEN"),
StorePath: "./keyring-metadata.json",
})
// Create a key
record, _ := kr.NewAccountWithOptions("sequencer", popsigner.KeyOptions{
Exportable: true,
})
// Sign with OpenBao backend
sig, pubKey, _ := kr.Sign("sequencer", []byte("transaction data"), nil)
}See Deployment Guide for Kubernetes setup.
| Local Keyring | Cloud KMS | POPSigner | |
|---|---|---|---|
| Key exposure | On disk | Decrypted in app | Never exposed |
| secp256k1 | ✅ | ❌ | ✅ |
| Placement | Local only | Their region | Your region |
| Self-hostable | ✅ | ❌ | ✅ |
| Managed option | ❌ | ✅ | ✅ |
| Exit guarantee | N/A | ❌ | Always |
POPSigner is designed with exit as a first-class primitive.
- Key Export: Your keys are exportable by default. No ceremony. No approval workflow.
- Recovery Anchor: Recovery data is anchored to neutral data availability infrastructure.
- Force Exit: If POPSigner is unavailable for any reason, you can force recovery. This is not gated.
POPSigner ships with secp256k1. But the plugin architecture is the actual product.
- Plugins are chain-agnostic
- Plugins are free
- Plugins don't require approval
// Built-in secp256k1
sig, pubKey, _ := kr.Sign("my-key", signBytes, signMode)
// Your custom algorithm tomorrowgo get github.com/Bidon15/popsigner/sdk-goimport popsigner "github.com/Bidon15/popsigner/sdk-go"
client := popsigner.NewClient("psk_live_xxxxx")See Go SDK README for full documentation.
[dependencies]
popsigner = "0.1"
tokio = { version = "1", features = ["full"] }use popsigner::Client;
let client = Client::new("psk_live_xxxxx");See Rust SDK README for full documentation.
POPSigner provides a Celestia-compatible keyring:
import (
popsigner "github.com/Bidon15/popsigner/sdk-go"
"github.com/celestiaorg/celestia-node/api/client"
)
func main() {
ctx := context.Background()
// Create a Celestia-compatible keyring backed by POPSigner
kr, _ := popsigner.NewCelestiaKeyring(
os.Getenv("POPSIGNER_API_KEY"),
"your-key-id",
)
// Use with Celestia client
cfg := client.Config{
ReadConfig: client.ReadConfig{
BridgeDAAddr: "http://localhost:26658",
DAAuthToken: os.Getenv("CELESTIA_AUTH_TOKEN"),
},
SubmitConfig: client.SubmitConfig{
DefaultKeyName: kr.KeyName(),
Network: "mocha-4",
},
}
celestiaClient, _ := client.New(ctx, cfg, kr)
// Submit blobs—signing happens inline via POPSigner
fmt.Printf("Connected with address: %s\n", kr.CelestiaAddress())
}Drop-in replacement for Lumina's client:
use popsigner::celestia::Client;
let client = Client::builder()
.rpc_url("ws://localhost:26658")
.grpc_url("http://localhost:9090")
.popsigner("psk_live_xxx", "my-key")
.build()
.await?;
// Same API as Lumina—keys never exposed
client.blob().submit(&[blob], TxConfig::default()).await?;POPSigner supports worker-native architecture for burst workloads:
// Create signing workers
keys, _ := client.Keys.CreateBatch(ctx, popsigner.CreateBatchRequest{
Prefix: "blob-worker",
Count: 4,
NamespaceID: namespaceID,
})
// Sign in parallel—no blocking
results, _ := client.Sign.SignBatch(ctx, popsigner.BatchSignRequest{
Requests: []popsigner.SignRequest{
{KeyID: keys[0].ID, Data: tx1},
{KeyID: keys[1].ID, Data: tx2},
{KeyID: keys[2].ID, Data: tx3},
{KeyID: keys[3].ID, Data: tx4},
},
})POPSigner provides two CLIs for different use cases:
For managing keys via the POPSigner Control Plane API:
# Install
go install github.com/Bidon15/popsigner/popctl@latest
# Configure
popctl config init
# or set environment variables:
export POPSIGNER_API_KEY="psk_xxx"
export POPSIGNER_NAMESPACE_ID="your-namespace-uuid"
# Key management
popctl keys list
popctl keys create my-sequencer --exportable
popctl keys create-batch blob-worker --count 4
popctl keys get <key-id>
popctl keys export <key-id>
popctl keys delete <key-id>
# Sign data
popctl sign <key-id> --data "message"
popctl sign <key-id> --file message.txtFor managing keys directly with your own OpenBao instance:
# Install
go install github.com/Bidon15/popsigner/cmd/popsigner@latest
# Configure
export BAO_ADDR="http://127.0.0.1:8200"
export BAO_TOKEN="your-bao-token"
# Key management
popsigner keys list
popsigner keys add my-validator --exportable
popsigner keys show my-validator
popsigner keys rename old-name new-name
popsigner keys export-pub my-validator
popsigner keys delete my-validator
# Migration
popsigner migrate import --from ~/.celestia-app/keyring-file --key-name my-key
popsigner migrate export --key my-validator --to ./backuppopctl keys import my-validator --private-key <base64-encoded-key>popsigner migrate import \
--from ~/.celestia-app/keyring-file \
--key-name my-validator# Cloud
popctl keys export <key-id>
# Self-hosted
popsigner migrate export --key my-validator --to ./backupSee Migration Guide for all options.
| Document | Description |
|---|---|
| Integration Guide | Celestia client integration |
| Migration Guide | Import/export keys |
| API Reference | REST API endpoints |
| Deployment Guide | Self-hosted Kubernetes setup |
| Architecture | Technical design |
| Plugin Design | OpenBao plugin details |
go get github.com/Bidon15/popsigner/sdk-gogo get github.com/Bidon15/popsigner[dependencies]
popsigner = "0.1"# Cloud CLI
go install github.com/Bidon15/popsigner/popctl@latest
# Self-Hosted CLI
go install github.com/Bidon15/popsigner/cmd/popsigner@latest| Deployment | Requirements |
|---|---|
| Cloud | API key only |
| Self-hosted | OpenBao + secp256k1 plugin, Kubernetes 1.25+ |
POPSigner (formerly BanhBaoRing) reflects a clearer articulation of what the system is: Point-of-Presence signing infrastructure.
The rename signals a shift from playful internal naming to category-defining infrastructure positioning.
git clone https://github.com/Bidon15/popsigner.git
cd popsigner
go mod download
go test ./...Apache License 2.0 - See LICENSE for details.
POPSigner — Signing at the point of execution.
Deploy POPSigner ·
Documentation ·
GitHub