There is no first-party Sigil SDK for Go yet. The wire format is JSON-RPC over HTTPS, and the signing scheme is Ed25519 over a BLAKE3 hash of a tagged transcript — both of which Go’s standard library plus one BLAKE3 dependency cover cleanly. This page shows the canonical pattern that a published sigil-go package will eventually expose.

Install

go mod init github.com/yourorg/sigil-quickstart
go get github.com/zeebo/blake3
The standard library covers HTTP, JSON, and Ed25519 already.

Configuration

Use a small struct rather than init()-time globals so tests can stub it.
package sigil

import "time"

type Network struct {
    ChainID   string
    RPCURL    string
    Timeout   time.Duration
}

var (
    Mainnet = Network{
        ChainID: "sigil-mainnet-1",
        RPCURL:  "https://rpc.sigil.ml/rpc",
        Timeout: 30 * time.Second,
    }
    Testnet = Network{
        ChainID: "sigil-testnet-1",
        RPCURL:  "https://testnet-rpc.sigil.ml/rpc",
        Timeout: 30 * time.Second,
    }
)
Recommended environment variables:
export SIGIL_RPC_URL=https://testnet-rpc.sigil.ml/rpc
export SIGIL_CHAIN_ID=sigil-testnet-1
export SIGIL_SIGNING_KEY_HEX=...   # 64 hex chars (Ed25519 seed)
export SIGIL_SENDER_DID=did:oas:sigil:agent:...

Identity

package sigil

import (
    "crypto/ed25519"
    "crypto/rand"
    "encoding/hex"
    "errors"
    "fmt"

    "github.com/zeebo/blake3"
)

type Identity struct {
    DID        string
    PublicKey  ed25519.PublicKey
    PrivateKey ed25519.PrivateKey
}

func NewAgent() (*Identity, error) {
    pub, priv, err := ed25519.GenerateKey(rand.Reader)
    if err != nil {
        return nil, fmt.Errorf("generate ed25519 key: %w", err)
    }
    addr := blake3.Sum256(pub)
    return &Identity{
        DID:        fmt.Sprintf("did:oas:sigil:agent:%s", hex.EncodeToString(addr[:])),
        PublicKey:  pub,
        PrivateKey: priv,
    }, nil
}

func LoadAgent(seedHex string) (*Identity, error) {
    seed, err := hex.DecodeString(seedHex)
    if err != nil {
        return nil, fmt.Errorf("decode seed: %w", err)
    }
    if len(seed) != ed25519.SeedSize {
        return nil, errors.New("seed must be 32 bytes")
    }
    priv := ed25519.NewKeyFromSeed(seed)
    pub, ok := priv.Public().(ed25519.PublicKey)
    if !ok {
        return nil, errors.New("public key extraction failed")
    }
    addr := blake3.Sum256(pub)
    return &Identity{
        DID:        fmt.Sprintf("did:oas:sigil:agent:%s", hex.EncodeToString(addr[:])),
        PublicKey:  pub,
        PrivateKey: priv,
    }, nil
}
DIDs are derived deterministically from the public key. Persist only the 32-byte seed; the public key derives from it.

Reading chain state

Build a single Call helper, then layer typed methods on top.
package sigil

import (
    "bytes"
    "context"
    "encoding/json"
    "errors"
    "fmt"
    "net/http"
)

type rpcRequest struct {
    JSONRPC string `json:"jsonrpc"`
    ID      int    `json:"id"`
    Method  string `json:"method"`
    Params  any    `json:"params"`
}

type rpcResponse struct {
    Result json.RawMessage `json:"result"`
    Error  *struct {
        Code    int    `json:"code"`
        Message string `json:"message"`
    } `json:"error"`
}

type RPCError struct {
    Code    int
    Message string
}

func (e *RPCError) Error() string {
    return fmt.Sprintf("rpc %d: %s", e.Code, e.Message)
}

func Call[T any](
    ctx context.Context,
    client *http.Client,
    network Network,
    method string,
    params any,
) (T, error) {
    var zero T
    payload, err := json.Marshal(rpcRequest{
        JSONRPC: "2.0", ID: 1, Method: method, Params: params,
    })
    if err != nil {
        return zero, fmt.Errorf("marshal: %w", err)
    }

    req, err := http.NewRequestWithContext(ctx, http.MethodPost, network.RPCURL, bytes.NewReader(payload))
    if err != nil {
        return zero, fmt.Errorf("new request: %w", err)
    }
    req.Header.Set("content-type", "application/json")

    res, err := client.Do(req)
    if err != nil {
        return zero, fmt.Errorf("post: %w", err)
    }
    defer res.Body.Close()

    var envelope rpcResponse
    if err := json.NewDecoder(res.Body).Decode(&envelope); err != nil {
        return zero, fmt.Errorf("decode: %w", err)
    }
    if envelope.Error != nil {
        return zero, &RPCError{Code: envelope.Error.Code, Message: envelope.Error.Message}
    }
    if len(envelope.Result) == 0 {
        return zero, errors.New("missing result")
    }
    var out T
    if err := json.Unmarshal(envelope.Result, &out); err != nil {
        return zero, fmt.Errorf("unmarshal result: %w", err)
    }
    return out, nil
}

