Communitygithub.com

Django TDD(テスト駆動開発)

pytest-django、TDD手法、factory_boy、モッキング、カバレッジ、そしてDjango REST Framework APIのテストを用いたDjangoテスト戦略。DjangoやDRFのテストをpytest-djangoで記述する際や、Djangoの機能をテストファーストで開発する際に使用します。

Django TDD(テスト駆動開発) とは?

このスキルは、Djangoアプリケーションにおけるテスト駆動開発(TDD)を包括的に支援します。開発者はpytest-django、factory_boy、モッキングを活用し、モデル、ビュー、シリアライザー、DRF API、さらには管理コマンドやCeleryタスクのテストを、Red-Green-Refactorサイクルに沿って体系的に記述できます。pytestの設定や専用のテスト設定、conftestのフィクスチャ、Factory Boyによる柔軟なテストデータ生成、カバレッジ計測といった実践的なテスト基盤を提供します。このスキルは主にClaude CodeやCursorのような開発環境を意識して設計されており、プロンプトテンプレートとして.aiファイルを介して利用されます。テストファーストな開発を促進し、Djangoプロジェクトの品質と保守性を高めることを目的としています。

対応~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/affaan-m/everything-claude-code/tree/main/skills/django-tdd

お気に入りのAIに質問する

このエージェントスキルを事前に読み込んだ状態で新しいチャットを開きます。

ドキュメント

Django TDD(テスト駆動開発)テスト手法

pytest、factory_boy、Django REST Frameworkを使用したDjangoアプリケーションのテスト駆動開発。

アクティベートすべきタイミング

  • 新しいDjangoアプリケーションを作成するとき
  • Django REST Framework APIを実装するとき
  • Djangoのモデル、ビュー、シリアライザーをテストするとき
  • Djangoプロジェクトにテスト基盤を設定するとき

DjangoのためのTDDワークフロー

Red-Green-Refactorサイクル

# ステップ1: RED - 失敗するテストを書く
def test_user_creation():
    user = User.objects.create_user(email='[email protected]', password='testpass123')
    assert user.email == '[email protected]'
    assert user.check_password('testpass123')
    assert not user.is_staff

# ステップ2: GREEN - テストを通過させる
# Userモデルまたはファクトリを作成

# ステップ3: REFACTOR - テストをグリーンに保ちながら改善する

セットアップ

pytest設定

# pytest.ini
[pytest]
DJANGO_SETTINGS_MODULE = config.settings.test
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
addopts =
    --reuse-db
    --nomigrations
    --cov=apps
    --cov-report=html
    --cov-report=term-missing
    --strict-markers
markers =
    slow: テストを遅いとマーク
    integration: テストを統合テストとマーク

テスト設定

# config/settings/test.py
from .base import *

DEBUG = True
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': ':memory:',
    }
}

# マイグレーションを無効化して高速化
class DisableMigrations:
    def __contains__(self, item):
        return True

    def __getitem__(self, item):
        return None

MIGRATION_MODULES = DisableMigrations()

# パスワードハッシュを高速化
PASSWORD_HASHERS = [
    'django.contrib.auth.hashers.MD5PasswordHasher',
]

# メールバックエンド
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'

# Celeryを常に即時実行
CELERY_TASK_ALWAYS_EAGER = True
CELERY_TASK_EAGER_PROPAGATES = True

conftest.py

# tests/conftest.py
import pytest
from django.utils import timezone
from django.contrib.auth import get_user_model

User = get_user_model()

@pytest.fixture(autouse=True)
def timezone_settings(settings):
    """タイムゾーンを一貫させる。"""
    settings.TIME_ZONE = 'UTC'

@pytest.fixture
def user(db):
    """テストユーザーを作成する。"""
    return User.objects.create_user(
        email='[email protected]',
        password='testpass123',
        username='testuser'
    )

@pytest.fixture
def admin_user(db):
    """管理者ユーザーを作成する。"""
    return User.objects.create_superuser(
        email='[email protected]',
        password='adminpass123',
        username='admin'
    )

@pytest.fixture
def authenticated_client(client, user):
    """認証済みクライアントを返す。"""
    client.force_login(user)
    return client

@pytest.fixture
def api_client():
    """DRF APIクライアントを返す。"""
    from rest_framework.test import APIClient
    return APIClient()

