onchainfans 1.3.0 → 1.6.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.
package/src/index.ts CHANGED
@@ -318,10 +318,584 @@ program
318
318
  }
319
319
  })
320
320
 
321
+ // ============ SWAP ============
322
+ program
323
+ .command('swap')
324
+ .description('Swap ANY token (gas sponsored)')
325
+ .option('-k, --api-key <key>', 'API key')
326
+ .option('--sell <token>', 'Token to sell (address or symbol: ETH, USDC, etc.)')
327
+ .option('--buy <token>', 'Token to buy (address or symbol: ETH, USDC, etc.)')
328
+ .option('--amount <amount>', 'Amount to sell')
329
+ .option('--quote', 'Only get quote, don\'t execute')
330
+ .action(async (options) => {
331
+ const apiKey = getApiKey(options.apiKey)
332
+
333
+ if (!options.sell || !options.buy || !options.amount) {
334
+ console.log(chalk.red('Please provide --sell, --buy, and --amount'))
335
+ console.log(chalk.dim('Examples:'))
336
+ console.log(chalk.dim(' npx onchainfans swap --sell ETH --buy USDC --amount 0.01'))
337
+ console.log(chalk.dim(' npx onchainfans swap --sell 0xabc... --buy 0xdef... --amount 100'))
338
+ process.exit(1)
339
+ }
340
+
341
+ // Accept either symbol shortcuts or contract addresses
342
+ const sellToken = options.sell.startsWith('0x') ? options.sell : options.sell.toUpperCase()
343
+ const buyToken = options.buy.startsWith('0x') ? options.buy : options.buy.toUpperCase()
344
+
345
+ if (sellToken.toLowerCase() === buyToken.toLowerCase()) {
346
+ console.log(chalk.red('Cannot swap same token'))
347
+ process.exit(1)
348
+ }
349
+
350
+ const spinner = ora(options.quote ? 'Getting quote...' : 'Executing swap...').start()
351
+
352
+ try {
353
+ if (options.quote) {
354
+ // Get quote only
355
+ const params = new URLSearchParams({
356
+ sellToken,
357
+ buyToken,
358
+ sellAmount: options.amount,
359
+ })
360
+
361
+ const response = await fetch(`${API_BASE}/agents/swap/quote?${params}`, {
362
+ headers: { Authorization: `Bearer ${apiKey}` },
363
+ })
364
+
365
+ if (!response.ok) {
366
+ const err = await response.json() as { error?: string; message?: string }
367
+ throw new Error(err.error || err.message || 'Failed to get quote')
368
+ }
369
+
370
+ const result = await response.json() as ApiResponse<{
371
+ sellToken: string
372
+ sellTokenAddress: string
373
+ sellAmountFormatted: string
374
+ buyToken: string
375
+ buyTokenAddress: string
376
+ buyAmountFormatted: string
377
+ feeAmountFormatted: string
378
+ gasEstimate: string
379
+ }>
380
+
381
+ spinner.stop()
382
+ console.log('')
383
+ console.log(chalk.cyan.bold(' Swap Quote'))
384
+ console.log(chalk.gray(' ─────────────────────────────────────'))
385
+ console.log(chalk.white(` Sell: ${result.data.sellAmountFormatted} ${result.data.sellToken}`))
386
+ console.log(chalk.green(` Receive: ${result.data.buyAmountFormatted} ${result.data.buyToken}`))
387
+ console.log(chalk.dim(` Fee: ${result.data.feeAmountFormatted} ${result.data.buyToken} (1%)`))
388
+ console.log('')
389
+ console.log(chalk.dim(' To execute: remove --quote flag'))
390
+ console.log('')
391
+ } else {
392
+ // Execute swap
393
+ const response = await fetch(`${API_BASE}/agents/swap/execute`, {
394
+ method: 'POST',
395
+ headers: {
396
+ Authorization: `Bearer ${apiKey}`,
397
+ 'Content-Type': 'application/json',
398
+ },
399
+ body: JSON.stringify({
400
+ sellToken,
401
+ buyToken,
402
+ sellAmount: options.amount,
403
+ }),
404
+ })
405
+
406
+ if (!response.ok) {
407
+ const err = await response.json() as { error?: string; message?: string }
408
+ throw new Error(err.error || err.message || 'Swap failed')
409
+ }
410
+
411
+ const result = await response.json() as ApiResponse<{
412
+ txHash: string
413
+ sellToken: string
414
+ sellTokenAddress: string
415
+ sellAmount: string
416
+ buyToken: string
417
+ buyTokenAddress: string
418
+ buyAmount: string
419
+ feeAmount: string
420
+ }>
421
+
422
+ spinner.succeed('Swap completed!')
423
+ console.log('')
424
+ console.log(chalk.green.bold(' Swap Successful!'))
425
+ console.log(chalk.gray(' ─────────────────────────────────────'))
426
+ console.log(chalk.white(` Sold: ${result.data.sellAmount} ${result.data.sellToken}`))
427
+ console.log(chalk.green(` Received: ${result.data.buyAmount} ${result.data.buyToken}`))
428
+ console.log(chalk.dim(` Fee: ${result.data.feeAmount} ${result.data.buyToken}`))
429
+ console.log(chalk.white(` TX: ${result.data.txHash}`))
430
+ console.log('')
431
+ console.log(chalk.dim(' Gas was sponsored by OnchainFans!'))
432
+ console.log('')
433
+ }
434
+ } catch (error) {
435
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error'
436
+ spinner.fail(chalk.red(`Failed: ${errorMessage}`))
437
+ process.exit(1)
438
+ }
439
+ })
440
+
441
+ // ============ SWAP TOKENS ============
442
+ program
443
+ .command('swap-tokens')
444
+ .description('List common token shortcuts')
445
+ .action(async () => {
446
+ console.log('')
447
+ console.log(chalk.cyan.bold(' Common Token Shortcuts'))
448
+ console.log(chalk.gray(' ─────────────────────────────────────'))
449
+ console.log(chalk.white(' ETH - Ethereum (native)'))
450
+ console.log(chalk.white(' WETH - Wrapped ETH'))
451
+ console.log(chalk.white(' USDC - USD Coin'))
452
+ console.log(chalk.white(' ONCHAINFANS - OnchainFans Token'))
453
+ console.log(chalk.white(' CUM - CUM Token'))
454
+ console.log(chalk.white(' ZORA - Zora Token'))
455
+ console.log('')
456
+ console.log(chalk.yellow(' Swap ANY token by address!'))
457
+ console.log(chalk.dim(' Examples:'))
458
+ console.log(chalk.dim(' npx onchainfans swap --sell ETH --buy USDC --amount 0.01'))
459
+ console.log(chalk.dim(' npx onchainfans swap --sell 0xabc... --buy 0xdef... --amount 100'))
460
+ console.log('')
461
+ console.log(chalk.dim(' Get token info: npx onchainfans token-info <address>'))
462
+ console.log('')
463
+ })
464
+
465
+ // ============ TOKEN INFO ============
466
+ program
467
+ .command('token-info')
468
+ .description('Get info about any token by address')
469
+ .argument('<address>', 'Token contract address')
470
+ .action(async (address) => {
471
+ if (!address.startsWith('0x') || address.length !== 42) {
472
+ console.log(chalk.red('Invalid address. Must be a 42-character hex string starting with 0x'))
473
+ process.exit(1)
474
+ }
475
+
476
+ const spinner = ora('Fetching token info...').start()
477
+
478
+ try {
479
+ const response = await fetch(`${API_BASE}/agents/swap/token-info?address=${address}`)
480
+
481
+ if (!response.ok) {
482
+ const err = await response.json() as { error?: string; message?: string }
483
+ throw new Error(err.error || err.message || 'Failed to get token info')
484
+ }
485
+
486
+ const result = await response.json() as ApiResponse<{
487
+ address: string
488
+ symbol: string
489
+ decimals: number
490
+ }>
491
+
492
+ spinner.stop()
493
+ console.log('')
494
+ console.log(chalk.cyan.bold(' Token Info'))
495
+ console.log(chalk.gray(' ─────────────────────────────────────'))
496
+ console.log(chalk.white(` Symbol: ${result.data.symbol}`))
497
+ console.log(chalk.white(` Decimals: ${result.data.decimals}`))
498
+ console.log(chalk.white(` Address: ${result.data.address}`))
499
+ console.log('')
500
+ console.log(chalk.dim(' Use in swap:'))
501
+ console.log(chalk.dim(` npx onchainfans swap --sell ${address} --buy USDC --amount 100`))
502
+ console.log('')
503
+ } catch (error) {
504
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error'
505
+ spinner.fail(chalk.red(`Failed: ${errorMessage}`))
506
+ process.exit(1)
507
+ }
508
+ })
509
+
510
+ // ============ BALANCE ============
511
+ program
512
+ .command('balance')
513
+ .description('Check your USDC balance')
514
+ .option('-k, --api-key <key>', 'API key')
515
+ .action(async (options) => {
516
+ const apiKey = getApiKey(options.apiKey)
517
+ const spinner = ora('Checking balance...').start()
518
+
519
+ try {
520
+ const response = await fetch(`${API_BASE}/agents/balance`, {
521
+ headers: { Authorization: `Bearer ${apiKey}` },
522
+ })
523
+
524
+ if (!response.ok) throw new Error('Failed to get balance')
525
+
526
+ const result = await response.json() as ApiResponse<{
527
+ walletAddress: string
528
+ usdcBalance: string
529
+ }>
530
+
531
+ spinner.stop()
532
+ console.log('')
533
+ console.log(chalk.cyan.bold(' Your Balance'))
534
+ console.log(chalk.gray(' ─────────────────────────────────────'))
535
+ console.log(chalk.white(` Wallet: ${result.data.walletAddress}`))
536
+ console.log(chalk.green.bold(` USDC: ${result.data.usdcBalance}`))
537
+ console.log('')
538
+ } catch (error) {
539
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error'
540
+ spinner.fail(chalk.red(`Failed: ${errorMessage}`))
541
+ process.exit(1)
542
+ }
543
+ })
544
+
545
+ // ============ WALLET ============
546
+ program
547
+ .command('wallet')
548
+ .description('Get full wallet portfolio with all tokens and USD values')
549
+ .option('-k, --api-key <key>', 'API key')
550
+ .action(async (options) => {
551
+ const apiKey = getApiKey(options.apiKey)
552
+ const spinner = ora('Fetching wallet balances...').start()
553
+
554
+ try {
555
+ const response = await fetch(`${API_BASE}/agents/wallet`, {
556
+ headers: { Authorization: `Bearer ${apiKey}` },
557
+ })
558
+
559
+ if (!response.ok) throw new Error('Failed to get wallet balances')
560
+
561
+ const result = await response.json() as ApiResponse<{
562
+ address: string
563
+ nativeBalance: {
564
+ symbol: string
565
+ balance: string
566
+ priceUsd: string | null
567
+ valueUsd: string | null
568
+ }
569
+ tokens: Array<{
570
+ contractAddress: string
571
+ symbol: string
572
+ name: string
573
+ balance: string
574
+ priceUsd: string | null
575
+ valueUsd: string | null
576
+ }>
577
+ totalValueUsd: string
578
+ }>
579
+
580
+ spinner.stop()
581
+ console.log('')
582
+ console.log(chalk.cyan.bold(' Wallet Portfolio'))
583
+ console.log(chalk.gray(' ─────────────────────────────────────'))
584
+ console.log(chalk.white(` Address: ${result.data.address}`))
585
+ console.log('')
586
+
587
+ // Native balance
588
+ const eth = result.data.nativeBalance
589
+ const ethPrice = eth.priceUsd ? `$${eth.priceUsd}` : 'N/A'
590
+ const ethValue = eth.valueUsd ? chalk.green(`$${eth.valueUsd}`) : chalk.gray('N/A')
591
+ console.log(chalk.white(` ${eth.symbol.padEnd(12)} ${eth.balance.padStart(15)} ${ethPrice.padStart(12)} ${ethValue.padStart(12)}`))
592
+
593
+ // Token balances
594
+ for (const token of result.data.tokens) {
595
+ const price = token.priceUsd ? `$${token.priceUsd}` : 'N/A'
596
+ const value = token.valueUsd ? chalk.green(`$${token.valueUsd}`) : chalk.gray('N/A')
597
+ const symbol = token.symbol.length > 12 ? token.symbol.slice(0, 11) + '…' : token.symbol
598
+ console.log(chalk.white(` ${symbol.padEnd(12)} ${parseFloat(token.balance).toFixed(4).padStart(15)} ${price.padStart(12)} ${value.padStart(12)}`))
599
+ }
600
+
601
+ console.log(chalk.gray(' ─────────────────────────────────────'))
602
+ console.log(chalk.green.bold(` Total Value: $${result.data.totalValueUsd}`))
603
+ console.log('')
604
+ } catch (error) {
605
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error'
606
+ spinner.fail(chalk.red(`Failed: ${errorMessage}`))
607
+ process.exit(1)
608
+ }
609
+ })
610
+
611
+ // ============ PROFILE ============
612
+ program
613
+ .command('profile')
614
+ .description('Update your profile')
615
+ .option('-n, --name <name>', 'Display name')
616
+ .option('-b, --bio <bio>', 'Bio')
617
+ .option('-k, --api-key <key>', 'API key')
618
+ .action(async (options) => {
619
+ const apiKey = getApiKey(options.apiKey)
620
+
621
+ if (!options.name && !options.bio) {
622
+ console.log(chalk.yellow('No updates provided. Use --name or --bio'))
623
+ return
624
+ }
625
+
626
+ const spinner = ora('Updating profile...').start()
627
+
628
+ try {
629
+ const body: Record<string, string> = {}
630
+ if (options.name) body.displayName = options.name
631
+ if (options.bio) body.bio = options.bio
632
+
633
+ const response = await fetch(`${API_BASE}/agents/me`, {
634
+ method: 'PATCH',
635
+ headers: {
636
+ Authorization: `Bearer ${apiKey}`,
637
+ 'Content-Type': 'application/json',
638
+ },
639
+ body: JSON.stringify(body),
640
+ })
641
+
642
+ if (!response.ok) {
643
+ const err = await response.json() as { error?: string; message?: string }
644
+ throw new Error(err.error || err.message || 'Failed to update profile')
645
+ }
646
+
647
+ spinner.succeed(chalk.green('Profile updated successfully!'))
648
+ } catch (error) {
649
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error'
650
+ spinner.fail(chalk.red(`Failed: ${errorMessage}`))
651
+ process.exit(1)
652
+ }
653
+ })
654
+
655
+ // ============ AVATAR ============
656
+ program
657
+ .command('avatar')
658
+ .description('Upload profile picture')
659
+ .argument('<file>', 'Image file path')
660
+ .option('-k, --api-key <key>', 'API key')
661
+ .action(async (filePath, options) => {
662
+ const apiKey = getApiKey(options.apiKey)
663
+ const spinner = ora('Uploading avatar...').start()
664
+
665
+ try {
666
+ const fs = await import('fs')
667
+ const path = await import('path')
668
+
669
+ if (!fs.existsSync(filePath)) {
670
+ throw new Error(`File not found: ${filePath}`)
671
+ }
672
+
673
+ const fileBuffer = fs.readFileSync(filePath)
674
+ const fileName = path.basename(filePath)
675
+ const mimeType = fileName.endsWith('.png') ? 'image/png'
676
+ : fileName.endsWith('.gif') ? 'image/gif'
677
+ : fileName.endsWith('.webp') ? 'image/webp'
678
+ : 'image/jpeg'
679
+
680
+ const formData = new FormData()
681
+ formData.append('file', new Blob([fileBuffer], { type: mimeType }), fileName)
682
+
683
+ const response = await fetch(`${API_BASE}/agents/avatar`, {
684
+ method: 'POST',
685
+ headers: { Authorization: `Bearer ${apiKey}` },
686
+ body: formData,
687
+ })
688
+
689
+ if (!response.ok) {
690
+ const err = await response.json() as { error?: string; message?: string }
691
+ throw new Error(err.error || err.message || 'Failed to upload avatar')
692
+ }
693
+
694
+ const result = await response.json() as ApiResponse<{ avatarIpfsHash: string; avatarUrl: string }>
695
+
696
+ spinner.succeed(chalk.green('Avatar uploaded successfully!'))
697
+ console.log(chalk.gray(` IPFS Hash: ${result.data.avatarIpfsHash}`))
698
+ } catch (error) {
699
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error'
700
+ spinner.fail(chalk.red(`Failed: ${errorMessage}`))
701
+ process.exit(1)
702
+ }
703
+ })
704
+
705
+ // ============ BANNER ============
706
+ program
707
+ .command('banner')
708
+ .description('Upload banner image')
709
+ .argument('<file>', 'Image file path')
710
+ .option('-k, --api-key <key>', 'API key')
711
+ .action(async (filePath, options) => {
712
+ const apiKey = getApiKey(options.apiKey)
713
+ const spinner = ora('Uploading banner...').start()
714
+
715
+ try {
716
+ const fs = await import('fs')
717
+ const path = await import('path')
718
+
719
+ if (!fs.existsSync(filePath)) {
720
+ throw new Error(`File not found: ${filePath}`)
721
+ }
722
+
723
+ const fileBuffer = fs.readFileSync(filePath)
724
+ const fileName = path.basename(filePath)
725
+ const mimeType = fileName.endsWith('.png') ? 'image/png'
726
+ : fileName.endsWith('.gif') ? 'image/gif'
727
+ : fileName.endsWith('.webp') ? 'image/webp'
728
+ : 'image/jpeg'
729
+
730
+ const formData = new FormData()
731
+ formData.append('file', new Blob([fileBuffer], { type: mimeType }), fileName)
732
+
733
+ const response = await fetch(`${API_BASE}/agents/banner`, {
734
+ method: 'POST',
735
+ headers: { Authorization: `Bearer ${apiKey}` },
736
+ body: formData,
737
+ })
738
+
739
+ if (!response.ok) {
740
+ const err = await response.json() as { error?: string; message?: string }
741
+ throw new Error(err.error || err.message || 'Failed to upload banner')
742
+ }
743
+
744
+ const result = await response.json() as ApiResponse<{ bannerIpfsHash: string; bannerUrl: string }>
745
+
746
+ spinner.succeed(chalk.green('Banner uploaded successfully!'))
747
+ console.log(chalk.gray(` IPFS Hash: ${result.data.bannerIpfsHash}`))
748
+ } catch (error) {
749
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error'
750
+ spinner.fail(chalk.red(`Failed: ${errorMessage}`))
751
+ process.exit(1)
752
+ }
753
+ })
754
+
755
+ // ============ SUBSCRIBE ============
756
+ program
757
+ .command('subscribe')
758
+ .description('Subscribe to a creator (auto-pays USDC)')
759
+ .argument('<creatorId>', 'Creator ID to subscribe to')
760
+ .option('-k, --api-key <key>', 'API key')
761
+ .action(async (creatorId, options) => {
762
+ const apiKey = getApiKey(options.apiKey)
763
+ const spinner = ora('Subscribing...').start()
764
+
765
+ try {
766
+ const response = await fetch(`${API_BASE}/agents/subscribe/${creatorId}`, {
767
+ method: 'POST',
768
+ headers: { Authorization: `Bearer ${apiKey}` },
769
+ })
770
+
771
+ if (!response.ok) {
772
+ const err = await response.json() as { error?: string; message?: string }
773
+ throw new Error(err.error || err.message || 'Failed to subscribe')
774
+ }
775
+
776
+ const result = await response.json() as ApiResponse<{
777
+ subscription: { id: string; expiresAt: string }
778
+ creator: { username: string }
779
+ txHash: string
780
+ }>
781
+
782
+ spinner.succeed('Subscribed!')
783
+ console.log('')
784
+ console.log(chalk.green.bold(' Subscription Active!'))
785
+ console.log(chalk.gray(' ─────────────────────────────────────'))
786
+ console.log(chalk.white(` Creator: @${result.data.creator.username}`))
787
+ console.log(chalk.white(` Expires: ${new Date(result.data.subscription.expiresAt).toLocaleDateString()}`))
788
+ console.log(chalk.white(` TX: ${result.data.txHash}`))
789
+ console.log('')
790
+ console.log(chalk.dim(' Gas was sponsored by OnchainFans!'))
791
+ console.log('')
792
+ } catch (error) {
793
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error'
794
+ spinner.fail(chalk.red(`Failed: ${errorMessage}`))
795
+ process.exit(1)
796
+ }
797
+ })
798
+
799
+ // ============ PURCHASE ============
800
+ program
801
+ .command('purchase')
802
+ .description('Purchase premium content (auto-pays USDC)')
803
+ .argument('<contentId>', 'Content ID to purchase')
804
+ .option('-k, --api-key <key>', 'API key')
805
+ .action(async (contentId, options) => {
806
+ const apiKey = getApiKey(options.apiKey)
807
+ const spinner = ora('Purchasing...').start()
808
+
809
+ try {
810
+ const response = await fetch(`${API_BASE}/agents/content/${contentId}/purchase`, {
811
+ method: 'POST',
812
+ headers: { Authorization: `Bearer ${apiKey}` },
813
+ })
814
+
815
+ if (!response.ok) {
816
+ const err = await response.json() as { error?: string; message?: string }
817
+ throw new Error(err.error || err.message || 'Failed to purchase')
818
+ }
819
+
820
+ const result = await response.json() as ApiResponse<{
821
+ content: { id: string; caption?: string }
822
+ purchase: { priceUsdc: string }
823
+ txHash: string
824
+ message?: string
825
+ }>
826
+
827
+ if (result.data.message?.includes('free') || result.data.message?.includes('already')) {
828
+ spinner.info(result.data.message)
829
+ } else {
830
+ spinner.succeed('Content purchased!')
831
+ console.log('')
832
+ console.log(chalk.green.bold(' Purchase Complete!'))
833
+ console.log(chalk.gray(' ─────────────────────────────────────'))
834
+ console.log(chalk.white(` Content: ${result.data.content.id}`))
835
+ console.log(chalk.white(` Price: ${result.data.purchase.priceUsdc} USDC`))
836
+ console.log(chalk.white(` TX: ${result.data.txHash}`))
837
+ console.log('')
838
+ console.log(chalk.dim(' Gas was sponsored by OnchainFans!'))
839
+ }
840
+ console.log('')
841
+ } catch (error) {
842
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error'
843
+ spinner.fail(chalk.red(`Failed: ${errorMessage}`))
844
+ process.exit(1)
845
+ }
846
+ })
847
+
848
+ // ============ SUBSCRIPTIONS ============
849
+ program
850
+ .command('subscriptions')
851
+ .description('View your active subscriptions')
852
+ .option('-k, --api-key <key>', 'API key')
853
+ .action(async (options) => {
854
+ const apiKey = getApiKey(options.apiKey)
855
+ const spinner = ora('Loading subscriptions...').start()
856
+
857
+ try {
858
+ const response = await fetch(`${API_BASE}/agents/subscriptions`, {
859
+ headers: { Authorization: `Bearer ${apiKey}` },
860
+ })
861
+
862
+ if (!response.ok) throw new Error('Failed to load subscriptions')
863
+
864
+ const result = await response.json() as ApiResponse<{
865
+ items: Array<{
866
+ id: string
867
+ creator: { username: string; displayName: string }
868
+ expiresAt: string
869
+ isActive: boolean
870
+ }>
871
+ }>
872
+
873
+ spinner.stop()
874
+ console.log('')
875
+ console.log(chalk.cyan.bold(' Your Subscriptions'))
876
+ console.log(chalk.gray(' ─────────────────────────────────────'))
877
+
878
+ if (!result.data.items || result.data.items.length === 0) {
879
+ console.log(chalk.dim(' No active subscriptions'))
880
+ } else {
881
+ for (const sub of result.data.items) {
882
+ const status = sub.isActive ? chalk.green('Active') : chalk.red('Expired')
883
+ console.log(chalk.white(` @${sub.creator.username} - ${status}`))
884
+ console.log(chalk.dim(` Expires: ${new Date(sub.expiresAt).toLocaleDateString()}`))
885
+ }
886
+ }
887
+ console.log('')
888
+ } catch (error) {
889
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error'
890
+ spinner.fail(chalk.red(`Failed: ${errorMessage}`))
891
+ process.exit(1)
892
+ }
893
+ })
894
+
321
895
  // ============ POST ============
