x402-engineer 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/AGENT.md +102 -0
  2. package/README.md +43 -0
  3. package/dist/cli.cjs +137 -0
  4. package/package.json +51 -0
  5. package/skills/stellar-dev/SKILL.md +146 -0
  6. package/skills/stellar-dev/advanced-patterns.md +188 -0
  7. package/skills/stellar-dev/api-rpc-horizon.md +521 -0
  8. package/skills/stellar-dev/common-pitfalls.md +510 -0
  9. package/skills/stellar-dev/contracts-soroban.md +565 -0
  10. package/skills/stellar-dev/ecosystem.md +430 -0
  11. package/skills/stellar-dev/frontend-stellar-sdk.md +651 -0
  12. package/skills/stellar-dev/resources.md +306 -0
  13. package/skills/stellar-dev/security.md +491 -0
  14. package/skills/stellar-dev/standards-reference.md +94 -0
  15. package/skills/stellar-dev/stellar-assets.md +419 -0
  16. package/skills/stellar-dev/testing.md +786 -0
  17. package/skills/stellar-dev/zk-proofs.md +136 -0
  18. package/skills/x402-add-paywall/SKILL.md +208 -0
  19. package/skills/x402-add-paywall/references/patterns.md +132 -0
  20. package/skills/x402-debug/SKILL.md +92 -0
  21. package/skills/x402-debug/references/checklist.md +146 -0
  22. package/skills/x402-explain/SKILL.md +136 -0
  23. package/skills/x402-init/SKILL.md +129 -0
  24. package/skills/x402-init/templates/env-example.md +17 -0
  25. package/skills/x402-init/templates/express/config.ts.md +29 -0
  26. package/skills/x402-init/templates/express/server.ts.md +30 -0
  27. package/skills/x402-init/templates/fastify/adapter.ts.md +66 -0
  28. package/skills/x402-init/templates/fastify/config.ts.md +29 -0
  29. package/skills/x402-init/templates/fastify/server.ts.md +90 -0
  30. package/skills/x402-init/templates/hono/config.ts.md +29 -0
  31. package/skills/x402-init/templates/hono/server.ts.md +31 -0
  32. package/skills/x402-init/templates/next-app-router/config.ts.md +29 -0
  33. package/skills/x402-init/templates/next-app-router/server.ts.md +31 -0
  34. package/skills/x402-stellar/SKILL.md +139 -0
  35. package/skills/x402-stellar/references/api.md +237 -0
  36. package/skills/x402-stellar/references/patterns.md +276 -0
  37. package/skills/x402-stellar/references/setup.md +138 -0
  38. package/skills/x402-stellar/scripts/check-deps.js +218 -0