@pytest.fixture
def authenticated_api_client(api_client, user):
    """認証済みAPIクライアントを返す。"""
    api_client.force_authenticate(user=user)
    return api_client

Factory Boy

ファクトリのセットアップ

# tests/factories.py
import factory
from factory import fuzzy
from datetime import datetime, timedelta
from django.contrib.auth import get_user_model
from apps.products.models import Product, Category

User = get_user_model()

class UserFactory(factory.django.DjangoModelFactory):
    """Userモデル用ファクトリ。"""

    class Meta:
        model = User

    email = factory.Sequence(lambda n: f"user{n}@example.com")
    username = factory.Sequence(lambda n: f"user{n}")
    password = factory.PostGenerationMethodCall('set_password', 'testpass123')
    first_name = factory.Faker('first_name')
    last_name = factory.Faker('last_name')
    is_active = True

class CategoryFactory(factory.django.DjangoModelFactory):
    """Categoryモデル用ファクトリ。"""

    class Meta:
        model = Category

    name = factory.Faker('word')
    slug = factory.LazyAttribute(lambda obj: obj.name.lower())
    description = factory.Faker('text')

class ProductFactory(factory.django.DjangoModelFactory):
    """Productモデル用ファクトリ。"""

    class Meta:
        model = Product

    name = factory.Faker('sentence', nb_words=3)
    slug = factory.LazyAttribute(lambda obj: obj.name.lower().replace(' ', '-'))
    description = factory.Faker('text')
    price = fuzzy.FuzzyDecimal(10.00, 1000.00, 2)
    stock = fuzzy.FuzzyInteger(0, 100)
    is_active = True
    category = factory.SubFactory(CategoryFactory)
    created_by = factory.SubFactory(UserFactory)

    @factory.post_generation
    def tags(self, create, extracted, **kwargs):
        """タグを製品に追加する。"""
        if not create:
            return
        if extracted:
            for tag in extracted:
                self.tags.add(tag)

ファクトリの使用例

# tests/test_models.py
import pytest
from tests.factories import ProductFactory, UserFactory

def test_product_creation():
    """ファクトリを使用した製品作成のテスト。"""
    product = ProductFactory(price=100.00, stock=50)
    assert product.price == 100.00
    assert product.stock == 50
    assert product.is_active is True

def test_product_with_tags():
    """タグ付き製品のテスト。"""
    tags = [TagFactory(name='electronics'), TagFactory(name='new')]
    product = ProductFactory(tags=tags)
    assert product.tags.count() == 2

def test_multiple_products():
    """複数製品の作成テスト。"""
    products = ProductFactory.create_batch(10)
    assert len(products) == 10

モデルのテスト

モデルテスト

# tests/test_models.py
import pytest
from django.core.exceptions import ValidationError
from tests.factories import UserFactory, ProductFactory

class TestUserModel:
    """Userモデルのテスト。"""

    def test_create_user(self, db):
        """通常ユーザーの作成テスト。"""
        user = UserFactory(email='[email protected]')
        assert user.email == '[email protected]'
        assert user.check_password('testpass123')
        assert not user.is_staff
        assert not user.is_superuser

    def test_create_superuser(self, db):
        """スーパーユーザーの作成テスト。"""
        user = UserFactory(
            email='[email protected]',
            is_staff=True,
            is_superuser=True
        )
        assert user.is_staff
        assert user.is_superuser

    def test_user_str(self, db):
        """ユーザーの文字列表現のテスト。"""
        user = UserFactory(email='[email protected]')
        assert str(user) == '[email protected]'

class TestProductModel:
    """Productモデルのテスト。"""

    def test_product_creation(self, db):
        """製品作成のテスト。"""
        product = ProductFactory()
        assert product.id is not None
        assert product.is_active is True
        assert product.created_at is not None

    def test_product_slug_generation(self, db):
        """自動スラッグ生成のテスト。"""
        product = ProductFactory(name='Test Product')
        assert product.slug == 'test-product'

    def test_product_price_validation(self, db):
        """価格が負にならないことのテスト。"""
        product = ProductFactory(price=-10)
        with pytest.raises(ValidationError):
            product.full_clean()

    def test_product_manager_active(self, db):
        """アクティブマネージャーメソッドのテスト。"""
        ProductFactory.create_batch(5, is_active=True)
        ProductFactory.create_batch(3, is_active=False)

        active_count = Product.objects.active().count()
        assert active_count == 5

    def test_product_stock_management(self, db):
        """在庫管理のテスト。"""
        product = ProductFactory(stock=10)
        product.reduce_stock(5)
        product.refresh_from_db()
        assert product.stock == 5

        with pytest.raises(ValueError):
            product.reduce_stock(10)  # 在庫が足りない

