---
title: "Amazon pay 串接筆記"
description: "詳細記錄 Amazon Pay 串接流程，包含金鑰產生、前端 React 按鈕整合、後端 C# SDK 初始化、取得付款資訊、建立授權訂單與請款實作。"
canonical_url: "https://blog.markkulab.net/post/integrate-amazonpay"
author: "Mark Ku"
author_url: "https://blog.markkulab.net/author/mark-ku"
site: "Mark Ku's Blog"
date_published: "2024-03-08 01:01:35 +0800"
category: "Payment"
tags: ["amazon pay", "payment", "checkout", "react", "csharp", "integration", "ecommerce"]
language: "zh-TW"
license: "CC BY 4.0"
license_url: "https://creativecommons.org/licenses/by/4.0/"
attribution: "轉載或引用請註明作者並附上原文連結"
---

# Amazon pay 串接筆記

## 時空背景
這是我第二次接手 Amaozn pay，上次是因為剛加入公司時，幫忙處理Amazon pay 一直瘋狂掉單，這次重新接 Amazon pay 是為了德國的電商網站，藉由此次重新整理了一下界接的技巧。

## 相關界接文件
* [界接文件](https://developer.amazon.com/docs/amazon-pay-checkout/get-set-up-for-integration.html)
* [如何產生公私鑰](https://sellercentral.amazon.de/external-payments/amazon-pay/integration-central/lwa?ref=py_clientid_confcard_sboxhome_GB)

## 界接流程圖
Amazon pay checkout 其實就很像 payapal express 的 checkout 
![Amazonpay integration flow](https://blog.markkulab.net/content/markku/posts/integrate-amazonpay/images/amazonpay-integration-flow.png)

## 首先，取得Amazon pay 相關秘鑰
### 接著，進入後台 > 選擇Sandbox View > [Integration central](https://sellercentral.amazon.de/gp/pyop/seller/integrationcentral?ref=py_intcentr_confcard_sboxhome_GB) 

![Find Integration central](https://blog.markkulab.net/content/markku/posts/integrate-amazonpay/images/find-integration-central.png)

### 因為我有兩個國家的帳號，對照之後發現這邊有個bug，有個帳號會找不到Self-Developed，因此無法顯示Create keys按鈕(上傳 public key)功能
![Integration central - 1](https://blog.markkulab.net/content/markku/posts/integrate-amazonpay/images/integration-central-1.png)
如果你沒有Self-developed 下拉選項，此時可以選擇 Woo Commerce
![Integration central - 2](https://blog.markkulab.net/content/markku/posts/integrate-amazonpay/images/integration-central-2.png)

### 依據官方文件[Doc](https://developer.amazon.com/docs/amazon-pay-api-v2/manually-generating-key-pairs.html#generating-key-pair) , 產生 rsa 公鑰與私鑰

```
ssh-keygen -t rsa -b 2048 -m PKCS8 -f privateKey.pem
ssh-keygen -f privateKey.pem -e -m PKCS8 > publicKey.pub
```       

### 因為第一個選項功能應該是壞了，只會產生公鑰，而無法下載私鑰，請選擇第二個選項，請自行上傳 Amazon pay 公鑰，

![Upload public key](https://blog.markkulab.net/content/markku/posts/integrate-amazonpay/images/upload-public-key.png)
### 此時在這畫面，你就能找到大部份所需要的金鑰
![All key in here](https://blog.markkulab.net/content/markku/posts/integrate-amazonpay/images/keys.png)

### 但Store Id 和 Client secret 你需要在另外一個[頁面](https://sellercentral.amazon.de/external-payments/amazon-pay/integration-central/lwa?ref=py_clientid_confcard_sboxhome_GB)去查詢
![store id and secret](https://blog.markkulab.net/content/markku/posts/integrate-amazonpay/images/store-id-and-secret.png)

## 測試注意事項
* 因為Amazon pay付款需要導頁來導頁去，需要有https，也要有自己的domain，我這邊是透過Cloudfalre tunnel 當反向代理測試
* 正式和測試的金鑰一樣其實蠻方便的，要使用 Sandbox 環境 只要初始化時將 isSandbox 設定成true 即可
* 關於測試帳號部份，進入後台切到Sandbox view 就能找到 [Test accunts](https://sellercentral.amazon.de/external-payments/sandbox/home)，這邊就可以增加sandbox 的測試帳號。

## 前端範例程式 - React (Next js App route)
```
'use client';
import { PaymentReturnType } from '@/const/cart/payment-image';
import { PaymentOptions } from '@/const/payment/payment-option';
import {
    useBuildPaymentInfoMutation,
    useCaptureMutation,
    useLazyGetConfigQuery,
    useProcessMutation,
} from '@/redux/api/test-payment-apiSlice';
import { IGeneralPaymentInfo, IGeneralPaymentResult, IGernalPaymentParams } from '@/typing/cart';
import { ApiResponse } from '@/typing/common';
import { useSearchParams } from 'next/navigation';
import { useEffect, useRef } from 'react';
import { v4 as uuidv4 } from 'uuid';

export default function TestAmaonPay() {
    const gernalPaymentParams: IGernalPaymentParams = {
        paymentTypeCode: PaymentOptions[PaymentOptions.Paypal],
        orderNo: uuidv4(),
    } as IGernalPaymentParams;

    const [GetConfig, result] = useLazyGetConfigQuery(); // rtk query 取得sdk 初始化的參數

    const [Process] = useProcessMutation(); // rtk query 用來呼叫建立建立授權訂單的api

    const [Capture] = useCaptureMutation(); // rtk query 用來呼叫提取信用及完成訂單的api
    const [BuildPaymentInfoMutation] = useBuildPaymentInfoMutation(); // rtk query 用來取得Amazon Pay 付款資訊

    const amazonCheckoutSessionId = useRef('');
    const amazonHasRedirectedBack = useRef(false);

    const searchParams = useSearchParams();

    const loadAmazonPay = () => {
        const script = document.createElement('script');

        script.src = 'https://static-eu.payments-amazon.com/checkout.js';
        script.async = true;
        script.onload = () => {
            // debugger;
            if (window.amazon && window.amazon.Pay) {
                GetConfig(gernalPaymentParams.paymentTypeCode)
                    .unwrap()
                    .then((data: any) => {
                        if (!data) {
                            return;
                        }
                        window.amazon.Pay.renderButton('#amazonpaybutton', {
                            merchantId: data.merchantId,
                            sandbox: data.isSandbox === 'True', // dev environment
                            ledgerCurrency: data.ledgerCurrency, // Amazon Pay account ledger currency
                            checkoutLanguage: 'en_GB', // render language
                            productType: 'PayAndShip', // checkout type
                            placement: 'Cart', // button placement
                            buttonColor: 'Gold',
                            createCheckoutSessionConfig: {
                                payloadJSON: data.payloadJSON,
                                signature: data.signature,
                                publicKeyId: data.publicKeyId,
                            },
                        });
                    });
            }
        };
        document.body.appendChild(script);
    };

    const getPaymentInfoFromToken = (token: string) => {
        const params: IGernalPaymentParams = { ...gernalPaymentParams, paymentGatewaySessionID: token };

        BuildPaymentInfoMutation(params)
            .unwrap()
            .then((res: ApiResponse<IGeneralPaymentInfo>) => {
                document.getElementById('paymentInfo')!.innerText = JSON.stringify(res.data);
            });
    };

    useEffect(() => {
        // Dynamically load the Amazon Pay script
        loadAmazonPay();
        const session = searchParams?.get('amazonCheckoutSessionId') || '';

        const isComplete = searchParams?.get('isComplete') || '';

        if (session) {
            amazonCheckoutSessionId.current = session;
            amazonHasRedirectedBack.current = true;

            if (!isComplete) {
                getPaymentInfoFromToken(session);
            } else {
                const params: IGernalPaymentParams = {
                    ...gernalPaymentParams,
                    paymentGatewaySessionID: session,
                };

                Capture(params)
                    .unwrap()
                    .then((res: ApiResponse<IGeneralPaymentResult>) => {
                        if (res.isSuccess) {
                            alert('Payment success');
                        }
                    });
            }
        }
    }, []); // Empty dependency array means this effect runs once on mount

    const clickHandler = () => {
        const params: IGernalPaymentParams = {
            ...gernalPaymentParams,
            paymentGatewaySessionID: amazonCheckoutSessionId.current,
        };

        Process(params)
            .unwrap()
            .then((res: ApiResponse<IGeneralPaymentResult>) => {
                // debugger;
                if (res.isSuccess && res.data.paymentReturnType === PaymentReturnType.RedirectUrl) {
                    location.href = res.data.paymentReturnValue;
                }
            });
    };

    return (
        <div>
            <h1>test amazon pay</h1>
            <div id="amazonpaybutton"></div>
            <div id="paymentInfo"></div>

            <button
                className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
                onClick={clickHandler}
            >
                Checkout
            </button>
        </div>
    );
}
```

## 後端程式範例 - C#
### 首先，Nuget安裝 Amazon.Pay.API.SDK

### 定義後端設定檔 - Appsetting.json
```
"Payment": {
    "PaymentOptions": [
 {
   "PaymentName": "AmazonPay",
   "ClientId": "Your clientId", // live and sandbox are the same
   "Secret": "Your clientId secret", // live and sandbox are the same
   "IsSandbox": true,
   "StoreID": "",
   "StoreName": "Your storename",
   "MerchantID": "Your MerchantID", // from amazon 
   "PublicKeyID": "Upload to amazon backoffice PublicKeyID", // 將公鑰上傳amazon pay 產上的ID
   "PrivateKey": "Your private key", // 透過電腦自己產的公鑰，base64 加密起來就不用額外檔案
   "EndPoint": "https://api.paypal.com"
  }
 ]
 }
```

### 初始化 Amazon SDK 的 WebStoreClient，因為後面很常使用到，所以抽成一個function 
```
 private WebStoreClient InitiateClient()
 {
    var payConfiguration = new ApiConfiguration
    (
        region: Region.Europe,
        environment: GeneralPaymentConfig.IsSandbox ? Amazon.Pay.API.Types.Environment.Sandbox : Amazon.Pay.API.Types.Environment.Live,
        publicKeyId: GeneralPaymentConfig.PublicKeyID,
        privateKey: System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(GeneralPaymentConfig.PrivateKey))
    );

    var client = new WebStoreClient(payConfiguration);

    return client;
 }
```

### 從後端取回init Amazon button 所需要的參數
```
 public async Task<Result<Dictionary<string, string>>> GetConfig(string returnUrl = "")
 {
    var req = HttpContext.Current.Request;
    var result = new Result<Dictionary<string, string>> { IsSuccess = false, Message = "" };
    try
    {
       if (string.IsNullOrWhiteSpace(returnUrl))
       {
          returnUrl = "/en/test-payment/";
       }

       string ChangePaymentReferID = string.Empty;
       var isChangePayment = !string.IsNullOrEmpty(ChangePaymentReferID);  // for change payment

       var client = InitiateClient();
       var request = new CreateCheckoutSessionRequest(
           checkoutReviewReturnUrl: AppSettingsConstVars.FrontendUrl + returnUrl + (isChangePayment ? $"/payments?id={ChangePaymentReferID}" : ""),
           storeId: GeneralPaymentConfig.StoreID
       );
       request.PaymentDetails.CanHandlePendingAuthorization = false;
       request.DeliverySpecifications.AddressRestrictions.Type = RestrictionType.Allowed;
       request.DeliverySpecifications.AddressRestrictions.AddCountryRestriction("DE");

       //generate the button signature
       var signature = client.GenerateButtonSignature(request);
       var payload = request.ToJson();

       result.Data.Add("signature", signature);
       result.Data.Add("payloadJSON", payload);
       result.Data.Add("publicKeyId", GeneralPaymentConfig.PublicKeyID);
       result.Data.Add("merchantId", GeneralPaymentConfig.MerchantID);
       result.Data.Add("isSandbox", GeneralPaymentConfig.IsSandbox.ToString()); // need test 
       result.Data.Add("ledgerCurrency", HardCodeKey.BasedCurrency);

       result.Success();
    }
    catch (Exception ex)
    {
       NLogUtil.WriteSEQLog($"[Paypal][GetConfig]Error:{ex.Message},StackTrace:{ex.StackTrace}", NLog.LogLevel.Error);
    }

    return result;
 }
```


### 接著實作取回付款地址
```
 public async Task<Result<GeneralPaymentInfo>> GetPaymentInfoAsync(GernalPaymentParameter paymentParameter)
 {
    Result<GeneralPaymentInfo> result = new Result<GeneralPaymentInfo> { IsSuccess = false, Message = "" };

    try
    {
       var client = InitiateClient();
       var getInfo = client.GetCheckoutSession(paymentParameter.PaymentGatewaySessionID);

       if (getInfo == null)
       {
          throw new Exception($"[AmazonPayService][GetPaymentInfoAsync]:{getInfo.RawResponse}");
       }

       result.Data.Address = ConvertToGeneralAddress(getInfo.ShippingAddress, getInfo.Buyer.Email);

       result.Success();
    }
    catch (Exception ex)
    {
       NLogUtil.WriteSEQLog($"[Paypal][GetPaymentInfoAsync]Error:{ex.Message},StackTrace:{ex.StackTrace}", NLog.LogLevel.Error);
    }

    return result;
 }
 
 private GeneralAddress ConvertToGeneralAddress(Address amazonShipAddress, string Mail)
{
   GeneralAddress generalAddress = new GeneralAddress();
   generalAddress.Line1 = amazonShipAddress.AddressLine1;
   generalAddress.Line2 = (string.IsNullOrEmpty(amazonShipAddress.AddressLine2) && string.IsNullOrEmpty(amazonShipAddress.AddressLine3)) ? "" : string.Join(",", amazonShipAddress.AddressLine2, amazonShipAddress.AddressLine3);
   generalAddress.FirstName = GetMaxLengthStr(ParseName(amazonShipAddress.Name).Item1, 80);
   generalAddress.LastName = GetMaxLengthStr(ParseName(amazonShipAddress.Name).Item2, 80);
   generalAddress.CountryCode = amazonShipAddress.CountryCode;
   generalAddress.PostalCode = amazonShipAddress.PostalCode;
   generalAddress.City = amazonShipAddress.City;
   generalAddress.StateProvinceCode = amazonShipAddress.StateOrRegion;
   generalAddress.Phone = amazonShipAddress.PhoneNumber;
   return generalAddress;
}

private string GetMaxLengthStr(string Text, int Max)
{
   if (string.IsNullOrWhiteSpace(Text))
   {
      return "";
   }

   return Text.Substring(0, Math.Min(Max, Text.Length));
}

private Tuple<string, string> ParseName(string Name)
{
   if (string.IsNullOrWhiteSpace(Name))
   {
      return Tuple.Create("", "");
   }

   var names = Name.Split(' ');

   if (names.Length == 1)
   {
      return Tuple.Create(Name, "");
   }

   var last = names.Last();

   return Tuple.Create(Name.Replace(last, "").Trim(), last);
} 
```
### 取得 Amazon 交易頁面 Url( PurchaseGetAmazonPayRedirectUrl )
```
 public async Task<Result<GeneralPaymentResult>> ProcessAsync(GernalPaymentParameter paymentParameter, string returnUrl)
 {
    Result<GeneralPaymentResult> result = new Result<GeneralPaymentResult> { IsSuccess = false, Message = "Please select another credit card or change payment method and try again." };

    try
    {
       var client = InitiateClient();
       var request = new UpdateCheckoutSessionRequest();

       if (string.IsNullOrWhiteSpace(returnUrl))
       {
          returnUrl = "/en/test-payment/?isComplete=true";
       }

       request.WebCheckoutDetails.CheckoutResultReturnUrl = AppSettingsConstVars.FrontendUrl + returnUrl;

       var currencyCode = (Currency)Enum.Parse(typeof(Currency), HardCodeKey.BasedCurrency);

       request.PaymentDetails.ChargeAmount.Amount = HardCodeKey.Payment.TestAmount;
       request.PaymentDetails.ChargeAmount.CurrencyCode = currencyCode;
       request.PaymentDetails.CanHandlePendingAuthorization = false;
       request.PaymentDetails.PaymentIntent = Authorize ? PaymentIntent.Authorize : PaymentIntent.AuthorizeWithCapture;

       NLogUtil.WriteSEQLog($"[AmazonPay][AmazonV2Helper][PurchaseGetAmazonPayRedirectUrl][UpdateCheckoutSession][Authorize]:{Authorize}", NLog.LogLevel.Info);
       request.MerchantMetadata.MerchantReferenceId = paymentParameter.OrderNo;

       request.MerchantMetadata.MerchantStoreName = GeneralPaymentConfig.StoreName;
       request.MerchantMetadata.NoteToBuyer = GetProductDescription();

       var updateResult = client.UpdateCheckoutSession(paymentParameter.PaymentGatewaySessionID, request);

       if (updateResult.Success)
       {
          result.Data.PaymentReturnType = PaymentReturnType.RedirectUrl;
          result.Data.PaymentReturnValue = updateResult.WebCheckoutDetails.AmazonPayRedirectUrl;
          result.Success();

          // TODO Remove this log when stable
          NLogUtil.WriteSEQLog($"[AmazonPayService][ProcessAsync][UpdateCheckoutSession][Success]{JsonConvert.SerializeObject(updateResult)}", NLog.LogLevel.Info);
       }
       else
       {
          NLogUtil.WriteSEQLog($"[AmazonPayService][ProcessAsync][UpdateCheckoutSession][Error]Request:{JsonConvert.SerializeObject(request)},Response:{JsonConvert.SerializeObject(updateResult)}", NLog.LogLevel.Error);
       }
    }
    catch (Exception ex)
    {
       NLogUtil.WriteSEQLog($"[Paypal][ProcessAsync]Error:{ex.Message},StackTrace:{ex.StackTrace}", NLog.LogLevel.Error);
    }

    return result;
 }
```
### 完成訂單及請款(Capture)
```
 public virtual async Task<Result<GeneralPaymentResult>> CaptureAsync(GernalPaymentParameter paymentParameter)
 {
    var result = new Result<GeneralPaymentResult> { IsSuccess = true, Message = "No need implemented" };

    try
    {
       var currencyCode = (Currency)Enum.Parse(typeof(Currency), HardCodeKey.BasedCurrency);
       var client = InitiateClient();
       var request = new CompleteCheckoutSessionRequest(HardCodeKey.Payment.TestAmount, currencyCode);
       CheckoutSessionResponse complete = client.CompleteCheckoutSession(paymentParameter.PaymentGatewaySessionID, request);

       if (complete.Success)
       {
          var paymentTransition = new GeneralPaymentTransition();
          paymentTransition.Authcode = complete.ChargeId;
          paymentTransition.TransactionID = complete.ChargeId;
          paymentTransition.Amount = HardCodeKey.Payment.TestAmount.ToString(); // todo
          paymentParameter.PaymentTypeCode = GeneralPaymentConfig.PaymentName;
          result.Data.PaymentTransition = paymentTransition;
          NLogUtil.WriteSEQLog($"[Paypal][CaptureAsync][Complete][Success]complete:{JsonConvert.SerializeObject(complete)}", NLog.LogLevel.Info);
          result.Success();
       }
       else
       {
          NLogUtil.WriteSEQLog($"[Paypal][CaptureAsync][Complete][Fail]complete:{JsonConvert.SerializeObject(complete)}", NLog.LogLevel.Error);
       }            
    }
    catch (Exception ex)
    {
       NLogUtil.WriteSEQLog($"[Paypal][CaptureAsync]Error:{ex.Message},StackTrace:{ex.StackTrace}", NLog.LogLevel.Error);
    }

    return result;
 }
```
## 最後
Amazon pay 界接方式真的蠻復古的，導頁來導頁去，而且後台常常會有功能改壞，接了兩次，其實不是很好界接。

---

## 關於本文與作者

本文出自 [Mark Ku's Blog](https://blog.markkulab.net/post/integrate-amazonpay)

授權條款： [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) — 轉載或引用請註明作者並附上原文連結

### 關於作者

**[Mark Ku](https://blog.markkulab.net/author/mark-ku)** — Software Solution Provider

- 10+ 年資深軟體工程師，現為 AI 應用 Builder
- 專注大型平台架構設計，從北美電商到AI SaaS訂閱收費系統
- 結合 AI Agent 與自動化，打造高效可演進的產品技術基礎

### 作者開發的免費工具

以下工具皆可免費使用：

- [免費 PDF 簽名工具](https://blog.markkulab.net/tools/pdf-sign): 線上 PDF 簽名工具，瀏覽器內完成手繪、打字、上傳簽名，可拖曳放置、縮放、下載。所有處理都在你的裝置完成，檔案不會上傳。
- [VS Code Refactory](https://blog.markkulab.net/tools/refactory): Refactory 是一款 VS Code 重構擴充套件：34 個重構動作、37 條 code smell 檢查、Code Health 儀表板、18 種語言、534 支測試。懂你的專案慣例：介面放哪、DI 註冊寫在哪、'use client' 該不該加；還會用 git 修改頻率 × 複雜度排出「該先修哪個檔案」，並一鍵把壞味道交給你自己電腦上的 Claude Code 修。免費使用，原始碼不離開你的機器。
- [DB-Kit 資料庫管理工具](https://blog.markkulab.net/tools/db-kit): DB-Kit 是一個用 Tauri + Rust + React 打造的輕量跨平台資料庫管理工具，用單一一致的介面同時管理 MySQL、MariaDB、PostgreSQL、SQL Server、Oracle、SQLite、MongoDB、Redis、Kafka、Elasticsearch 與 RabbitMQ 十一種資料來源：連線密碼以 OS keychain 加密、SSH Tunnel、完整 CRUD、視覺化查詢建構器、多結果集同時顯示、跨連線資料傳輸與比對同步、Excel / CSV 匯入匯出、執行計畫視覺化、ER 圖、排程備份、SQL 壓力測試（p50～p99 延遲百分位）、15 條規則的 SQL 審查、Kafka 訊息瀏覽與監控告警；繁中 / 英文雙語介面，內建 AI 助手（自然語言生成 SQL、AI 審查與調校建議）與命令列工具 dbk。免費開源（MIT），提供 Windows / macOS / Linux 安裝檔。
- [VS Code Super Mermaid](https://blog.markkulab.net/tools/super-mermaid): Super Mermaid 是一款 VS Code 擴充套件：開箱即用的漂亮 Mermaid 圖表，自動上色、即時預覽、滑鼠平移縮放、PNG / SVG 高解析匯出，內建 21 種範本與多種主題。免費開源（MIT）。
- [React Super Mermaid](https://blog.markkulab.net/tools/react-super-mermaid): react-super-mermaid 是一個開源 React 元件庫：一行 <MermaidViewer> 即可渲染漂亮的 Mermaid 圖表，內建 colorful / sketch 主題、平移縮放、圖內搜尋、SVG / PNG 高解析匯出。輕量、SSR 安全、完整 TypeScript 型別。免費開源（MIT）。
- [Jira / Confluence Super Mermaid](https://blog.markkulab.net/tools/jira-super-mermaid): Atlassian Forge app：在 Jira issue 與 Confluence 內文直接寫 Mermaid 語法，畫流程圖、時序圖、狀態機與甘特圖。11 種圖表、SVG / PNG 匯出、明暗主題、完整中日韓文字支援。取得 Runs on Atlassian 資格：圖表存在你自己的站台，app 不呼叫任何第三方服務。免費，即將上架 Atlassian Marketplace。
- [Mermaid 線上預覽](https://blog.markkulab.net/tools/mermaid-preview): 在瀏覽器裡寫 Mermaid、即時看圖，整張圖表壓進網址就能分享。免註冊、不上傳伺服器，相容 mermaid.live 的分享連結。
- [React Intl Phone Number](https://blog.markkulab.net/tools/react-intl-phone-number): react-intl-phone-number 是一個開源 React 元件：framework-agnostic、不依賴 antd，提供 E.164 進出、可搜尋國旗 / 國碼下拉、可配置驗證等級（strict / mobile-strict / loose）、可主題化 CSS 與 i18n，電話邏輯由 google-libphonenumber 驅動。輕量、完整 TypeScript 型別。免費開源（MIT）。
- [Uptime Kuma Cluster](https://blog.markkulab.net/tools/uptime-kuma-cluster): 把單機版 Uptime Kuma 改造成高可用叢集：OpenResty + Lua 智慧負載平衡、MariaDB 共享狀態、健康檢查與自動 Failover，附叢集管理 REST API，一行 Docker Compose 啟動。免費開源（MIT）。
- [特教專案](https://blog.markkulab.net/education): 為特殊教育學生製作的學習教材

### 每日 Podcast

- [科技新鮮事](https://blog.markkulab.net/category/tech-news): 每日精選 AI 與科技趨勢，透過語音摘要快速掌握最新技術動態，涵蓋 AI 應用、軟體架構、DevOps 與工程實戰。 — RSS: https://blog.markkulab.net/feed.xml
- [AI股市蝦聊](https://blog.markkulab.net/category/ai-stock-chat): 每個交易日用 AI 分析台股盤勢，以雙人對話聊當天的盤中觀察與隔日預測。 — RSS: https://blog.markkulab.net/ai-stock-chat/feed.xml

### 電子報

[訂閱電子報](https://blog.markkulab.net/subscribe) — 第一時間收到新文章通知，無垃圾信、隨時可取消訂閱。
