Create and fund a Gift

A Gift escrows a USDC amount on the Solana fund-release contract. The recipient claims it by presenting a 16-character release code— a bearer credential that you generate, hand to them once, and never store. This guide walks through the four steps a merchant integration needs: generating a release code, creating the Gift, funding it on-chain, and sharing the code.

Time to first claim: about ten minutes if you already have a Solana wallet with USDC and a Sithril account.

Before you begin

You need four things:

  • A Sithril account. Sign in at sithril.com/login with a Solana wallet.
  • An API key. Generate one in the dashboard under API keys. The examples below use a placeholder sk_live_….
  • A Solana wallet holding the USDC you intend to gift, plus a small amount of SOL for transaction fees. The same wallet must sign the funding transaction in step 3.
  • The ability to run code in a trusted environment (your server / backend job). The release code is a bearer credential and must be generated server-side, not in your customer’s browser.

How it works

A Gift moves through four states. You drive the first three; the recipient drives the fourth.

StatusMeansYou do
requires_actionGift exists on Sithril, not yet funded on chain.Sign and submit the funding transaction we return.
openFunded; waiting for the recipient to claim.Share the release code (or the closed link).
releasedRecipient claimed; funds are theirs.Nothing — terminal state.
reclaimedYou pulled the gift back before it was claimed.Nothing — terminal state.

Release code spec

The release code is the bearer credential that lets the recipient claim the gift. The contract verifies the recipient’s ownership of the code via an Ed25519 signature derived from it — so anyone who knows the code can claim, and only them.

Sithril never sees the code in cleartext beyond the moment of release. We store a verification key derived from it (a Solana pubkey); the code itself stays in your environment.

Format

  • Length: 16 characters total — 15 data + 1 checksum.
  • Alphabet: Crockford base32 — 0-9 A-Z excluding I, L, O, U (32 symbols). The check character is computed mod 37 over the extended alphabet 0-9 A-Z * ~ $ = U.
  • Normalization: whitespace and dashes stripped, lowercase uppercased, I and L rewritten to 1, O to 0. So g7k-9p2-mwxn-3hQR and G7K9P2MWXN3HQR normalize to the same code (and the checksum protects against typos).
  • Entropy:15 Crockford symbols ≈ 75 bits, which is comfortable for a single-use credential and easy to hand-type from a paper insert.

Verification key derivation

Sithril identifies a Gift on chain by its release_verification_key— the public half of the Ed25519 keypair derived from the code:

ed25519_seed = SHA-256( utf8(release_code[:15]) )      # 32 bytes
keypair      = Ed25519.from_seed(ed25519_seed)
release_verification_key = base58(keypair.public)       # Solana pubkey

You hand the verification key to Sithril at create time. You never hand us the code. The contract derives the same keypair from the recipient’s submitted code at release time and verifies a signature against the stored key.

Generate codes in a trusted backend.Never expose release-code generation to a browser you don’t fully control — anyone with the code can claim the gift, full stop.

Step 1: Generate a release code

The implementation below is a Node port of Sithril’s own generator. Drop it into your backend. The generateReleaseCode() return value is what you’ll share with the recipient; verificationKeyFor(code) is what you’ll send to Sithril.

// release-code.js  (Node 20+, ESM)
import { createHash, randomBytes } from "node:crypto";
import nacl from "tweetnacl";
import { PublicKey } from "@solana/web3.js";

const CROCKFORD = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
const CHECK     = CROCKFORD + "*~$=U";   // 37 symbols, indexed 0..36

function checkChar(data /* 15 chars */) {
  let value = 0n;
  for (const ch of data) value = value * 32n + BigInt(CROCKFORD.indexOf(ch));
  return CHECK[Number(value % 37n)];
}

export function generateReleaseCode() {
  const bytes = randomBytes(15);
  let data = "";
  for (let i = 0; i < 15; i++) data += CROCKFORD[bytes[i] % 32];
  return data + checkChar(data);             // 16 chars
}

export function verificationKeyFor(code) {
  const seed = createHash("sha256")
    .update(Buffer.from(code.slice(0, 15), "utf8"))
    .digest();
  const { publicKey } = nacl.sign.keyPair.fromSeed(Uint8Array.from(seed));
  return new PublicKey(publicKey).toBase58();
}