ビューのテスト

Djangoビューのテスト

# tests/test_views.py
import pytest
from django.urls import reverse
from tests.factories import ProductFactory, UserFactory

class TestProductViews:
    """製品ビューのテスト。"""

    def test_product_list(self, client, db):
        """製品一覧ビューのテスト。"""
        ProductFactory.create_batch(10)

        response = client.get(reverse('products:list'))

        assert response.status_code == 200
        assert len(response.context['products']) == 10

    def test_product_detail(self, client, db):
        """製品詳細ビューのテスト。"""
        product = ProductFactory()

        response = client.get(reverse('products:detail', kwargs={'slug': product.slug}))

        assert response.status_code == 200
        assert response.context['product'] == product

    def test_product_create_requires_login(self, client, db):
        """製品作成には認証が必要であることのテスト。"""
        response = client.get(reverse('products:create'))

        assert response.status_code == 302
        assert response.url.startswith('/accounts/login/')

    def test_product_create_authenticated(self, authenticated_client, db):
        """認証済みユーザーによる製品作成のテスト。"""
        response = authenticated_client.get(reverse('products:create'))

        assert response.status_code == 200

    def test_product_create_post(self, authenticated_client, db, category):
        """POSTによる製品作成のテスト。"""
        data = {
            'name': 'Test Product',
            'description': 'A test product',
            'price': '99.99',
            'stock': 10,
            'category': category.id,
        }

        response = authenticated_client.post(reverse('products:create'), data)

        assert response.status_code == 302
        assert Product.objects.filter(name='Test Product').exists()

DRF APIのテスト

シリアライザーのテスト

# tests/test_serializers.py
import pytest
from rest_framework.exceptions import ValidationError
from apps.products.serializers import ProductSerializer
from tests.factories import ProductFactory

class TestProductSerializer:
    """ProductSerializerのテスト。"""

    def test_serialize_product(self, db):
        """製品のシリアライズテスト。"""
        product = ProductFactory()
        serializer = ProductSerializer(product)

        data = serializer.data

        assert data['id'] == product.id
        assert data['name'] == product.name
        assert data['price'] == str(product.price)

    def test_deserialize_product(self, db):
        """製品データのデシリアライズテスト。"""
        data = {
            'name': 'Test Product',
            'description': 'Test description',
            'price': '99.99',
            'stock': 10,
            'category': 1,
        }

        serializer = ProductSerializer(data=data)

        assert serializer.is_valid()
        product = serializer.save()

        assert product.name == 'Test Product'
        assert float(product.price) == 99.99

    def test_price_validation(self, db):
        """価格バリデーションのテスト。"""
        data = {
            'name': 'Test Product',
            'price': '-10.00',
            'stock': 10,
        }

        serializer = ProductSerializer(data=data)

        assert not serializer.is_valid()
        assert 'price' in serializer.errors

    def test_stock_validation(self, db):
        """在庫が負にならないことのバリデーションテスト。"""
        data = {
            'name': 'Test Product',
            'price': '99.99',
            'stock': -5,
        }

        serializer = ProductSerializer(data=data)

        assert not serializer.is_valid()
        assert 'stock' in serializer.errors

API ViewSetのテスト

# tests/test_api.py
import pytest
from rest_framework.test import APIClient
from rest_framework import status
from django.urls import reverse
from tests.factories import ProductFactory, UserFactory