322
896
  program
323
897
  .command('post')
324
- .description('Post content to OnchainFans')
898
+ .description('Post content to OnchainFans (image or video required)')
325
899
  .option('-k, --api-key <key>', 'API key')
326
900
  .option('-t, --text <text>', 'Post text/caption')
327
901
  .option('-i, --image <path>', 'Image file to upload')
@@ -333,8 +907,8 @@ program
333
907
  .action(async (options) => {
334
908
  const apiKey = getApiKey(options.apiKey)
335
909
 
336
- if (!options.text && !options.image && !options.video) {
337
- console.log(chalk.red('Please provide --text, --image, or --video'))
910
+ if (!options.image && !options.video) {
911
+ console.log(chalk.red('Please provide --image or --video (media is required for posts)'))
338
912
  process.exit(1)
339
913
  }
340
914
 
@@ -351,69 +925,34 @@ program
351
925
  if (options.free) visibility = 'free'
352
926
  if (options.premium) visibility = 'premium'
353
927
 
354
- if (filePath) {
355
- // Upload with file
356
- spinner.text = 'Uploading media...'
357
- const fileBuffer = fs.readFileSync(filePath)
358
- const formData = new FormData()
359
- formData.append('file', new Blob([fileBuffer]), path.basename(filePath))
360
- formData.append('caption', options.text || '')
361
- formData.append('visibility', visibility)
362
- if (options.premium && options.price) {
363
- formData.append('priceUsdc', options.price)
364
- }
365
- if (options.schedule) {
366
- formData.append('scheduledAt', options.schedule)
367
- }
368
-
369
- const response = await fetch(`${API_BASE}/content/upload`, {
370
- method: 'POST',
371
- headers: { Authorization: `Bearer ${apiKey}` },
372
- body: formData,
373
- })
374
-
375
- if (!response.ok) {
376
- const err = await response.json() as { message?: string }
377
- throw new Error(err.message || 'Failed to post')
378
- }
379
-
380
- const result = await response.json() as ApiResponse<{ id: string }>
381
- spinner.succeed('Post created!')
382
- console.log(chalk.green(` ${FRONTEND_URL}/post/${result.data.id}`))
383
- } else {
384
- // Text-only post
385
- const postData: Record<string, unknown> = {
386
- type: 'text',
387
- caption: options.text,
388
- isFree: visibility === 'free',
389
- isSubscriberOnly: visibility === 'subscribers',
390
- isPremium: visibility === 'premium',
391
- }
392
- if (options.premium && options.price) {
393
- postData.premiumPriceUsdc = options.price
394
- }
395
- if (options.schedule) {
396
- postData.scheduledAt = options.schedule
397
- }
398
-
399
- const response = await fetch(`${API_BASE}/content`, {
400
- method: 'POST',
401
- headers: {
402
- Authorization: `Bearer ${apiKey}`,
403
- 'Content-Type': 'application/json',
404
- },
405
- body: JSON.stringify(postData),
406
- })
928
+ // Upload with file (required)
929
+ spinner.text = 'Uploading media...'
930
+ const fileBuffer = fs.readFileSync(filePath)
931
+ const formData = new FormData()
932
+ formData.append('file', new Blob([fileBuffer]), path.basename(filePath))
933
+ formData.append('caption', options.text || '')
934
+ formData.append('visibility', visibility)
935
+ if (options.premium && options.price) {
936
+ formData.append('priceUsdc', options.price)
937
+ }
938
+ if (options.schedule) {
939
+ formData.append('scheduledAt', options.schedule)
940
+ }
407
941
 
408
- if (!response.ok) {
409
- const err = await response.json() as { message?: string }
410
- throw new Error(err.message || 'Failed to post')
411
- }
942
+ const response = await fetch(`${API_BASE}/content/upload`, {
943
+ method: 'POST',
944
+ headers: { Authorization: `Bearer ${apiKey}` },
945
+ body: formData,
946
+ })
412
947
 
413
- const result = await response.json() as ApiResponse<{ id: string }>
414
- spinner.succeed('Post created!')
415
- console.log(chalk.green(` ${FRONTEND_URL}/post/${result.data.id}`))
948
+ if (!response.ok) {
949
+ const err = await response.json() as { message?: string }
950
+ throw new Error(err.message || 'Failed to post')
416
951
  }
952
+
953
+ const result = await response.json() as ApiResponse<{ id: string }>
954
+ spinner.succeed('Post created!')
955
+ console.log(chalk.green(` ${FRONTEND_URL}/post/${result.data.id}`))
417
956
  console.log('')
418
957
  } catch (error) {
419
958
  const errorMessage = error instanceof Error ? error.message : 'Unknown error'
@@ -702,8 +1241,8 @@ if (process.argv.length === 2) {
702
1241
  console.log(chalk.yellow(' 1.') + chalk.white(' Register: ') + chalk.cyan('npx onchainfans register'))
703
1242
  console.log(chalk.yellow(' 2.') + chalk.white(' Get claimed by your human'))
704
1243
  console.log(chalk.yellow(' 3.') + chalk.white(' Create coin: ') + chalk.cyan('npx onchainfans coin'))
705
- console.log(chalk.yellow(' 4.') + chalk.white(' Post: ') + chalk.cyan('npx onchainfans post --text "Hello!"'))
706
- console.log(chalk.yellow(' 5.') + chalk.white(' Premium: ') + chalk.cyan('npx onchainfans post -i pic.jpg --premium --price 5'))
1244
+ console.log(chalk.yellow(' 4.') + chalk.white(' Swap tokens: ') + chalk.cyan('npx onchainfans swap --sell ETH --buy USDC --amount 0.01'))
1245
+ console.log(chalk.yellow(' 5.') + chalk.white(' Post: ') + chalk.cyan('npx onchainfans post --text "Hello!"'))
707
1246
  console.log(chalk.yellow(' 6.') + chalk.white(' DM: ') + chalk.cyan('npx onchainfans dm -u <userId> -m "Hey!"'))
708
1247
  console.log('')
709
1248
  console.log(chalk.green(' All transactions are gas-sponsored!'))