Then in the call site:

const code = generateReleaseCode();
const rvk  = verificationKeyFor(code);
// store `code` somewhere temporary if you need to redisplay it
// (Sithril returns it to you only at create time)
Why client-side?Sithril could generate codes for you, but then we’d have to either log them (bad) or trust our TLS layer end-to-end with a bearer secret. Keeping generation in your environment means a Sithril compromise can’t claim outstanding gifts.

Step 2: Create the Gift

Send the amount, your sending wallet, and the verification key from step 1. Amount is in USDC minor units — USDC has 6 decimals, so $5.00 is 5000000.

curl https://api.sithril.com/v1/gifts \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 5000000,
    "sender_wallet": "CnRg8gM7xGD8eM4Sf814MzyoRhQn4SPm1DnPGe5AHoLC",
    "release_verification_key": "G3jKbu...base58...Q2"
  }'

Response:

{
  "id": "gift_1AbCdEf2GhIj",
  "object": "gift",
  "status": "requires_action",
  "amount": 5000000,
  "currency": "usdc",
  "sender_wallet": "CnRg...HoLC",
  "release_verification_key": "G3jK...Q2",
  "recipient_wallet": null,
  "next_action": {
    "type": "submit_transaction",
    "transaction": {
      "signer_wallet": "CnRg...HoLC",
      "serialized_transaction": "AQID...base64..."  // unsigned, ready for the sender to sign
    }
  },
  "created": 1748687600
}
  • The id (a gift_-prefixed token) is the handle for the closed redemption URL described in step 4.
  • next_action.transaction.serialized_transaction is the unsigned funding transaction. Your sender wallet must sign and submit it; the gift stays in requires_action until that transaction confirms.

Step 3: Sign and submit the funding transaction

Decode the base64 transaction, sign it with the sender wallet, and submit it to a Solana RPC. The example uses a server-held Keypair; if you sign with a browser wallet (Phantom, Solflare, etc.), hand the decoded Transactionto the wallet adapter’s signAndSendTransaction instead.

import { Connection, Transaction, Keypair } from "@solana/web3.js";

const create  = await createGiftResponse.json();   // from step 2
const senderKp = Keypair.fromSecretKey(/* 64-byte secret */);
const conn    = new Connection("https://api.mainnet-beta.solana.com");

const tx = Transaction.from(
  Buffer.from(create.next_action.transaction.serialized_transaction, "base64"),
);
tx.sign(senderKp);

const sig = await conn.sendRawTransaction(tx.serialize());
await conn.confirmTransaction(sig, "confirmed");

Once the transaction confirms, Sithril’s polling loop notices the on-chain escrow and transitions the gift to open. You can confirm with GET /v1/gifts/{id}— poll once after a few seconds rather than aggressively.

Step 4: Share the code with your recipient

The release code is all the recipient needs — they paste it at sithril.com/gifts/redeem, connect any Solana wallet, and sign. The destination is whichever wallet they connect, so they keep custody throughout.

You may also share a closed redemption link at sithril.com/gifts/{id}. That page renders the amount and your business name before the recipient pastes their code — useful when the gift is part of a larger flow (an order confirmation email, an in-app reward, etc.). The link is optional: the code alone is sufficient.

Hi <recipient>,

You have a $5.00 USDC gift waiting. Claim it at:
  https://sithril.com/gifts/redeem

Your release code is:
  G7K9-P2MW-XN3H-QR4S

(Or use the direct link: https://sithril.com/gifts/gift_1AbCdEf2GhIj)
The code is a bearer credential.Treat it like a password or an API key — anyone who sees it can claim the gift. Send it over a channel the recipient controls (their email, their account inbox). If you need to revoke before they claim, see What’s next.

What’s next

  • Reclaim an unclaimed gift POST /v1/gifts/{id}/reclaim returns a signing transaction that returns the escrow to the original sender wallet. Works any time before status becomes released.
  • List your gifts GET /v1/gifts?status=open for outstanding ones; supports cursor pagination via limit, created_gte, etc.
  • Webhooks— not yet available. For now, poll GET /v1/gifts/{id} on the cadence that suits your flow.
  • Full API reference /docs/api#tag/Gifts lists every field on every endpoint.