class TestProductAPI:
    """Product APIエンドポイントのテスト。"""

    @pytest.fixture
    def api_client(self):
        """APIクライアントを返す。"""
        return APIClient()

    def test_list_products(self, api_client, db):
        """製品一覧のテスト。"""
        ProductFactory.create_batch(10)

        url = reverse('api:product-list')
        response = api_client.get(url)

        assert response.status_code == status.HTTP_200_OK
        assert response.data['count'] == 10

    def test_retrieve_product(self, api_client, db):
        """製品詳細の取得テスト。"""
        product = ProductFactory()

        url = reverse('api:product-detail', kwargs={'pk': product.id})
        response = api_client.get(url)

        assert response.status_code == status.HTTP_200_OK
        assert response.data['id'] == product.id

    def test_create_product_unauthorized(self, api_client, db):
        """認証なしでの製品作成テスト。"""
        url = reverse('api:product-list')
        data = {'name': 'Test Product', 'price': '99.99'}

        response = api_client.post(url, data)

        assert response.status_code == status.HTTP_401_UNAUTHORIZED

    def test_create_product_authorized(self, authenticated_api_client, db):
        """認証済みユーザーによる製品作成テスト。"""
        url = reverse('api:product-list')
        data = {
            'name': 'Test Product',
            'description': 'Test',
            'price': '99.99',
            'stock': 10,
        }

        response = authenticated_api_client.post(url, data)

        assert response.status_code == status.HTTP_201_CREATED
        assert response.data['name'] == 'Test Product'

    def test_update_product(self, authenticated_api_client, db):
        """製品更新のテスト。"""
        product = ProductFactory(created_by=authenticated_api_client.user)

        url = reverse('api:product-detail', kwargs={'pk': product.id})
        data = {'name': 'Updated Product'}

        response = authenticated_api_client.patch(url, data)

        assert response.status_code == status.HTTP_200_OK
        assert response.data['name'] == 'Updated Product'

    def test_delete_product(self, authenticated_api_client, db):
        """製品削除のテスト。"""
        product = ProductFactory(created_by=authenticated_api_client.user)

        url = reverse('api:product-detail', kwargs={'pk': product.id})
        response = authenticated_api_client.delete(url)

        assert response.status_code == status.HTTP_204_NO_CONTENT
        assert not Product.objects.filter(id=product.id).exists()

管理コマンドのテスト

# tests/test_commands.py
import pytest
from io import StringIO
from django.core.management import call_command
from tests.factories import ProductFactory

class TestManagementCommands:
    """管理コマンドのテスト。"""

    def test_export_products_command(self, db):
        """製品エクスポートコマンドのテスト。"""
        ProductFactory.create_batch(5)

        out = StringIO()
        call_command('export_products', '--format=csv', stdout=out)

        output = out.getvalue()
        assert 'name' in output
        assert 'price' in output

    def test_cleanup_expired_products(self, db):
        """期限切れ製品のクリーンアップコマンドテスト。"""
        ProductFactory(is_active=False, updated_at=timezone.now() - timedelta(days=30))
        ProductFactory(is_active=True)

        out = StringIO()
        call_command('cleanup_products', '--days=7', stdout=out)

        assert 'Cleaned up 1 expired products' in out.getvalue()

Celeryタスクのテスト

# tests/test_tasks.py
import pytest
from unittest.mock import patch, Mock
from apps.products.tasks import send_stock_alert, update_product_prices

@pytest.mark.django_db
class TestCeleryTasks:
    """Celeryタスクのテスト。"""

    @patch('apps.products.tasks.send_mail')
    def test_send_stock_alert(self, mock_send_mail, db, product):
        """在庫アラートメール送信タスクのテスト。"""
        result = send_stock_alert.delay(product.id)

        assert result.successful()
        mock_send_mail.assert_called_once()

    @patch('apps.products.tasks.Product')
    def test_update_product_prices(self, mock_product, db):
        """製品価格一括更新タスクのテスト。"""
        mock_queryset = Mock()
        mock_product.objects.filter.return_value = mock_queryset
        mock_queryset.update.return_value = 5

        result = update_product_prices.delay(10.0)

        assert result.successful()
        mock_product.objects.filter.assert_called_once_with(is_active=True)
        mock_queryset.update.assert_called_once_with(price=F('price') * (1 + 10.0 / 100))

モッキング

unittest.mock を使ったモッキング

# tests/test_mocking.py
import pytest
from unittest.mock import patch, Mock, MagicMock, PropertyMock
from django.test import override_settings
from tests.factories import ProductFactory

