这篇教程带你给 Dots 智能体单独开一个“会花钱”的钱包:绑定支付方式、设置单笔与日限额、配置允许交易的商户白名单,最后让 Dots 实际完成一笔代付,并把账单核对清楚、把退款路径走通。
这篇能做出什么(成果先行)
跟着做下来,你会得到四样东西:
1. 一个隔离的智能体钱包:和你的个人账户分开,出问题可以单独冻结,不影响其他业务。
2. 一套能限制支出的规则:单笔上限 200 元、每日累计 1000 元、每天最多 20 笔,超出直接拒付而不是等人审批。
3. 一份商户白名单:只允许在物流、SaaS 订阅等指定商户扣款,其余一律拦下。
4. 一条完整的闭环:Dots 发起一笔代付 → 支付成功 → 拉流水对账 → 发起部分退款 → 确认账目回到初始状态。
一个具体场景:Dots 在客服会话里帮用户补寄快递,需要代付 39 元运费。这笔钱不能刷到公司主账户上,也不能让它哪天被诱导着去买别的东西,于是走智能体钱包 + 白名单 + 限额。
前置条件清单
动手前先确认这些,缺一项后面就会卡住:
- 钱包平台的账号与权限:需要能创建钱包、绑定支付方式、配置消费策略的管理员权限。普通成员账号通常只能看不能改。
- 一个沙箱或测试环境:先用测试卡号跑通全流程,再切生产。测试环境的接口地址、密钥、商户编号和生产是两套,别混用。
- 可用的支付方式:企业卡或对公账户,确认它支持平台要求的验证方式(小额打款验证、3DS 验证等)。
- Dots 的运行环境:能改工具(tool)配置、能读到环境变量、能发 HTTPS 请求。本地跑也行。
- 三个环境变量:
WALLET_API_BASE、WALLET_API_KEY、WALLET_ID。密钥只放环境变量或密钥管理服务,不要写进代码仓库。 - 一条内部审批口径:超过多少金额需要人工确认、谁负责看每日流水。技术上能设限额,制度上也要有人盯。
```bash
建议写进 .env,且 .env 加入 .gitignore
export WALLET_API_BASE="https://<钱包平台接口域名>"
export WALLET_API_KEY="<你的 API Key>"
export WALLET_ID="<创建后返回的钱包 ID>"
```
接口路径、字段名、金额单位会随平台版本变化,以官方文档当前版本为准。下面的请求体按常见命名书写,照着改字段名即可。
第一步:先把花钱规则写成一张表
不要一上来就调接口。先想清楚“允许花多少、花给谁”,因为限额和名单是要提交给钱包平台的配置,改起来比写代码慢。
| 场景 | 单笔上限 | 每日上限 | 每日笔数 | 允许商户 |
|---|---|---|---|---|
| 客服补寄运费 | 50 元 | 300 元 | 10 | 物流服务商 |
| 工具订阅续费 | 200 元 | 200 元 | 2 | 指定 SaaS 商户 |
| 其他 | 全部拒绝 | — | — | 空 |
把“其他”写成全部拒绝,是这套方案能成立的关键:白名单模式下,未列出的商户默认拦截,而不是默认放行。
第二步:创建钱包
新建一个专供 Dots 使用的钱包,名称里带上用途和负责人,方便日后审计。
```bash
curl -sS -X POST "$WALLET_API_BASE/v1/wallets" \
-H "Authorization: Bearer $WALLET_API_KEY" \
-H "Content-Type: application/json" \
-H "AI 词典:Idempotency">Idempotency-Key: create-wallet-dots-$(date +%Y%m%d)" \
-d '{
"name": "dots-agent-ops",
"type": "agent",
"owner": "team-ops",
"currency": "CNY",
"metadata": {
"agent": "dots",
"purpose": "customer-service-reimbursement"
}
}'
```
返回里会带钱包 ID 和状态。状态要等到 active 才能收付款,刚创建时可能是 pending,需要做一次身份或主体信息确认。
```bash
查询钱包状态
curl -sS "$WALLET_API_BASE/v1/wallets/$WALLET_ID" \
-H "Authorization: Bearer $WALLET_API_KEY" | jq '.status, .balance'
```
第三步:绑定支付方式
钱包是“壳”,真正出钱的是绑定的支付方式。绑卡通常分两步:提交卡信息拿到一个临时 token,再用 token 完成绑定。
```bash
1) 生成支付方式 token(在服务端调用,不要把卡号传进前端或智能体)
curl -sS -X POST "$WALLET_API_BASE/v1/payment-methods/tokens" \
-H "Authorization: Bearer $WALLET_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"type": "card",
"card": {
"number": "<卡号>",
"exp_month": 12,
"exp_year": 2030,
"cvc": "<安全码>",
"holder_name": "<持卡人>"
}
}'
2) 用 token 绑定到钱包
curl -sS -X POST "$WALLET_API_BASE/v1/wallets/$WALLET_ID/payment-methods" \
-H "Authorization: Bearer $WALLET_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"token": "<上一步返回的 token>",
"set_default": true
}'
```
对公账户走的是另一条路:提交开户信息后,平台会打一笔小额到账让你回填金额,验证完成后才能用。这一步的到账时间不可控,别把它排在发布前一天做。
绑定完成后,平台一般会做一次授权检查(冻结一小笔再释放)。看到这笔记录不要慌,它不是扣款。
第四步:设置单笔与日限额
限额挂在钱包上,智能体侧改不了,这是它比“在提示词里写‘不要超过 200 元’”可靠的地方。
```bash
curl -sS -X PUT "$WALLET_API_BASE/v1/wallets/$WALLET_ID/limits" \
-H "Authorization: Bearer $WALLET_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"currency": "CNY",
"limits": {
"per_transaction_max": 20000,
"per_transaction_min": 100,
"daily_max": 100000,
"daily_txn_count_max": 20,
"monthly_max": 2000000
},
"on_exceed": "reject"
}'
```
几点说明:
- 金额单位:上面的数字按“分”写。如果平台用元,要整体除以 100。这是最容易出错的地方,先在沙箱下 1 元试一笔确认单位。
on_exceed:设成reject直接拒付;也有平台支持review转人工。做自动化流程建议先reject,跑稳了再对小额场景放宽。- 日限额的“日”:按平台所在时区还是本地时区?跨零点前后下单会看到“明明没花超却被拒”,先问清楚。
- 降额立即生效,升额可能需要复核,所以初始额度别设太低,免得半夜改不动。
第五步:配置商户白名单
白名单两种写法:按商户 ID,或按商户类别码(MCC)。前者精确,后者覆盖广但粗。建议以商户 ID 为主,MCC 作为兜底约束。
```bash
curl -sS -X PUT "$WALLET_API_BASE/v1/wallets/$WALLET_ID/merchant-policy" \
-H "Authorization: Bearer $WALLET_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"mode": "allowlist",
"allow": {
"merchant_ids": ["mch_logistics_demo", "mch_saas_demo"],
"mcc": ["4215", "7372"]
},
"deny": {
"merchant_ids": [],
"countries": ["**"]
},
"require_verified_merchant": true,
"block_cross_border": true
}'
```
要点:
mode必须是allowlist。默认模式常常是open,不显式改就等于没设。- 商户 ID 要拿真实的,从你已签约的商户后台取,不要照抄教程里的示例值。
- 开启
block_cross_border,能挡掉大部分套现和误操作。 - 白名单变更后,已经在途的授权不受影响,只有新交易按新规则走。改完最好等一分钟再测。
第六步:把钱包接进 Dots
Dots 通过工具调用来花钱。给它一个明确的工具定义,参数越窄,出错越少。不要暴露通用的“调用任意接口”能力。
```json
{
"name": "wallet_pay",
"description": "为当前用户代付一笔费用。仅在用户明确同意金额后调用。",
"parameters": {
"type": "object",
"properties": {
"amount": { "type": "integer", "description": "金额,单位为分" },
"currency": { "type": "string", "enum": ["CNY"] },
"merchant_id": { "type": "string", "description": "商户 ID,必须来自白名单" },
"order_id": { "type": "string", "description": "内部订单号,用于对账" },
"reason": { "type": "string", "description": "代付原因,写入备注" }
},
"required": ["amount", "currency", "merchant_id", "order_id", "reason"]
}
}
```
对应的执行函数(Python 示例):
```python
import os
import uuid
import requests
def wallet_pay(amount: int, currency: str, merchant_id: str,
order_id: str, reason: str) -> dict:
resp = requests.post(
f"{os.environ['WALLET_API_BASE']}/v1/wallets/{os.environ['WALLET_ID']}/payments",
headers={
"Authorization": f"Bearer {os.environ['WALLET_API_KEY']}",
"Content-Type": "application/json",
幂等键由业务单号派生,重试不会重复扣款
"Idempotency-Key": f"pay-{order_id}",
},
json={
"amount": amount,
"currency": currency,
"merchant_id": merchant_id,
"reference": order_id,
"description": reason,
"capture": True,
},
timeout=15,
)
resp.raise_for_status()
return resp.json()
```
Idempotency-Key 由 order_id 派生,是这一步最重要的设计:Dots 重试、网络超时重发、用户连点两次,都不会变成两笔扣款。
第七步:跑通第一笔代付
先在沙箱里用一笔小金额试。
```bash
curl -sS -X POST "$WALLET_API_BASE/v1/wallets/$WALLET_ID/payments" \
-H "Authorization: Bearer $WALLET_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: pay-order-20240501-0001" \
-d '{
"amount": 3900,
"currency": "CNY",
"merchant_id": "mch_logistics_demo",
"reference": "order-20240501-0001",
"description": "客服补寄运费",
"capture": true
}'
```
预期返回里能看到 status: succeeded(有的平台先返回 processing,靠 webhook 通知最终状态)和平台侧的 payment_id。
立刻把 payment_id 写回你自己的订单表,别只存在日志里。对账时它就是唯一能把两边对上的东西。
然后再验三条拦截规则,确认它们真的生效:
```bash
1) 超单笔限额:应被拒
amount 改成 30000(300 元,超过 200 元上限)
2) 非白名单商户:应被拒
merchant_id 改成 mch_unknown
3) 重复提交:应返回同一笔结果,不产生第二次扣款
用同一个 Idempotency-Key 再发一次
```
三条都符合预期,说明限额和白名单是真的在钱包侧生效,不是靠智能体自觉。
第八步:对账
对账做三件事:拉流水、做匹配、看差异。
```bash
拉取某天的钱包流水(分页,注意 page_size 上限)
curl -sS -G "$WALLET_API_BASE/v1/wallets/$WALLET_ID/transactions" \
-H "Authorization: Bearer $WALLET_API_KEY" \
--data-urlencode "from=2024-05-01T00:00:00+08:00" \
--data-urlencode "to=2024-05-01T23:59:59+08:00" \
--data-urlencode "type=payment,refund" \
--data-urlencode "page_size=100" \
| jq -r '.items[] | [.payment_id, .reference, .amount, .currency, .status] | @csv' \
> wallet_20240501.csv
```
匹配用 reference(你的订单号)做左连接:
```python
import csv
def reconcile(wallet_csv: str, orders: dict) -> None:
seen = set()
with open(wallet_csv, newline="") as f:
for row in csv.DictReader(f):
ref = row["reference"]
seen.add(ref)
order = orders.get(ref)
if order is None:
print(f"[钱包有、内部无] {ref} {row['amount']}")
elif int(row["amount"]) != order["amount"]:
print(f"[金额不一致] {ref} 钱包={row['amount']} 内部={order['amount']}")
elif row["status"] != "succeeded":
print(f"[状态异常] {ref} {row['status']}")
for ref in orders.keys() - seen:
print(f"[内部有、钱包无] {ref}")
差异分三类:钱包有内部无、内部有钱包无、金额或状态不一致
```
每日跑一次,把三类差异输出到值班群。差异不要自动冲平,先人工看,前几周大概率是你的 reference 没写进去,或者分页没拉全。
第九步:退款路径
退款要分全额和部分,还要确认退款后限额是否回补。
```bash
全额退款
curl -sS -X POST "$WALLET_API_BASE/v1/payments/<payment_id>/refunds" \
-H "Authorization: Bearer $WALLET_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: refund-<payment_id>-full" \
-d '{"amount": 3900, "reason": "requested_by_customer"}'
部分退款(比如只退一半)
curl -sS -X POST "$WALLET_API_BASE/v1/payments/<payment_id>/refunds" \
-H "Authorization: Bearer $WALLET_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: refund-<payment_id>-partial-1" \
-d '{"amount": 1950, "reason": "partial_service"}'
```
退款要盯的几件事:
- 原路退回:钱回到绑定的支付方式,不是钱包余额。用户看到的到账时间取决于发卡行,平台侧显示“已退款”不等于用户已经收到。
- 多次部分退款不能超过原额,靠
Idempotency-Key和累计校验双重保证。 - 限额是否回补:多数实现里,退款不会冲减当天的“已用额度”,也就是当天退了钱也不能立刻再花出去。这条一定要实测确认,否则会出现“明明退了款还是被拒”的困惑。
- 对账时把退款单独算一类:退款流水通常是负向金额或独立类型,匹配逻辑里要覆盖,否则每天都会冒出假差异。
跑完这九步,做一次完整回归:付 39 元 → 对账匹配 → 全额退款 → 再对账确认净额为 0,且当天已用额度没有异常回补。
常见坑与排错
| 现象 | 常见原因 | 处理方式 |
|---|---|---|
一直拒付,提示 amount_invalid | 金额单位搞错,元当成分或分当成元 | 沙箱下 1 元试一笔,确认单位 |
| 白名单商户也被拒 | mode 没显式设成 allowlist,或商户 ID 用了测试环境的值 | 查策略回显,核对商户 ID 来源 |
| 同一订单扣了两次 | 没传或每次重试都换了 Idempotency-Key | 幂等键由订单号派生,全局固定 |
| 钱包显示成功,商户侧没收到 | 只做了授权(authorize)没做请款(capture) | 确认 capture: true 或补调请款接口 |
| 跨零点被拒 | 日限额按平台时区重置,与你以为的“今天”不一致 | 明确时区口径,跨零点前后避开批量任务 |
| 对账天天有差异 | 分页没拉全 / 退款类型没纳入匹配 | 加总数校验,把 refund 纳入匹配 |
| 回调重复处理 | 未做事件去重,或未验签 | 按事件 ID 去重,回调必须验签 |
| 密钥泄露风险 | 密钥写进代码、日志或提示词 | 只放环境变量,日志脱敏 |
排错顺序建议:先看钱包侧返回的错误码和 decline_code,再看策略配置回显,最后才怀疑 Dots 的工具参数。绝大多数问题在策略配置那一层就能定位。
下一步建议
1. 加一层人工确认:金额超过某个阈值时,让 Dots 生成一条待确认请求,走 IM 或工单审批后再执行,而不是直接付。
2. 做额度预警:当日已用额度到 80% 时推送提醒,别等被拒了才发现。
3. 给每类业务开独立钱包:客服补寄、订阅续费、采购试用各一个,限额和白名单分别设,出了问题只冻结一个。
4. 把对账做成每日任务:定时拉流水、跑匹配、输出差异报表,连续两周零差异再考虑放开额度。
5. 沉淀一份运维手册:谁有权改限额、改完怎么验证、异常怎么冻结钱包、退款争议找谁,写下来比记在脑子里可靠。
最后提醒一句:这套流程的价值不在于“让智能体自己花钱”,而在于把花钱的边界放在智能体改不动的地方。限额、白名单、幂等键、对账,这四样做扎实了,再谈自动化才有意义。具体接口细节与费率规则,以官方文档当前版本为准。
