@khalander/forge-sdk
v0.1.3
Published
Minimal on-chain payment-logging SDK for the ForgePay showcase contract (Sepolia).
Readme
@khalander/forge-sdk - wallet-native payments for ForgePay (Sepolia)
The complete payments stack for apps that settle on-chain: wallet authentication, user-signed charging, and verifiable on-chain receipts - one SDK, one integration.
- Live contract:
0x5cB9072417727384F50948212086d885f2EEa2EC(Sepolia, source-verified) - Security: re-entrancy secured and timestamp-dependency secured by architecture, not by patch (see §6).
1. Install
bun add @khalander/forge-sdk thirdweb(thirdweb ships the wallet-adapter layer - connectors and TypeScript types.
Every payment, auth, and settlement decision is made by ForgePay.)
2. Env
# free at https://thirdweb.com/create-api-key
NEXT_PUBLIC_THIRDWEB_CLIENT_ID=YOUR_THIRDWEB_CLIENT_ID
# your own wallet: every user payment goes straight here. Set once per deployment.
NEXT_PUBLIC_DEV_ADDRESS=0xYOUR_DEV_ADDRESS3. Authenticate + charge with ForgePay (your money path)
ForgePay authentication is wallet-native: there are no passwords and no sessions
to hijack - the wallet is the credential. ForgePayProvider establishes the
session, ConnectButton onboards the user (Phantom-first), and useActiveAccount
is the logged-in identity for everything downstream:
"use client";
// Everything flows through the SDK: one runtime, one wallet store.
import {
ConnectButton,
FORGE_PAY_WALLETS as wallets,
createThirdwebClient,
prepareTransaction,
sepolia,
sendTransaction,
toWei,
useActiveAccount,
} from "@khalander/forge-sdk/react";
const client = createThirdwebClient({
clientId: process.env.NEXT_PUBLIC_THIRDWEB_CLIENT_ID!,
});
export function PayButton() {
// Hardcoded per deployment via env - every user payment goes straight here.
const devAddress = process.env.NEXT_PUBLIC_DEV_ADDRESS! as `0x${string}`;
// The authenticated user. Null = signed out.
const account = useActiveAccount();
async function pay() {
if (!account) throw new Error("Connect a wallet first");
const tx = prepareTransaction({
to: devAddress,
value: toWei("0.01"),
chain: sepolia,
client,
});
const { transactionHash } = await sendTransaction({ transaction: tx, account });
return transactionHash; // <-- save as payTxHash (see §4)
}
return (
<>
<ConnectButton client={client} chain={sepolia} wallets={wallets} />
<button onClick={pay}>Subscribe - 0.01 ETH</button>
{account && <p>Paying as {account.address}</p>}
</>
);
}4. Create the DB table (Drizzle + Postgres assumed)
export const payments = pgTable("payments", {
id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
userAddress: text("user_address").notNull(),
devAddress: text("dev_address").notNull(),
amountWei: text("amount_wei").notNull(),
plan: text("plan").notNull().default("monthly"),
status: text("status").notNull().default("paid"), // paid | failed
payTxHash: text("pay_tx_hash"),
logTxHash: text("log_tx_hash"),
createdAt: timestamp("created_at").defaultNow().notNull(),
});5. Settle with @khalander/forge-sdk (the on-chain receipt)
Wrap the app once, then settle after the payment succeeds. Settlement is a
two-phase commit, both phases signed by the user: (1) value transfer to the
dev, (2) ForgePay.logPayment receipt - amount, parties, and plan sealed
on-chain, with the protocol counters (totalTxs, totalDevs) advancing:
"use client";
import { ForgePayProvider, useLogPayment } from "@khalander/forge-sdk/react";
export function SubscribeCard() {
// Same env dev address the payment went to.
const devAddress = process.env.NEXT_PUBLIC_DEV_ADDRESS! as `0x${string}`;
return (
<ForgePayProvider thirdwebClientId={process.env.NEXT_PUBLIC_THIRDWEB_CLIENT_ID!}>
<SubscribeInner devAddress={devAddress} />
</ForgePayProvider>
);
}
function SubscribeInner({ devAddress }: { devAddress: `0x${string}` }) {
const { log, isPending, address } = useLogPayment();
async function subscribe(payTxHash: `0x${string}`) {
if (!address) throw new Error("Connect a wallet first");
// 1. record the real payment in YOUR db
const row = await db.insert(payments).values({
userAddress: address,
devAddress,
amountWei: toWei("0.01").toString(),
plan: "monthly",
status: "paid",
payTxHash,
});
// 2. settle it on-chain through ForgePay
const { txHash } = await log({
devAddress,
userAddress: address,
amountWei: BigInt(toWei("0.01").toString()),
plan: "monthly",
});
// 3. save the receipt back to the row, show status from YOUR table
await db.update(payments).set({ logTxHash: txHash }).where(eq(payments.id, row.id));
}
}Vanilla (non-React) alternative:
import { createForgePay } from "@khalander/forge-sdk";
const forgePay = createForgePay({
thirdwebClientId: process.env.NEXT_PUBLIC_THIRDWEB_CLIENT_ID!,
});
const { txHash } = await forgePay.logPayment(
{ devAddress, userAddress, amountWei, plan: "monthly" },
account,
);Verify any settlement on Etherscan: https://sepolia.etherscan.io/tx/<logTxHash>
shows the PaymentLogged(dev, user, amount, plan) event, and the contract's
totalTxs / totalDevs counters grow with usage.
6. Security architecture
ForgePay's threat model starts from a simple observation: the most secure code is code that cannot be attacked because the attack surface does not exist. Both headline guarantees below are structural - they hold for every deployment, with no configuration to get wrong.
6.1 Re-entrancy: secured by construction
Classic re-entrancy (the DAO, SpankChain, dForce pattern) needs three ingredients: (a) the contract calls out to an untrusted address, (b) state updates happen after that call, (c) value is at stake. ForgePay removes all three:
- Zero external calls.
logPaymentinvokes no other contract and performs no.call{value}to users. The only cross-boundary effect is aneventemission, which cannot re-enter. There is no fallback function to hijack, no token hook (ERC777,ERC1155Receiver) to trigger, no callback path of any kind. - Check-Effects-Interactions ordering throughout. The two state effects -
totalTxs += 1and the first-seentotalDevsbump - execute before the event emission, so even a hypothetical future extension that adds callbacks inherits the safe ordering. - No value custody, ever. The contract is not payable outside pure logging, holds no balances, and owns no allowances. A re-entrancy exploit with nothing to drain is a non-issue - the maximum extractable value of this contract is exactly zero.
- Minimal privileged surface. The only authority in the system is
owner, fixed immutably at deploy time (constructor), with no transfer/renounce ceremony to mismanage and no admin functions touching funds - because there are no funds to administer.
Net effect: the entire re-entrancy family (single-function, cross-function, cross-contract, read-only) is inapplicable to ForgePay - not mitigated, but structurally absent.
6.2 Timestamp dependence: secured by elimination
Timestamp attacks (miner/validator time games, deadline bypasses, vesting and
lottery manipulation) need time-gated logic: expiries, lockups, windows, or
randomness derived from block.timestamp. ForgePay's money path contains none
of these:
- No deadlines, expiries, or lockups. Settlement is atomic per transaction: pay, then log. There is no window in which timing changes the outcome, so no validator can profit by shifting a timestamp.
- No on-chain randomness or sequencing from block data. Ordering, pricing, and plan selection all happen off-chain in the integrator's app; the chain only records the result. Nothing to grind, nothing to front-run for advantage.
block.timestampappears exactly once - as the informationalstartedAtfield in the showcase-onlyflagSubscription, which moves no funds, gates no access, and feeds no decision. A skewed timestamp there changes a displayed number and nothing else.
6.3 Transparency and least privilege
- Verified source, public counters. The deployed bytecode exactly matches the
published source (Etherscan Exact Match);
totalTxsandtotalDevslet anyone audit platform usage without trusting our servers. - User-signed everything. Both settlement phases require the subscriber's wallet signature. The SDK holds no keys, runs no relayer, and cannot move a single wei on anyone's behalf.
- Scope honesty: demo-grade deployment, Sepolia testnet only - no audit claims, no mainnet.