class TestExternalServices:
    """外部サービスとの連携テスト(モッキング使用)"""

    @patch('apps.products.services.PaymentGateway.charge')
    def test_payment_processing(self, mock_charge, db):
        """支払い処理のテスト。"""
        mock_charge.return_value = {'status': 'success', 'transaction_id': 'txn_123'}

        from apps.products.services import process_payment
        result = process_payment(user_id=1, amount=99.99)

        assert result['status'] == 'success'
        mock_charge.assert_called_once()

    @patch('requests.post')
    def test_send_notification(self, mock_post, db):
        """通知送信のテスト。"""
        mock_response = Mock()
        mock_response.status_code = 200
        mock_post.return_value = mock_response

        from apps.products.services import send_order_notification
        result = send_order_notification(order_id=1)

        assert result is True
        mock_post.assert_called_once()

    @patch('apps.products.models.Product.objects.filter')
    def test_expensive_query_mocking(self, mock_filter):
        """高価なクエリのモッキングテスト。"""
        mock_filter.return_value.count.return_value = 5

        from apps.products.services import get_popular_products
        result = get_popular_products()

        assert len(result) == 5

    @patch('apps.products.tasks.send_email.delay')
    def test_async_task(self, mock_delay, db):
        """非同期タスクのモッキングテスト。"""
        from apps.products.tasks import send_welcome_email
        send_welcome_email(user_id=1)

        mock_delay.assert_called_once_with(1)

コンテキストマネージャーとしてのモッキング

def test_with_context_manager(db):
    """コンテキストマネージャーを使ったモッキングのテスト。"""
    with patch('apps.products.services.PaymentGateway.charge') as mock_charge:
        mock_charge.return_value = {'status': 'success'}

        from apps.products.services import process_payment
        result = process_payment(user_id=1, amount=50.0)

        assert result['status'] == 'success'

    # テスト終了後、元の関数が復元される

def test_with_multiple_mocks(db):
    """複数モックを使ったテスト。"""
    with patch.multiple(
        'apps.products.services',
        PaymentGateway=Mock(),
        NotificationService=Mock()
    ):
        from apps.products.services import process_payment
        result = process_payment(user_id=1, amount=50.0)

        assert result is not None

カバレッジ

カバレッジ設定

# .coveragerc
[run]
source = apps
omit =
    */migrations/*
    */tests/*
    */admin.py
    */apps.py
    */wsgi.py
    */asgi.py
    manage.py

[report]
show_missing = True
skip_covered = True
precision = 2
fail_under = 80
exclude_lines =
    pragma: no cover
    def __repr__
    raise NotImplementedError
    if __name__ == .__main__.:

カバレッジの実施

# テスト実行とカバレッジレポート生成
pytest --cov=apps --cov-report=html --cov-report=term-missing

# 特定のファイルのみカバレッジを表示
pytest --cov=apps.products --cov=apps.orders

# カバレッジをHTMLとして出力
pytest --cov=apps --cov-report=html
open htmlcov/index.html

ベストプラクティス

テスト構造

# 整理されたテスト
class TestUserAPI:
    """User APIのテスト。"""

    def test_list_users(self): ...
    def test_create_user(self): ...
    def test_update_user(self): ...
    def test_delete_user(self): ...

# Fixtureを使った整然としたテスト
class TestProductAPI:
    @pytest.fixture(autouse=True)
    def setup_method(self, db):
        self.product = ProductFactory()
        self.url = reverse('api:product-list')

    def test_list_products(self, api_client):
        response = api_client.get(self.url)
        assert response.status_code == 200

テストの命名規則

# 明確で説明的なテスト名
def test_user_can_create_product(): ...
def test_unauthenticated_user_cannot_access_admin(): ...
def test_product_out_of_stock_raises_error(): ...
def test_price_update_reflects_discount(): ...

トラブルシューティング

一般的な問題

問題解決策
データベースエラー--reuse-db フラグを確認し、テスト設定が :memory: を指していることを確認
認証の問題force_login, force_authenticate, または認証ヘッダーが正しく使用されていることを確認
モックが効かないpatch デコレータのパスが正しいか確認(インポート元ではなく、使用元でパッチ)
マイグレーションの競合--nomigrations フラグを使用してテストを高速化
謎めいたエラー最初にデータベースをフラッシュしてからテストを実行

デバッグのヒント

# 環境変数を確認
import os
print(os.environ.get('DJANGO_SETTINGS_MODULE'))

# 利用可能なデータベースを確認
from django.db import connection
print(connection.settings_dict)