Typed queries

type BalanceResult struct {
    DID     string `json:"did"`
    Balance uint64 `json:"balance"`
    Nonce   uint64 `json:"nonce"`
}

func GetBalance(
    ctx context.Context, client *http.Client, network Network, did string,
) (BalanceResult, error) {
    return Call[BalanceResult](ctx, client, network, "sigil_getBalance",
        map[string]any{"did": did})
}

type NodeInfo struct {
    Version         string `json:"version"`
    ChainID         string `json:"chain_id"`
    ProtocolVersion string `json:"protocol_version"`
}

func GetNodeInfo(ctx context.Context, client *http.Client, network Network) (NodeInfo, error) {
    return Call[NodeInfo](ctx, client, network, "sigil_getNodeInfo", map[string]any{})
}

Signing and submitting transactions

The signing recipe is identical to every other SDK:
  1. Build the canonical JSON object that matches sigil_core::Transaction.
  2. Prefix sigil/transaction/v1\n.
  3. Hash with BLAKE3 (32-byte output).
  4. Sign with Ed25519.
package sigil

import (
    "crypto/ed25519"
    "encoding/hex"
    "encoding/json"
    "time"

    "github.com/zeebo/blake3"
)

const TxDomainTag = "sigil/transaction/v1\n"

type TxBody struct {
    ChainID   string         `json:"chain_id"`
    SenderDID string         `json:"sender_did"`
    Nonce     uint64         `json:"nonce"`
    GasLimit  uint64         `json:"gas_limit"`
    GasPrice  uint64         `json:"gas_price"`
    TxType    map[string]any `json:"tx_type"`
    Timestamp string         `json:"timestamp"`
}

type SignedTx struct {
    Tx               TxBody `json:"tx"`
    Signature        string `json:"signature"`
    SignerPublicKey  string `json:"signer_public_key"`
}

func TransferTx(
    chainID, senderDID, recipientDID string,
    amountBaseUnits uint64,
    nonce uint64,
    memo *string,
) TxBody {
    payload := map[string]any{
        "recipient_did": recipientDID,
        "amount":        amountBaseUnits,
        "memo":          memo,
    }
    return TxBody{
        ChainID:   chainID,
        SenderDID: senderDID,
        Nonce:     nonce,
        GasLimit:  200_000,
        GasPrice:  1,
        TxType:    map[string]any{"Transfer": payload},
        Timestamp: time.Now().UTC().Format(time.RFC3339Nano),
    }
}

func SignTx(tx TxBody, identity *Identity) (SignedTx, error) {
    body, err := json.Marshal(tx)
    if err != nil {
        return SignedTx{}, err
    }
    transcript := append([]byte(TxDomainTag), body...)
    digest := blake3.Sum256(transcript)
    signature := ed25519.Sign(identity.PrivateKey, digest[:])
    return SignedTx{
        Tx:              tx,
        Signature:       hex.EncodeToString(signature),
        SignerPublicKey: hex.EncodeToString(identity.PublicKey),
    }, nil
}

Submitting and waiting

type SendTxResult struct {
    TxHash string `json:"tx_hash"`
}

func SendTransaction(
    ctx context.Context, client *http.Client, network Network, signed SignedTx,
) (string, error) {
    result, err := Call[SendTxResult](ctx, client, network, "sigil_sendTransaction", signed)
    if err != nil {
        return "", err
    }
    return result.TxHash, nil
}

type Receipt struct {
    TxHash      string  `json:"tx_hash"`
    BlockHeight *uint64 `json:"block_height"`
    Status      string  `json:"status"`
    GasUsed     *uint64 `json:"gas_used"`
    Error       *string `json:"error"`
}