@@ -0,0 +1,651 @@
1
+ # Frontend with Stellar SDK (Next.js / React)
2
+
3
+ ## Goals
4
+ - Single SDK instance for the app (RPC/Horizon + transaction building)
5
+ - Freighter wallet integration (or multi-wallet via Stellar Wallets Kit)
6
+ - Clean separation of client/server in Next.js
7
+ - Transaction sending with proper confirmation handling
8
+
9
+ ## Quick Navigation
10
+ - SDK setup and env config: [SDK Initialization](#sdk-initialization)
11
+ - Wallet integrations: [Wallet Integration](#wallet-integration)
12
+ - Tx build/send patterns: [Transaction Building](#transaction-building), [Transaction Submission](#transaction-submission)
13
+ - React + Next.js patterns: [React Components](#react-components), [Next.js App Router Setup](#nextjs-app-router-setup)
14
+ - Smart wallets/passkeys: [Smart Accounts (Passkey Wallets)](#smart-accounts-passkey-wallets)
15
+ - Production UX checklist: [Transaction UX Checklist](#transaction-ux-checklist)
16
+
17
+ ## Recommended Dependencies
18
+
19
+ > **Requires Node.js 20+** — the Stellar SDK dropped Node 18 support.
20
+
21
+ ```bash
22
+ npm install @stellar/stellar-sdk @stellar/freighter-api
23
+ # Or for multi-wallet support:
24
+ npm install @stellar/stellar-sdk @creit.tech/stellar-wallets-kit
25
+ ```
26
+
27
+ ## SDK Initialization
28
+
29
+ > For the full API reference (RPC methods, Horizon endpoints, migration guide), see [api-rpc-horizon.md](api-rpc-horizon.md).
30
+
31
+ ### Basic Setup
32
+ ```typescript
33
+ import * as StellarSdk from "@stellar/stellar-sdk";
34
+
35
+ // For Testnet
36
+ const testnetServer = new StellarSdk.Horizon.Server("https://horizon-testnet.stellar.org");
37
+ const testnetRpc = new StellarSdk.rpc.Server("https://soroban-testnet.stellar.org");
38
+ const testnetNetworkPassphrase = StellarSdk.Networks.TESTNET;
39
+
40
+ // For Mainnet
41
+ const mainnetServer = new StellarSdk.Horizon.Server("https://horizon.stellar.org");
42
+ const mainnetRpcUrl = process.env.NEXT_PUBLIC_STELLAR_MAINNET_RPC_URL;
43
+ if (!mainnetRpcUrl) throw new Error("Missing NEXT_PUBLIC_STELLAR_MAINNET_RPC_URL");
44
+ const mainnetRpc = new StellarSdk.rpc.Server(mainnetRpcUrl); // set from your chosen RPC provider
45
+ const mainnetNetworkPassphrase = StellarSdk.Networks.PUBLIC;
46
+ ```
47
+
48
+ ### Environment Configuration
49
+ > Use a provider-specific mainnet RPC URL (see: https://developers.stellar.org/docs/data/apis/rpc/providers).
50
+
51
+ ```typescript
52
+ // lib/stellar.ts
53
+ import * as StellarSdk from "@stellar/stellar-sdk";
54
+
55
+ const NETWORK = process.env.NEXT_PUBLIC_STELLAR_NETWORK || "testnet";
56
+
57
+ const requireEnv = (name: string): string => {
58
+ const value = process.env[name];
59
+ if (!value) throw new Error(`Missing required env var: ${name}`);
60
+ return value;
61
+ };
62
+
63
+ export const config = {
64
+ testnet: {
65
+ horizonUrl: "https://horizon-testnet.stellar.org",
66
+ rpcUrl: "https://soroban-testnet.stellar.org",
67
+ networkPassphrase: StellarSdk.Networks.TESTNET,
68
+ friendbotUrl: "https://friendbot.stellar.org",
69
+ },
70
+ mainnet: {
71
+ horizonUrl: "https://horizon.stellar.org",
72
+ rpcUrl: requireEnv("NEXT_PUBLIC_STELLAR_MAINNET_RPC_URL"),
73
+ networkPassphrase: StellarSdk.Networks.PUBLIC,
74
+ friendbotUrl: null,
75
+ },
76
+ }[NETWORK]!;
77
+
78
+ export const horizon = new StellarSdk.Horizon.Server(config.horizonUrl);
79
+ export const rpc = new StellarSdk.rpc.Server(config.rpcUrl);
80
+ ```
81
+
82
+ ## Wallet Integration
83
+
84
+ ### Freighter (Primary Browser Wallet)
85
+ ```typescript
86
+ // hooks/useFreighter.ts
87
+ import { useState, useEffect, useCallback } from "react";
88
+ import {
89
+ isConnected,
90
+ isAllowed,
91
+ setAllowed,
92
+ getPublicKey,
93
+ signTransaction,
94
+ getNetwork,
95
+ } from "@stellar/freighter-api";
96
+
97
+ export function useFreighter() {
98
+ const [connected, setConnected] = useState(false);
99
+ const [address, setAddress] = useState<string | null>(null);
100
+ const [network, setNetwork] = useState<string | null>(null);
101
+
102
+ useEffect(() => {
103
+ checkConnection();
104
+ }, []);
105
+
106
+ const checkConnection = async () => {
107
+ const freighterConnected = await isConnected();
108
+ if (!freighterConnected) return;
109
+
110
+ const allowed = await isAllowed();
111
+ if (allowed) {
112
+ const pubKey = await getPublicKey();
113
+ const net = await getNetwork();
114
+ setConnected(true);
115
+ setAddress(pubKey);
116
+ setNetwork(net);
117
+ }
118
+ };
119
+
120
+ const connect = useCallback(async () => {
121
+ const freighterConnected = await isConnected();
122
+ if (!freighterConnected) {
123
+ throw new Error("Freighter extension not installed");
124
+ }
125
+
126
+ await setAllowed();
127
+ const pubKey = await getPublicKey();
128
+ const net = await getNetwork();
129
+
130
+ setConnected(true);
131
+ setAddress(pubKey);
132
+ setNetwork(net);
133
+
134
+ return pubKey;
135
+ }, []);
136
+
137
+ const disconnect = useCallback(() => {
138
+ setConnected(false);
139
+ setAddress(null);
140
+ setNetwork(null);
141
+ }, []);
142
+
143
+ const sign = useCallback(
144
+ async (xdr: string, networkPassphrase: string) => {
145
+ if (!connected) throw new Error("Wallet not connected");
146
+ return signTransaction(xdr, { networkPassphrase });
147
+ },
148
+ [connected]
149
+ );
150
+
151
+ return { connected, address, network, connect, disconnect, sign };
152
+ }
153
+ ```
154
+
155
+ ### Stellar Wallets Kit (Multi-Wallet)
156
+ ```typescript
157
+ // hooks/useStellarWallet.ts
158
+ import { useState, useCallback } from "react";
159
+ import {
160
+ StellarWalletsKit,
161
+ WalletNetwork,
162
+ allowAllModules,
163
+ FREIGHTER_ID,
164
+ LOBSTR_ID,
165
+ XBULL_ID,
166
+ } from "@creit.tech/stellar-wallets-kit";
167
+
168
+ const kit = new StellarWalletsKit({
169
+ network: WalletNetwork.TESTNET,
170
+ selectedWalletId: FREIGHTER_ID,
171
+ modules: allowAllModules(),
172
+ });
173
+
174
+ export function useStellarWallet() {
175
+ const [address, setAddress] = useState<string | null>(null);
176
+
177
+ const connect = useCallback(async () => {
178
+ await kit.openModal({
179
+ onWalletSelected: async (option) => {
180
+ kit.setWallet(option.id);
181
+ const { address } = await kit.getAddress();
182
+ setAddress(address);
183
+ },
184
+ });
185
+ }, []);
186
+
187
+ const disconnect = useCallback(() => {
188
+ setAddress(null);
189
+ }, []);
190
+
191
+ const sign = useCallback(async (xdr: string) => {
192
+ const { signedTxXdr } = await kit.signTransaction(xdr);
193
+ return signedTxXdr;
194
+ }, []);
195
+
196
+ return { address, connect, disconnect, sign, kit };
197
+ }
198
+ ```
199
+
200
+ ## Transaction Building
201
+
202
+ ### Basic Payment
203
+ ```typescript
204
+ import * as StellarSdk from "@stellar/stellar-sdk";
205
+ import { horizon, config } from "@/lib/stellar";
206
+
207
+ export async function buildPaymentTx(
208
+ sourceAddress: string,
209
+ destinationAddress: string,
210
+ amount: string,
211
+ asset: StellarSdk.Asset = StellarSdk.Asset.native()
212
+ ) {
213
+ const account = await horizon.loadAccount(sourceAddress);
214
+
215
+ const transaction = new StellarSdk.TransactionBuilder(account, {
216
+ fee: StellarSdk.BASE_FEE,
217
+ networkPassphrase: config.networkPassphrase,
218
+ })
219
+ .addOperation(
220
+ StellarSdk.Operation.payment({
221
+ destination: destinationAddress,
222
+ asset: asset,
223
+ amount: amount,
224
+ })
225
+ )
226
+ .setTimeout(180)
227
+ .build();
228
+
229
+ return transaction.toXDR();
230
+ }
231
+ ```
232
+
233
+ ### Soroban Contract Invocation
234
+ ```typescript
235
+ import * as StellarSdk from "@stellar/stellar-sdk";
236
+ import { rpc, config } from "@/lib/stellar";
237
+
238
+ export async function invokeContract(
239
+ sourceAddress: string,
240
+ contractId: string,
241
+ method: string,
242
+ args: StellarSdk.xdr.ScVal[]
243
+ ) {
244
+ const account = await rpc.getAccount(sourceAddress);
245
+
246
+ const contract = new StellarSdk.Contract(contractId);
247
+
248
+ let transaction = new StellarSdk.TransactionBuilder(account, {
249
+ fee: StellarSdk.BASE_FEE,
250
+ networkPassphrase: config.networkPassphrase,
251
+ })
252
+ .addOperation(contract.call(method, ...args))
253
+ .setTimeout(180)
254
+ .build();
255
+
256
+ // Simulate to get resource estimates
257
+ const simulation = await rpc.simulateTransaction(transaction);
258
+
259
+ if (StellarSdk.rpc.Api.isSimulationError(simulation)) {
260
+ throw new Error(`Simulation failed: ${simulation.error}`);
261
+ }
262
+
263
+ // Assemble with proper resources
264
+ transaction = StellarSdk.rpc.assembleTransaction(
265
+ transaction,
266
+ simulation
267
+ ).build();
268
+
269
+ return transaction.toXDR();
270
+ }
271
+ ```
272
+
273
+ ### Building ScVal Arguments
274
+ ```typescript
275
+ import * as StellarSdk from "@stellar/stellar-sdk";
276
+
277
+ // Common conversions
278
+ const addressVal = StellarSdk.Address.fromString(address).toScVal();
279
+ const i128Val = StellarSdk.nativeToScVal(BigInt(amount), { type: "i128" });
280
+ const u32Val = StellarSdk.nativeToScVal(42, { type: "u32" });
281
+ const stringVal = StellarSdk.nativeToScVal("hello", { type: "string" });
282
+ const symbolVal = StellarSdk.nativeToScVal("transfer", { type: "symbol" });
283
+
284
+ // Struct
285
+ const structVal = StellarSdk.nativeToScVal(
286
+ { name: "Token", decimals: 7 },
287
+ {
288
+ type: {
289
+ name: ["symbol", null],
290
+ decimals: ["u32", null],
291
+ },
292
+ }
293
+ );
294
+
295
+ // Vec
296
+ const vecVal = StellarSdk.nativeToScVal([1, 2, 3], { type: "i128" });
297
+ ```
298
+
299
+ ## Transaction Submission
300
+
301
+ ### Submit and Wait for Confirmation
302
+ ```typescript
303
+ import * as StellarSdk from "@stellar/stellar-sdk";
304
+ import { rpc, horizon, config } from "@/lib/stellar";
305
+
306
+ export async function submitTransaction(signedXdr: string) {
307
+ const transaction = StellarSdk.TransactionBuilder.fromXDR(
308
+ signedXdr,
309
+ config.networkPassphrase
310
+ );
311
+
312
+ // For Soroban transactions, use RPC
313
+ if (transaction.operations.some(op => op.type === "invokeHostFunction")) {
314
+ return submitSorobanTransaction(signedXdr);
315
+ }
316
+
317
+ // For classic transactions, use Horizon
318
+ return submitClassicTransaction(signedXdr);
319
+ }
320
+
321
+ async function submitSorobanTransaction(signedXdr: string) {
322
+ const transaction = StellarSdk.TransactionBuilder.fromXDR(
323
+ signedXdr,
324
+ config.networkPassphrase
325
+ ) as StellarSdk.Transaction;
326
+
327
+ const response = await rpc.sendTransaction(transaction);
328
+
329
+ if (response.status === "ERROR") {
330
+ throw new Error(`Transaction failed: ${response.errorResult}`);
331
+ }
332
+
333
+ // Poll for completion
334
+ let getResponse = await rpc.getTransaction(response.hash);
335
+ while (getResponse.status === "NOT_FOUND") {
336
+ await new Promise((resolve) => setTimeout(resolve, 1000));
337
+ getResponse = await rpc.getTransaction(response.hash);
338
+ }
339
+
340
+ if (getResponse.status === "SUCCESS") {
341
+ return {
342
+ hash: response.hash,
343
+ result: getResponse.returnValue,
344
+ };
345
+ }
346
+
347
+ throw new Error(`Transaction failed: ${getResponse.status}`);
348
+ }
349
+
350
+ async function submitClassicTransaction(signedXdr: string) {
351
+ const transaction = StellarSdk.TransactionBuilder.fromXDR(
352
+ signedXdr,
353
+ config.networkPassphrase
354
+ ) as StellarSdk.Transaction;
355
+
356
+ const response = await horizon.submitTransaction(transaction);
357
+ return {
358
+ hash: response.hash,
359
+ ledger: response.ledger,
360
+ };
361
+ }
362
+ ```
363
+
364
+ ## React Components
365
+
366
+ ### Connect Wallet Button
367
+ ```tsx
368
+ // components/ConnectButton.tsx
369
+ "use client";
370
+
371
+ import { useFreighter } from "@/hooks/useFreighter";
372
+
373
+ export function ConnectButton() {
374
+ const { connected, address, connect, disconnect } = useFreighter();
375
+
376
+ if (connected && address) {
377
+ return (
378
+ <div className="flex items-center gap-2">
379
+ <span className="text-sm">
380
+ {address.slice(0, 4)}...{address.slice(-4)}
381
+ </span>
382
+ <button
383
+ onClick={disconnect}
384
+ className="px-4 py-2 bg-red-500 text-white rounded"
385
+ >
386
+ Disconnect
387
+ </button>
388
+ </div>
389
+ );
390
+ }
391
+
392
+ return (
393
+ <button
394
+ onClick={connect}
395
+ className="px-4 py-2 bg-blue-500 text-white rounded"
396
+ >
397
+ Connect Wallet
398
+ </button>
399
+ );
400
+ }
401
+ ```
402
+
403
+ ### Send Payment Form
404
+ ```tsx
405
+ // components/SendPayment.tsx
406
+ "use client";
407
+
408
+ import { useState } from "react";
409
+ import { useFreighter } from "@/hooks/useFreighter";
410
+ import { buildPaymentTx, submitTransaction } from "@/lib/transactions";
411
+
412
+ export function SendPayment() {
413
+ const { address, sign } = useFreighter();
414
+ const [destination, setDestination] = useState("");
415
+ const [amount, setAmount] = useState("");
416
+ const [status, setStatus] = useState<string | null>(null);
417
+ const [loading, setLoading] = useState(false);
418
+
419
+ const handleSubmit = async (e: React.FormEvent) => {
420
+ e.preventDefault();
421
+ if (!address) return;
422
+
423
+ setLoading(true);
424
+ setStatus("Building transaction...");
425
+
426
+ try {
427
+ const xdr = await buildPaymentTx(address, destination, amount);
428
+
429
+ setStatus("Please sign in your wallet...");
430
+ const signedXdr = await sign(xdr, config.networkPassphrase);
431
+
432
+ setStatus("Submitting transaction...");
433
+ const result = await submitTransaction(signedXdr);
434
+
435
+ setStatus(`Success! Hash: ${result.hash}`);
436
+ } catch (error) {
437
+ setStatus(`Error: ${error.message}`);
438
+ } finally {
439
+ setLoading(false);
440
+ }
441
+ };
442
+
443
+ return (
444
+ <form onSubmit={handleSubmit} className="space-y-4">
445
+ <input
446
+ type="text"
447
+ placeholder="Destination Address"
448
+ value={destination}
449
+ onChange={(e) => setDestination(e.target.value)}
450
+ className="w-full p-2 border rounded"
451
+ />
452
+ <input
453
+ type="text"
454
+ placeholder="Amount (XLM)"
455
+ value={amount}
456
+ onChange={(e) => setAmount(e.target.value)}
457
+ className="w-full p-2 border rounded"
458
+ />
459
+ <button
460
+ type="submit"
461
+ disabled={loading || !address}
462
+ className="w-full p-2 bg-blue-500 text-white rounded disabled:opacity-50"
463
+ >
464
+ {loading ? "Processing..." : "Send"}
465
+ </button>
466
+ {status && <p className="text-sm">{status}</p>}
467
+ </form>
468
+ );
469
+ }
470
+ ```
471
+
472
+ ## Next.js App Router Setup
473
+
474
+ ### Provider Component
475
+ ```tsx
476
+ // app/providers.tsx
477
+ "use client";
478
+
479
+ import { ReactNode } from "react";
480
+
481
+ // Add any context providers here
482
+ export function Providers({ children }: { children: ReactNode }) {
483
+ return <>{children}</>;
484
+ }
485
+ ```
486
+
487
+ ### Layout
488
+ ```tsx
489
+ // app/layout.tsx
490
+ import { Providers } from "./providers";
491
+
492
+ export default function RootLayout({
493
+ children,
494
+ }: {
495
+ children: React.ReactNode;
496
+ }) {
497
+ return (
498
+ <html lang="en">
499
+ <body>
500
+ <Providers>{children}</Providers>
501
+ </body>
502
+ </html>
503
+ );
504
+ }
505
+ ```
506
+
507
+ ## Data Fetching
508
+
509
+ ### Account Balance
510
+ ```typescript
511
+ import { horizon } from "@/lib/stellar";
512
+
513
+ export async function getBalance(address: string) {
514
+ try {
515
+ const account = await horizon.loadAccount(address);
516
+ const nativeBalance = account.balances.find(
517
+ (b) => b.asset_type === "native"
518
+ );
519
+ return nativeBalance?.balance || "0";
520
+ } catch (error) {
521
+ if (error.response?.status === 404) {
522
+ return "0"; // Account not funded
523
+ }
524
+ throw error;
525
+ }
526
+ }
527
+ ```
528
+
529
+ ### Contract State
530
+ ```typescript
531
+ import * as StellarSdk from "@stellar/stellar-sdk";
532
+ import { rpc } from "@/lib/stellar";
533
+
534
+ export async function getContractData(
535
+ contractId: string,
536
+ key: StellarSdk.xdr.ScVal
537
+ ) {
538
+ const ledgerKey = StellarSdk.xdr.LedgerKey.contractData(
539
+ new StellarSdk.xdr.LedgerKeyContractData({
540
+ contract: new StellarSdk.Address(contractId).toScAddress(),
541
+ key: key,
542
+ durability: StellarSdk.xdr.ContractDataDurability.persistent(),
543
+ })
544
+ );
545
+
546
+ const entries = await rpc.getLedgerEntries(ledgerKey);
547
+
548
+ if (entries.entries.length === 0) {
549
+ return null;
550
+ }
551
+
552
+ return StellarSdk.scValToNative(
553
+ entries.entries[0].val.contractData().val()
554
+ );
555
+ }
556
+ ```
557
+
558
+ ## Smart Accounts (Passkey Wallets)
559
+
560
+ For passwordless authentication using WebAuthn passkeys, use Smart Account Kit.
561
+
562
+ ### Installation
563
+ ```bash
564
+ npm install smart-account-kit
565
+ ```
566
+
567
+ ### Quick Start
568
+ ```typescript
569
+ import { SmartAccountKit, IndexedDBStorage } from 'smart-account-kit';
570
+
571
+ const kit = new SmartAccountKit({
572
+ rpcUrl: 'https://soroban-testnet.stellar.org',
573
+ networkPassphrase: 'Test SDF Network ; September 2015',
574
+ accountWasmHash: 'YOUR_ACCOUNT_WASM_HASH',
575
+ webauthnVerifierAddress: 'CWEBAUTHN_VERIFIER_ADDRESS',
576
+ storage: new IndexedDBStorage(),
577
+ });
578
+
579
+ // On page load - silent restore from stored session
580
+ const result = await kit.connectWallet();
581
+ if (!result) {
582
+ showConnectButton(); // No stored session
583
+ }
584
+
585
+ // Create new wallet with passkey
586
+ const { contractId, credentialId } = await kit.createWallet(
587
+ 'My App',
588
+ 'user@example.com',
589
+ { autoSubmit: true }
590
+ );
591
+
592
+ // Connect to existing wallet (prompts for passkey)
593
+ await kit.connectWallet({ prompt: true });
594
+
595
+ // Sign and submit transactions
596
+ const result = await kit.signAndSubmit(transaction);
597
+
598
+ // Transfer tokens
599
+ await kit.transfer(tokenContract, recipient, amount);
600
+ ```
601
+
602
+ ### Key Features
603
+ - **Session Management**: Automatic credential persistence and silent reconnection
604
+ - **Multiple Signer Types**: Passkeys (secp256r1), Ed25519 keys, policies
605
+ - **Context Rules**: Fine-grained authorization for different operations
606
+ - **Policy Support**: Threshold multisig, spending limits, custom policies
607
+ - **External Wallet Support**: Connect Freighter, LOBSTR via adapters
608
+ - **Gasless Transactions**: Optional relayer integration for fee sponsoring
609
+
610
+ ### Fee Sponsorship with OpenZeppelin Relayer
611
+
612
+ The [OpenZeppelin Relayer](https://docs.openzeppelin.com/relayer/stellar) (also called Stellar Channels Service) handles gasless transaction submission. It replaces the deprecated Launchtube service and uses Stellar's native fee bump mechanism so users don't need XLM for fees.
613
+
614
+ ```typescript
615
+ import * as RPChannels from "@openzeppelin/relayer-plugin-channels";
616
+
617
+ const client = new RPChannels.ChannelsClient({
618
+ baseUrl: "https://channels.openzeppelin.com/testnet",
619
+ apiKey: "your-api-key",
620
+ });
621
+
622
+ // Submit a Soroban contract call with fee sponsorship
623
+ const response = await client.submitSorobanTransaction({
624
+ func: contractFunc,
625
+ auth: contractAuth,
626
+ });
627
+ ```
628
+
629
+ - **Testnet hosted instance**: `https://channels.openzeppelin.com/testnet` (API keys at `/gen`)
630
+ - **Production**: Self-host via Docker ([GitHub](https://github.com/OpenZeppelin/openzeppelin-relayer))
631
+ - **Stellar docs**: https://developers.stellar.org/docs/tools/openzeppelin-relayer
632
+
633
+ ### Resources
634
+ - **GitHub**: https://github.com/kalepail/smart-account-kit
635
+ - **OpenZeppelin Contracts**: https://github.com/OpenZeppelin/stellar-contracts
636
+ - **Legacy SDK**: https://github.com/kalepail/passkey-kit (for simpler use cases)
637
+
638
+ ## Transaction UX Checklist
639
+
640
+ - [ ] Show loading state during wallet signing
641
+ - [ ] Display transaction hash immediately after submission
642
+ - [ ] Track confirmation status (pending → success/failed)
643
+ - [ ] Handle common errors with clear messages:
644
+ - Wallet not connected
645
+ - User rejected signing
646
+ - Insufficient XLM for fees
647
+ - Account not funded
648
+ - Network mismatch (wallet on wrong network)
649
+ - Transaction timeout/expired
650
+ - [ ] Prevent double-submission while processing
651
+ - [ ] Show destination and amount before signing