# 現在のデータベース接続を確認
from django.db import connections
print(connections.databases)

Individual skills in this repo

This repo contains 20 individual skills — each has its own dedicated page.

accessibility

Design, implement, and audit inclusive digital products using WCAG 2.2 Level AA. Use when building or auditing UI that must meet WCAG 2.2 Level AA, or when reviewing a change for keyboard, contrast, or screen-reader support.

affaan-m/claude-api

Anthropic Claude API patterns for Python and TypeScript. Covers Messages API, streaming, tool use, vision, extended thinking, batches, prompt caching, and Claude Agent SDK. Use when building applications with the Claude API or Anthropic SDKs.

affaan-m/everything-claude-code

End-to-end marketing campaign planning and execution. Covers audience research, positioning, campaign angle definition, landing page copy, email sequences, social posts, ad copy, short-form video scripts, and content calendars. Use as the orchestration layer for multi-channel product launches. Use when planning or executing a multi-channel product launch, or producing landing page, email, social, or ad copy.

affaan-m/everything-claude-code

Development conventions and patterns for everything-claude-code. JavaScript project with conventional commits.

affaan-m/everything-claude-code-conventions

Development conventions and patterns for everything-claude-code. JavaScript project with conventional commits.

affaan-m/frontend-design

Create distinctive, production-grade frontend interfaces with high design quality. Use when the user asks to build web components, pages, or applications and the visual direction matters as much as the code quality.

affaan-m/gget

gget CLI and Python workflow for quick genomic database queries, sequence lookup, BLAST-style searches, enrichment checks, and reproducible bioinformatics evidence logs.

affaan-m/literature-review

Systematic literature-review workflow for academic, biomedical, technical, and scientific topics, including search planning, source screening, synthesis, citation checks, and evidence logging.

affaan-m/motion-ui

Production-ready UI motion system for React/Next.js. Use when implementing animations, transitions, or motion patterns.

affaan-m/project-guidelines-example

Example project-specific skill template based on a real production application.

affaan-m/pubmed-database

Direct PubMed and NCBI E-utilities search workflows for biomedical literature, MeSH queries, PMID lookup, citation retrieval, and API-backed literature monitoring.

affaan-m/scholar-evaluation

Structured scholarly-work evaluation for papers, proposals, literature reviews, methods sections, evidence quality, citation support, and research-writing feedback.

affaan-m/uspto-database

USPTO patent and trademark data workflow for official record lookup, PatentSearch queries, TSDR checks, assignment data, and reproducible IP research logs.

agent-architecture-audit

Full-stack diagnostic for agent and LLM applications. Audits the 12-layer agent stack for wrapper regression, memory pollution, tool discipline failures, hidden repair loops, and rendering corruption. Produces severity-ranked findings with code-first fixes. Essential for developers building agent applications, autonomous loops, or any LLM-powered feature. Use when an agent or LLM feature misbehaves and the failing layer is unknown, or before shipping an agent stack.

agent-eval

Head-to-head comparison of coding agents (Claude Code, Aider, Codex, etc.) on custom tasks with pass rate, cost, time, and consistency metrics. Use when choosing between coding agents, or when a change to an agent setup needs measured pass rate, cost, and time rather than an impression.

agent-harness-construction

Design and optimize AI agent action spaces, tool definitions, and observation formatting for higher completion rates. Use when defining or revising an agent

agentic-engineering

Operate as an agentic engineer using eval-first execution, decomposition, and cost-aware model routing. Use when planning or executing engineering work that agents will carry out end to end.

agentic-os

Build persistent multi-agent operating systems on Claude Code. Covers kernel architecture, specialist agents, slash commands, file-based memory, scheduled automation, and state management without external databases. Use when building a persistent multi-agent system on Claude Code with its own memory, commands, and scheduling.

agent-introspection-debugging

Structured self-debugging workflow for AI agent failures using capture, diagnosis, contained recovery, and introspection reports. Use when an agent run fails and you need a reproducible diagnosis instead of a retry.

agent-payment-x402

Add x402 payment execution to AI agents with per-task budgets, spending controls, and non-custodial wallets. Supports Base through agentwallet-sdk and X Layer through OKX Payments / OKX Agent Payments Protocol. Use when an agent must pay for something itself and needs per-task budgets, spending controls, and a non-custodial wallet.

関連スキル