func WaitReceipt(
    ctx context.Context, client *http.Client, network Network, txHash string,
) (Receipt, error) {
    deadline := time.Now().Add(30 * time.Second)
    for time.Now().Before(deadline) {
        r, err := Call[Receipt](ctx, client, network, "sigil_getTransactionReceipt",
            map[string]any{"tx_hash": txHash})
        if err == nil && r.Status != "pending" {
            return r, nil
        }
        select {
        case <-time.After(1500 * time.Millisecond):
        case <-ctx.Done():
            return Receipt{}, ctx.Err()
        }
    }
    return Receipt{}, fmt.Errorf("receipt for %s did not finalise", txHash)
}

Subscribing to events

WebSocket subscriptions are on the roadmap. Until then, poll sigil_getLatestBlock from a goroutine:
type Block struct {
    Height uint64 `json:"height"`
}

func StreamBlocks(ctx context.Context, client *http.Client, network Network) <-chan Block {
    ch := make(chan Block)
    go func() {
        defer close(ch)
        var last uint64
        for {
            block, err := Call[Block](ctx, client, network, "sigil_getLatestBlock", map[string]any{})
            if err == nil && block.Height > last {
                last = block.Height
                select {
                case ch <- block:
                case <-ctx.Done():
                    return
                }
            }
            select {
            case <-time.After(1500 * time.Millisecond):
            case <-ctx.Done():
                return
            }
        }
    }()
    return ch
}

Error handling

RPCError carries the JSON-RPC code. Inspect it before deciding to retry.
result, err := SendTransaction(ctx, client, sigil.Testnet, signed)
if err != nil {
    var rpcErr *sigil.RPCError
    if errors.As(err, &rpcErr) {
        switch rpcErr.Code {
        case -32002: // mempool full
            // backoff and retry
        case -32601: // method not found
            return fmt.Errorf("RPC node does not support this method: %w", err)
        }
    }
    return err
}
CodeMeaning
-32600Invalid JSON-RPC envelope
-32601Method not found
-32602Invalid params
-32603Internal error (retryable)
-32002Mempool full / rate-limited

Worked example: send a payment with retry

package main

import (
    "context"
    "errors"
    "fmt"
    "log"
    "net/http"
    "os"
    "strconv"
    "time"

    "github.com/yourorg/sigil-quickstart/sigil"
)

func sendPaymentWithRetry(
    ctx context.Context,
    client *http.Client,
    network sigil.Network,
    identity *sigil.Identity,
    recipientDID string,
    amountBaseUnits uint64,
) (string, error) {
    bal, err := sigil.GetBalance(ctx, client, network, identity.DID)
    if err != nil {
        return "", fmt.Errorf("fetch nonce: %w", err)
    }

    tx := sigil.TransferTx(network.ChainID, identity.DID, recipientDID,
        amountBaseUnits, bal.Nonce, nil)
    signed, err := sigil.SignTx(tx, identity)
    if err != nil {
        return "", fmt.Errorf("sign: %w", err)
    }

    delay := 250 * time.Millisecond
    for attempt := 0; attempt < 5; attempt++ {
        hash, err := sigil.SendTransaction(ctx, client, network, signed)
        if err == nil {
            return hash, nil
        }
        var rpcErr *sigil.RPCError
        if errors.As(err, &rpcErr) && (rpcErr.Code == -32002 || rpcErr.Code == -32603) && attempt < 4 {
            time.Sleep(delay)
            delay *= 2
            continue
        }
        return "", err
    }
    return "", errors.New("unreachable")
}

func main() {
    rpcURL := os.Getenv("SIGIL_RPC_URL")
    if rpcURL == "" {
        rpcURL = "https://testnet-rpc.sigil.ml/rpc"
    }
    network := sigil.Network{
        ChainID: os.Getenv("SIGIL_CHAIN_ID"),
        RPCURL:  rpcURL,
        Timeout: 30 * time.Second,
    }
    if network.ChainID == "" {
        network.ChainID = "sigil-testnet-1"
    }

    identity, err := sigil.LoadAgent(os.Getenv("SIGIL_SIGNING_KEY_HEX"))
    if err != nil {
        log.Fatalf("load identity: %v", err)
    }

    recipient := os.Getenv("RECIPIENT_DID")
    amount, err := strconv.ParseUint(os.Getenv("AMOUNT_BASE_UNITS"), 10, 64)
    if err != nil {
        log.Fatalf("AMOUNT_BASE_UNITS: %v", err)
    }

    ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
    defer cancel()
    client := &http.Client{Timeout: network.Timeout}

    hash, err := sendPaymentWithRetry(ctx, client, network, identity, recipient, amount)
    if err != nil {
        log.Fatalf("send: %v", err)
    }
    fmt.Println("submitted:", hash)
}

Reference